大家好,经过单链表的学习,接下来我们将学习双链表的内容!
首先,给大家展示一下双链表的结构,也就是带头双向循环链表:
注意:这⾥的“带头”跟前⾯我们说的“头节点”是两个概念,实际前⾯的在单链表阶段称呼不严谨,但是为了更好的理解就直接称为单链表的头节点。
带头链表⾥的头节点,实际为“哨兵位”,哨兵位节点不存储任何有效元素,只是站在这⾥“放哨的”
“哨兵位”存在的意义:
遍历循环链表避免死循环。
双向链表的实现
双链表的结构
struct ListNode{
LTDataType data;//存储数据
struct ListNode* next;//连接下一个节点
struct ListNode* prev;//连接上一个节点
}
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int LTDataType;//数据类型
typedef struct ListNode {
LTDataType data;
struct ListNode* next;
struct ListNode* prev;
}LTNode;
void LTPrint(LTNode* phead);//打印双链表
LTNode* LTInit();//初始化双链表
void LTDestroy(LTNode* phead);//销毁双链表
LTNode* ListBuyNode(LTDataType x);//创建新节点
void LTPushBack(LTNode* phead, LTDataType x);//尾插
void LTPushFront(LTNode* phead, LTDataType x);//头插
void LTPopBack(LTNode* phead);//尾删
void LTPopFront(LTNode* phead);//头删
void LTInsert(LTNode* pos, LTDataType x);//pos位置之后插入
void LTErase(LTNode* pos);//pos位置元素删除
LTNode* LTFind(LTNode* phead,LTDataType x);//找到指定节点
以上就是需要实现的函数
我们发现,双链表中传入的只是一级指针,而不像单链表一样传的是二级指针,这是为什么呢?
双链表(双向链表)与单链表最大的不同之处在于,每个节点不仅有一个指向后继节点的next
指针,还有一个指向前驱节点的prev
指针。这使得在双链表中可以通过next
和prev
指针轻松地访问、插入或删除前后节点。
相比之下,单链表只有
next
指针,无法直接获取前驱节点,在插入或删除节点时通常需要更改节点的地址。
因此,在使用双链表时,只需要传递一个指向头节点的一级指针就足够了,通过
next
和prev
指针,可以准确地访问、插入和删除双链表中的元素,而无需改变地址。这样的设计使得代码更加简洁和高效。
需要注意的是,在某些情况下,可能仍然需要使用二级指针(指向整个双链表的指针),特别是在操作头节点或删除整个链表等情况下。
打印双链表
void LTPrint(LTNode* phead) {
assert(phead);//头节点不为空
LTNode* pcur = phead->next;//从哨兵位的下一位开始
while (pcur != phead)//pcur走到头节点的时候结束循环
{
printf("%d->", pcur->data);
pcur = pcur->next;//pcur往后遍历
}
printf("\n");
}
示意图:
其实就是pcur从头节点的后一个元素开始向后遍历,依次打印链表中的数据
双链表的初始化
LTNode* LTInit() {
LTNode* phead = (LTNode*)malloc(sizeof(LTNode));//创建头节点
phead->data = -1;//这个没关系,哨兵位的值无用
phead->next = phead;
phead->prev = phead;//头节点的next和prev节点都指向自己
return phead;
}
示意图:
销毁双链表
void LTDestroy(LTNode* phead) {
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)//pcur遍历双链表
{
LTNode* next = pcur->next;//先把pcur的下一个节点存储下来
free(pcur);
pcur = NULL;
pcur = next;//pcur走向下一个节点
}
free(phead);//最后要把头节点(哨兵位)也释放掉
phead = NULL;
}
这里有一点需要注意,那就是当我们使用函数销毁双链表的时候,会发现一个问题:
我们会发现phead显然已经为空,而plist的后面的数据也已经清空,但是plist这个头节点却并没有置为空,但我们在销毁函数中明明已经写过了,这实在令人疑惑。
其实,在该销毁函数中,不能直接通过将 phead 设置为 NULL 来修改传入的头节点指针。这是因为 phead 是函数参数 LTNode* phead 的副本,对其进行修改不会影响原本的指针。
如果要修改原本的头节点指针,需要传入一个指向头节点指针的指针(即二级指针),并在函数内部通过间接访问来修改原本的指针。
但是传二级指针,又会影响接口的一致性,而我们的用户并不知道一级指针和二级指针的区别,这样子,使用起来就会很麻烦,所以,我们选择的方法是,在销毁之后,手动给plist置为空,而不使用二级指针,保证接口的一致性。
创建新节点
LTNode* ListBuyNode(LTDataType x) {
LTNode* node = (LTNode*)malloc(sizeof(LTNode));//动态开辟空间
if (node == NULL)//若开辟失败
{
perror("malloc");
return;
}
node->data = x;//给节点赋值
node->next = node->prev = NULL;//后继节点与前驱节点都指向空
return node;//返回节点
}
头插
void LTPushFront(LTNode* phead, LTDataType x) {
assert(phead);
LTNode* node = ListBuyNode(x);//先拿到新节点
//先处理node
node->prev = phead;//node的前一个节点是头节点
node->next = phead->next;//node的下一个节点是本来头节点的节点,也就是1
//在处理其他节点
phead->next->prev = node;//头节点的下一个节点的前一个节点不再是头节点,而是node
phead->next = node;//头节点的下一个节点是node
}
示意图:
头删
void LTPopFront(LTNode* phead) {
assert(phead);
assert(phead->next != phead);//删除都需要考虑是否还有节点可删
LTNode* del = phead->next;
del->next->prev = phead;//要头删的节点的下一个节点的前一个节点不再是要删除的节点,而是头节点
phead->next = del->next;//头节点的下一个节点变成要头删的节点的下一个节点
free(del);//删除节点
del = NULL;
}
示意图:
尾插
void LTPushBack(LTNode* phead, LTDataType x) {
assert(phead);
LTNode* node = ListBuyNode(x);//新节点
node->next = phead;//node的下一个节点就是头节点
node->prev = phead->prev;//node的前一个节点就是头节点原本的前一个节点
phead->prev->next = node;//头节点原本的前一个节点的下一个节点变为node
phead->prev = node;//头节点现在的前一个节点变为node
}
示意图:
尾删
void LTPopBack(LTNode* phead) {
assert(phead);
assert(phead->next != phead);
LTNode* del = phead->prev;
del->prev->next = phead;//del的前一个节点的下一个节点变成头节点
phead->prev = del->prev;//头节点的前一个节点变成del的前一个节点
free(del);//删除节点
del = NULL;
}
示意图:
在pos节点之后插入数据
void LTInsert(LTNode* pos, LTDataType x) {
assert(pos);
LTNode* node = ListBuyNode(x);//创建新节点
node->next = pos->next;//node的下一个节点是pos的下一个节点
node->prev = pos;//node的前一个节点连接pos
pos->next = node;//pos节点现在的下一个节点连接node
node->next->prev = node;//node节点的下一个节点的前一个节点现在不再是pos的下一个节点,而是与node相连
}
示意图:
删除pos节点的数据
void LTErase(LTNode* pos) {
assert(pos);
pos->prev->next = pos->next;//pos的前一个节点的的下一个节点改为pos的下一个节点
pos->next->prev = pos->prev;//pos的下一个节点的前一个节点变为pos的前一个节点
free(pos);//删除pos
pos = NULL;
}
示意图:
找到指定数据在链表中的位置
LTNode* LTFind(LTNode* phead,LTDataType x) {
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)//遍历链表
{
if (pcur->data == x)
return pcur;//找到了
pcur = pcur->next;
}
return NULL;//没找到返回空
}
相信通过上述的讲解,大家对于双链表一定多了些了解,而下面这张表,则是比较单链表和顺序表的差异的,大家也可以仔细看看!希望看完这篇文章大家都能学到东西!
特点 | 顺序表 | 链表(单链表) |
---|---|---|
存储空间上 | 物理上一定连续 | 物理上不一定连续 |
随机访问 | 支持O(1) | 不支持:O(N) |
任意位置插入和删除 | 可能需要搬移元素,效率低 O(N) | 只需修改指针指向 |
插入 | 动态顺序表,空间不够时需要扩容 | 没有容量的概念 |
应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除频繁 |