给链表装上“后视镜”:深入理解双向链表的自由与高效
前言:解锁数据结构新维度——深入探索双向链表
在日常的编程中,我们早已习惯了数组的简单直接,也领略了单链表的轻盈灵活。但你是否曾遇到过这样的困境:
想要删除单链表中的某个节点,却不得不从表头开始苦苦遍历,寻找它那“失联”的前驱节点?
在需要逆向遍历链表时,只能无奈地感叹“假如能倒着来该多好”?
面对需要频繁前后移动、插入删除的复杂数据关系时,感觉单链表有些“力不从心”?
如果你曾有过上述任何一丝念头,那么恭喜你,你即将打开一扇新世界的大门。
单链表如同一条只能向前的单行道,高效但缺乏回旋的余地。而双向链表则为我们提供了更优雅的解决方案。它像是为每个节点都配备了“前视镜”和“后视灯”,不仅知道下一个节点在哪,还清楚地记得上一个节点是谁。这种设计的巧妙,瞬间化解了单链表的诸多尴尬,让数据的穿梭游走变得前所未有的自由。
在这篇博客中,我们将一起:
剖析双向链表的内在结构与核心思想,看它如何用微小的空间代价换来巨大的操作便利。
手把手实现一个功能完整的双向链表,并探讨其中需要注意的细节与边界条件。
分析比较顺序表和链表之间的差异。
无论你是正在准备技术面试,还是渴望优化手头项目的性能,亦或是单纯地对数据结构充满好奇,掌握双向链表都将极大提升你对“数据链接”的理解深度。
让我们一起,告别单向的束缚,拥抱双向的自由。开启这段旅程,你会发现,数据操作的效率与优雅,原来可以兼得。
一,双向链表的结构
如图所示,便是一个简单双向链表的结构图。
它由头结点+结点组成。
相邻结点之间是互通的。
值得注意的是:头结点的prev(上一个)指针指向最后一个结点,最后一个结点的next(下一个)指针指向头结点。
双向链表的结构体
typedef int LTDataType;
typedef struct ListNode
{LTDataType data;struct ListNode* next;struct ListNode* prev;
}LTNode;
它在 单链表具有的 data 和 next基础上,增加了 prev 用来指向上一个结点。
二,双向链表的实现
1)申请空间
LTNode* LTBuyNode(LTDataType x)
{LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));if (newnode == NULL){perror("mallloc fail");exit(1);}newnode->data = x;newnode->next = newnode->prev = newnode;return newnode;
}
申请的第一个结点肯定为头结点,一个结点也必须保证首尾相连,即头结点的next和prev指向自己而不是NULL
2)初始化
LTNode* LTInit()
{LTNode* phead = LTBuyNode(-1);return phead;
}
初始化即创立头结点
3)尾插
由图可知,需改变或添加四个指针,我们可以先定义新结点的相关指针,避免提前改变原链表导致指针丢失
void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);newnode->next = phead;newnode->prev = phead->prev;phead->prev->next = newnode;phead->prev = newnode;}
4)头插
双向链表的头插是指在头结点后插入结点,不是在头结点之前插入结点(因为这样跟尾插效果一样,本质上为尾插)
跟尾插相似,我们可以先改变newnode相关指针,避免提前修改原链表导致指针丢失或错误。
void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);newnode->next = phead->next;newnode->prev = phead;phead->next->prev = newnode;phead->next = newnode;
}
5)打印
为了检测代码是否真确,我们可以新建链表,对其进行增删查改并打印检验
打印没必要打印头结点,遍历结束条件为 pcur不为 头结点
void LTPrint(LTNode* phead)
{LTNode* pcur = phead->next;while (pcur != phead){printf("%d->", pcur->data);pcur = pcur->next;}printf("\n");
}
例如,我们进行尾插
void Test01()
{LTNode* plist = LTInit();LTPushBack(plist,1);LTPushBack(plist, 2);LTPushBack(plist, 3);LTPushBack(plist, 4);LTPrint(plist);
}int main()
{Test01();return 0;
}
我们进行头插
void Test01()
{LTNode* plist = LTInit();LTPushFront(plist,1);LTPushFront(plist, 2);LTPushFront(plist, 3);LTPushFront(plist, 4);LTPrint(plist);
}int main()
{Test01();return 0;
}
可证明我们前面代码正确 。
6)判空
判断链表是否为空链表即只有头结点
7)尾删
由图可知,需要改变的相关节点为头结点和最后两个结点
其中del(最后一节结点) = phead->prev prev(倒数第二个结点) = del ->prev
void LTPopBack(LTNode* phead)
{assert(phead);assert(!LTEmpty(phead));LTNode* del = phead->prev;LTNode* prev = del->prev;phead->prev = prev;prev->next = phead;free(del);del = NULL;
}
检验
void Test01()
{LTNode* plist = LTInit();LTPushFront(plist,1);LTPushFront(plist, 2);LTPushFront(plist, 3);LTPushFront(plist, 4);LTPrint(plist);LTPopBack(plist);LTPrint(plist);
}
8)头删
参照尾删
del = phead->next;
Next = del->next;
void LTPopFront(LTNode* phead)
{assert(phead);assert(!LTEmpty(phead));LTNode* del = phead->next;LTNode* Next = del->next;phead->next = Next;Next->prev = phead;free(del);del = NULL;
}
检验
void Test01()
{LTNode* plist = LTInit();LTPushFront(plist,1);LTPushFront(plist, 2);LTPushFront(plist, 3);LTPushFront(plist, 4);LTPrint(plist);LTPopFront(plist);LTPrint(plist);
}
9)查找
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;
}
10)在pos位置之后插入数据
与头插尾插类似,不过多说明
void LTInsert(LTNode* pos, LTDataType x)
{assert(pos);LTNode* newnode = LTBuyNode(x);//pos newnode pos->nextnewnode->next = pos->next;newnode->prev = pos;pos->next->prev = newnode;pos->next = newnode;
}
11)删除pos结点
与头删尾删类似,不过多说明
void LTErase(LTNode* pos)
{assert(pos);pos->prev->next = pos->next;pos->next->prev = pos->prev;free(pos);pos = NULL;
}
12)销毁链表
void LTDesTroy(LTNode** pphead)
{assert(pphead && *pphead);LTNode* pcur = (*pphead)->next;while (pcur != *pphead){LTNode* Next = pcur->next;free(pcur);pcur = Next;}//销毁哨兵位结点free(*pphead);*pphead = NULL;pcur = NULL;
}
void LTDesTroy2(LTNode* phead)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){LTNode* Next = pcur->next;free(pcur);pcur = Next;}free(phead);phead = pcur = NULL;
}
LTDesTroy2不会改变plist
最后得手动将plist = NULL
三,链表与顺序表的区别
不同点 | 顺序表 | 链表(单链表) |
存储空间上 | 物理上⼀定连续 | 逻辑上连续,但物理上不⼀定连续 |
随机访问 | ⽀持O(1) | 不⽀持:O(N) |
任意位置插⼊或者删除元素 | 可能需要搬移元素,效率低O(N) | 只需修改指针指向 |
插⼊ | 动态顺序表,空间不够时需要扩容和空间浪费 | 没有容量的概念,按需申请释放,不存在空间浪费 |
应⽤场景 | 元素⾼效存储+频繁访问 | 任意位置⾼效插⼊和删除 |