数据结构之链表
什么是链表
链表一种时间复杂度和空间复杂度为线性的数据结构,通过指针将一个个零散的内存块连接起来,链表的每个内存块称为结点。
链表的种类主要有三种,单链表,双链表,双向循环链表。
链表的优缺点
链表是一种链式结构,使得它对内存没有太大的要求。可以充分使用散乱的内存空间,插入和删除的时间复杂度比数组好。但是结构实现起来比较复杂,容易出错。而且不具备随机访问的特性,每次查询都要从头节点出发。需要付出额外的空间去保存下一个元素的指针。
单链表
单链表的每个节点有一个保存值的属性,还有一个指向下一个节点的指针。
从头节点出发可以完整的遍历整个链表。
#include <stdio.h>
#include <stdlib.h>
typedef struct ListNode
{
struct ListNode *next;
int val;
} ListNode;
基本操作实现
初始化
void init(ListNode **head)
{
if (head == NULL)
{
return;
}
//为什么这里是ListNode ** head?
*head = (ListNode *)malloc(sizeof(ListNode));
(*head)->next = NULL;
(*head)->val = -1;
}
为什么初始化的时候是List ** 类型
为什么这里是ListNode ** head?
首先我们在main函数中定义的是LisNode * head,这个是指针
如果我们函数传参的时候传递的是 head 也就是 ListNode * 类型的
那我们的init函数中的head实际上是 main.head 的拷贝
main.head.address != init.head.address (两个变量的地址不等,后续指向发生改变也不会同步)
当我们分配空间之后 init.head 指向的是我们的申请分配的空间
而 main.head 依然是 NULL
当传入的是ListNode ** 时,init.head 指向的是main.head 的地址
当*init.he ad 指向新分配的空间时,main.head 的指向也发生了改变
一个原则 : 当你想改变一个变量的值时,就传递它的地址
增加元素
void insertNode(ListNode *head, int val, int index)
{
if (head == NULL)
{
return;
}
ListNode *h = head;
//定位到插入位置的前一个位置
while (index > 0 && h->next != NULL)
{
h = h->next;
index--;
}
ListNode *node = (ListNode *)malloc(sizeof(ListNode));
node->val = val;
//开始插入数据
node->next = h->next;
h->next = node;
}
删除元素
void deleteNode(ListNode *head, int index)
{
if (head == NULL)
{
return;
}
ListNode *h = head;
while (index > 0 && h->next != NULL)
{
h = h->next;
index--;
}
//确保是删除最后一个元素
if (h->next != NULL)
{
ListNode *next = h->next;
h->next = h->next->next;
// 记得释放空间
free(next);
}
}
双向链表
既然有了单链表,为什么还还要双链表呢?
单链表不管元素的在前面还是后面都需要从头节点出发,使用双链表我们可以根据元素的位置选择从头节点开始出发或者尾节点出发。
这里实现的只带头节点的双向链表。
结构定义
#include <stdio.h>
#include <stdlib.h>
typedef struct ListNode
{
struct ListNode *last;
struct ListNode *next;
int val;
} ListNode;
基本操作实现
链表初始化
void init(ListNode **head)
{
*head = (ListNode *)malloc(sizeof(ListNode));
(*head)->last = NULL;
(*head)->next = NULL;
(*head)->val = -1;
}
插入操作
void insertNode(ListNode *head, int val, int index)
{
if (head == NULL || index < 0)
{
return;
}
ListNode *h = head;
//定位到要新元素的前一个位置
while (index > 0 && h->next != NULL)
{
h = h->next;
index--;
}
ListNode *node = (ListNode *)malloc(sizeof(ListNode));
node->val = val;
node->next = h->next;
// 如果当前位置之后还有元素,更新下个元素的指向
if(node->next != NULL){
h->next->last = node;
}
node->last = h;
h->next = node;
}
删除操作
void deleteNode(ListNode *head, int index)
{
if (head == NULL || index < 0)
{
return;
}
ListNode *h = head;
//定位到要删除的元素的前一个位置
while (index > 0 && h->next != NULL)
{
h = h->next;
index--;
}
if (h->next != NULL)
{
//保存要删除的元素 释放空间
ListNode *next = h->next;
// 如果要函删除的元素之后还有元素 更新指向
if(h->next->next != NULL){
h->next->next->last = h;
}
h->next = h->next->next;
free(next);
}
}
循环链表
尾指针指向的不是 NULL,而是指向我们的头节点。我们从头节点出发可以循环遍历到头节点。
如何快速写出正确的链表
链表的难度主要在元素发生变动时前后指针的更新。我们初学时不太熟悉,可以在纸上画出相应的图形去决定指针变换的顺序。同时需要多加联系,面试中经常遇到与链表相关的题目。
循环链表和带尾节点的链表这里就不实现了,大家可以在这个基础上自己去改造实现相应的数据结构。
尾插法和头插法
尾插法即将新元素插入在链表的末尾,头插即插入在头节点的下一个位置。
链表的时间复杂度一般为O(n),空间复杂度为O(n)
数据结构中相关的代码我已上传到gitlab:
https://gitlab.com/BitLegend/c-data-structure.git
此后的数据结构代码我都会上传在这个仓库中,需要的同学可以自己下载
感谢你能看到这里,欢迎关注我的vx公众号:BitLegend,学习更多的知识!我们下期再见!