链表的介绍
链表与顺序表一样,也属于线性表。
一个线性表是某类数据元素的一个集合,表里同时记录着元素之间的顺序关系。
线性表的数据之间有顺序关系,顺序关系分为两种,一种是物理有序,即数据物理存储的位置顺序与数据之间的顺序关系一致,另一种是逻辑有序,即数据之间的顺序关系是由某种逻辑关系(如指针)来决定的,与物理存储的位置无关。
顺序表是物理有序,而链表是逻辑有序。
链表的分类
单向链表
最简单的链表是单向链表。
单向链表也叫单链表,是链表中最简单的一种形式,它的每个节点包含两个内容,一个用来存储数据,另一个用来存指针,指针记录下一个节点的地址,最后一个节点的指针则为空。
单向链表也分为头节点(也叫哨兵)和不带头节点两种,头节点的主要作用是简化边缘条件,让代码更容易处理或跟高效,头节点中不存数据,只保存下一个节点的地址。
![](https://i-blog.csdnimg.cn/blog_migrate/18411e15bd02bd8818b032323d04ff43.png)
双向链表
双向链表又称双面链表,相对于单向链表,双向链表除了向后的指针,还有向前的指针。
每个节点包含三个内容,一个存数据,两个存指针。一个指针指向前一个节点,当此节点为第一个节点时,指向空值,另一个指针指向下一个节点,当此节点为最后一个节点时,指向空值。
![](https://i-blog.csdnimg.cn/blog_migrate/f170f6cbdccee6e261cde34a1eb98a8b.png)
循环链表
循环链表是将单向链表“首尾相连”。
单向链表中最后一个节点的指针指向的是空,而单向循环链表中,最后一个节点的指针指向之前的节点或者链表的头节点,形成一个“环形”的链。
![](https://i-blog.csdnimg.cn/blog_migrate/d025152898d2566afaa204c291cf7103.png)
带头双向链表是指结构体中包含两个指针,一个指向上一个节点,一个指向下一个节点,并且拥有哨兵节点的链表,吸收了所有类型链表的有点,理论上能完美解决链表的所有缺点
![](https://i-blog.csdnimg.cn/blog_migrate/eb21a0e2a1ae5dbeadd49cd75e144155.png)
链表的实现
ListCreate(创建并初始化链表)
创建一个结构体,因为是双向链表,所以结构体中放入了两个指针
函数的返回值用结构体指针接收,避免指针出了函数就销毁。也可以传二级指针代替返回值
if是防止头节点创建失败,一般小内存的节点创建并不会失败,所以后面的创建并没有检查
typedef int ListDataType;
struct ListNode
{
ListDataType data;//存放数据
struct ListNode* next;//存下一个节点的地址
struct ListNode* prev;//存上一个节点的地址
};
struct ListNode* ListCreate()
{
struct ListNode* guard = (struct ListNode*)malloc(sizeof(struct ListNode));
if (guard == NULL)//防止头节点创建失败
{
perror("malloc fail");
exit(-1);
}
guard->next = guard;
guard->prev = guard;
return guard;
}
BuyListNode(创建节点)
因为要多次创建节点,所以单独写一个函数来创建节点方便调用,新创建的节点的内存不需要过多处理,这样可以保证函数的简洁,节点的指针在调用函数时再做处理
struct ListNode* BuyListNode(ListDataType x)
{
struct ListNode* newnode = (struct ListNode*)malloc(sizeof(struct ListNode));
newhead->next = NULL;//创建的节点不需要在函数中赋值
newhead->prev = NULL;
return newnode;
}
ListInsert(在pos处前增加一个节点)
写好插入以后头插和尾插不需要单独再写,直接调用函数即可
这里建议用一个指针记录上一个节点的位置,防止修改指针时找不到对应位置
void ListInsert(struct ListNode* pos, ListDataType x)
{
assert(pos);
struct ListNode* newnode = BuyListNode(x);
newnode->data = x;
struct ListNode * cur = pos->prev;
cur->next = newnode;
newnode->prev = cur;
newnode->next = pos;
pos->prev = newnode;
}
ListErase(删除pos位置)
用两个指针记录前后节点的位置,以免修改时找不到对应位置
空链表和头节点不能删除要多一个检查
void ListErase(struct ListNode* pos)
{
assert(pos);
assert(pos->next != pos);
struct ListNode* next = pos->next;
struct ListNode* prev = pos->prev;
prev->next = next;
next->prev = prev;
free(pos);
}
ListPushFront(头插)
为了方便理解把头插等代码的方法单独写
方法一:先对newhead->next进行修改,不然会出现找不到对应节点的情况
方法二:创建一个额外的指针指向phead->next ,这样就不需要考虑顺序
方法三:直接调用ListInsert函数
void ListPushFront(struct ListNode* phead, LTDataType x)
{
assert(phead);//防止传错
//方法一
struct ListNode* newnode = BuyListNode(x);
newnode->next = phead->next;//优先修改
phead->next->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
//方法二
//struct ListNode* newnode = BuyListNode(x);
//struct ListNode* first = phead->next; // 创建一个指针指向下一个节点
//phead->next = newnode;
//newnode->prev = phead;
//newnode->next = first;
//first->prev = newnode;
//方法三
//ListInsert(phead->next, x);
}
ListPushBack(尾插)
由于是双向链表,所以尾插非常方便,直接从头节点就可以找到尾节点
和头插相同,也是三种方法,这里展示一种
void ListPushBack(LTNode* phead, ListDataType x)
{
assert(phead);//防止传错
LTNode* tail = phead->prev;
LTNode* newnode = BuyListNode(x);
//将新节点插入
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
ListPopFront(头删)
空链表和头节点时不能删除
由于链表一般不需要单独写头插头删和尾插尾删,所以这里判断空列表没有单独封装函数
void ListPopFront(LTNode* phead)
{
assert(phead);//防止传错
assert(phead->next != phead);//判断链表不为空
LTNode* cur = phead->next->next;
LTNode* del = phead->next;
phead->next = cur;
cur->prev = phead;
free(del);
}
ListPopBack(尾删)
最后展示一下为什么说不需要单独写头插等函数,直接调用即可,非常方便
void ListPopBack(struct ListNode* phead)
{
ListErase(phead->prev);
}
ListDestory(销毁链表)
不能直接把头指针释放,这样会造成内存泄漏,应该遍历链表把每一个节点单独释放
用两个指针记录位置,一个指针用来释放节点,另一个指针用来记录被释放的节点的下一个节点
头指针需要在外面单独处理,防止野指针
void ListDestory(struct ListNode* phead)
{
assert(phead);
struct ListNode* first = phead->next;
struct ListNode* second = first->next;
while (first != phead)
{
free(first);
first = second;
second = second->next;
}
free(phead);
}