
个人主页 : 流年如梦
文章目录
- 一.了解双向链表
- 二.双向链表的实现
-
- [2.1头文件声明 --> List.h](#2.1头文件声明 --> List.h)
- [2.2源文件实现 --> List.c](#2.2源文件实现 --> List.c)
- [2.3主函数 --> test.c](#2.3主函数 --> test.c)
- 🎯总结
- ⚠️易错点
Ladies and gentlemen,本篇文章先了解一下双向链表,其中主要学习双向链表的实现(重点);全程高能,不容错过!!!
前言
双向链表是在单链表基础上优化升级的重要链式存储结构,通过前驱指针与后继指针实现双向访问,解决了单链表无法快速找到前驱节点的缺陷
一.了解双向链表
双向链表是一种带头、双向、循环 的链式结构(单链表则为不带头单向不循环链表 ),是实际开发中最常用、最好用的链表结构
1.1什么是双向链表
- 物理空间不连续,逻辑连续
- 每个节点有三个部分:
数据域data--> 存有效数据
后继指针next--> 指向后一个节点
前驱指针prev--> 指向前一个节点 - 带有一个哨兵位头节点,不存数据,只用来简化操作
- 链表首尾相连,形成循环
1.2结构特点
带头(哨兵位) :
不用处理空指针,插入删除不用判断边界
双向 :
既能向后走,也能向前走,能直接找到前驱节点
循环🔁 :
尾节点的next指向哨兵位,哨兵位的prev指向尾节点
1.3与单链表的区别
| 区别 | |
|---|---|
| 单链表 | 只能往后走,找前驱必须遍历,效率低 |
| 双向链表 | 前后都能走 ,找前驱时间复杂度为O(1),任意位置插入删除也都是O(1) |
1.4优缺点
优点:
- 头尾插删效率
O(1)- 任意位置插入删除
O(1)- 不用找前驱,代码简单
- 没有扩容、没有空间浪费
- 不会出现单链表的边界错误
缺点:
- 多存一个指针,占用略微多一点内存
- 不支持随机访问(链表通病)
二.双向链表的实现
2.1头文件声明 --> List.h
参考代码如下:
c
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
typedef int LTDataType;
typedef struct ListNode
{
struct ListNode* next;
struct ListNode* prev;
LTDataType data;
}LTNode;
//创建哨兵位
LTNode* LTInit();
//销毁、打印
void LTDestroy(LTNode* phead);
void LTPrint(LTNode* phead);
//判断链表是否为空
bool LTEmpty(LTNode* phead);
//尾插尾删
void LTPushBack(LTNode* phead, LTDataType x);
void LTPopBack(LTNode* phead);
//头插头删
void LTPushFront(LTNode* phead, LTDataType x);
void LTPopFront(LTNode* phead);
//在pos之前之后插入
void LTInsertBefore(LTNode* pos, LTDataType x);
void LTInsert(LTNode* pos, LTDataType x);
//删除pos节点
void LTErase(LTNode* pos);
//查找
LTNode* LTFind(LTNode* phead, LTDataType x);
2.2源文件实现 --> List.c
2.2.1销毁
c
void LTDestroy(LTNode* phead)
{
assert(phead);
LTNode* pcur = phead->next;
while (pcur != phead)
{
LTNode* next = pcur->next;
free(pcur);
pcur = next;
}
free(phead);
}
🧐分析 :先逐个释放有效节点,避免内存泄漏;遍历到回到哨兵位即停止即cur != phead,最后释放哨兵位
2.2.2打印
c
void LTPrint(LTNode* phead)
{
assert(phead);
LTNode* pcur = phead->next;
printf("->");
while (pcur != phead)
{
printf("%d->", pcur->data);
pcur = pcur->next;
}
printf("\n");
}
🧐分析:从第一个有效节点开始遍历,到哨兵位结束
2.2.3创建哨兵位
c
LTNode* LTInit()
{
LTNode* phead = BuyListNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
🧐分析 :带头双向循环链表必须有哨兵位并且哨兵位不存有效数据(自己填认为是无效数据) ;初始自环 --> 前后都指向自己
2.2.4创建新节点
c
LTNode* BuyListNode(LTDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
newnode->data = x;
newnode->prev = NULL;
newnode->next = NULL;
return newnode;
}
这个不多说,跟之前的的大差不差
2.2.5判断空链表
c
bool LTEmpty(LTNode* phead)
{
assert(phead);
return phead->next == phead;
}
🧐分析 :如果为空,哨兵位的next指向自己
2.2.6尾插尾删
尾插:
c
void LTPushBack(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyListNode(x);
LTNode* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
🧐分析 :双向链表可直接找到尾节点,不用遍历,四步指针链接,保证不断链
尾删:
c
void LTPopBack(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
LTNode* tail = phead->prev;
LTNode* tailPrev = tail->prev;
tailPrev->next = phead;
phead->prev = tailPrev;
free(tail);
}
🧐分析 :直接找到尾节点及其前驱,然后修改指针后释放尾节点
2.2.7头插头删
头插:
c
void LTPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyListNode(x);
LTNode* first = phead->next;
phead->next = newnode;
newnode->prev = phead;
newnode->next = first;
first->prev = newnode;
}
🧐分析 :在哨兵与第一个节点之间插入;指针修改顺序要正确,不断链
头删:
c
void LTPopFront(LTNode* phead)
{
assert(phead);
assert(!LTEmpty(phead));
LTNode* first = phead->next;
LTNode* second = first->next;
phead->next = second;
second->prev = phead;
free(first);
}
🧐分析:要删除第一个有效节点,需保存第二个节点,修改指针
2.2.8在pos之前与之后插入
在pos之前插入:
c
void LTInsertBefore(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* newnode = BuyListNode(x);
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
🧐分析 :可以直接找到pos的前驱 (双向链表最大优势),在前驱与pos之间插入
在pos之后插入:
c
void LTInsert(LTNode* pos, LTDataType x)
{
assert(pos);
LTNode* newnode = BuyListNode(x);
LTNode* posNext = pos->next;
pos->next = newnode;
newnode->prev = pos;
newnode->next = posNext;
posNext->prev = newnode;
}
🧐分析:通用后插函数,可实现头插或尾插,只需改4个指针
2.2.9删除pos节点
c
void LTErase(LTNode* pos)
{
assert(pos);
LTNode* prev = pos->prev;
LTNode* next = pos->next;
prev->next = next;
next->prev = prev;
free(pos);
}
🧐分析 :直接通过pos找到前后节点,然后改指针后释放pos
2.2.10查找
c
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;
}
🧐分析 :先遍历找值为x的节点,然后找到返回节点地址,否则返回NULL;查找用于定位插入或删除位置
2.3主函数 --> test.c
参考代码如下:
c
#include "List.h"
void TestList()
{
//创建带头双向循环链表
LTNode* plist = LTInit();
//尾插
LTPushBack(plist, 1);
LTPushBack(plist, 2);
LTPushBack(plist, 3);
LTPushBack(plist, 4);
printf("尾插后:");
LTPrint(plist);
//头插
LTPushFront(plist, 0);
printf("头插后:");
LTPrint(plist);
//尾删
LTPopBack(plist);
printf("尾删后:");
LTPrint(plist);
//头删
LTPopFront(plist);
printf("头删后:");
LTPrint(plist);
//查找、在pos之前插入
LTNode* pos = LTFind(plist, 2);
if (pos != NULL)
{
LTInsertBefore(pos, 99);
printf("在 2 之前插入 99:");
LTPrint(plist);
}
//查找、在pos之后插入
pos = LTFind(plist, 1);
if (pos != NULL)
{
LTInsert(pos, 66);
printf("在 1 之后插入 66:");
LTPrint(plist);
}
//删除指定节点pos
pos = LTFind(plist, 2);
if (pos != NULL)
{
LTErase(pos);
printf("删除节点 2 后:");
LTPrint(plist);
}
//不要用的时候销毁,有借有还
LTDestroy(plist);
plist = NULL;
}
int main()
{
TestList();
return 0;
}
最终运行结果 :

🎯总结
- 双向链表是带头、双向、循环结构,含哨兵位
- 每个节点包含
prev、next、data三部分 - 头尾插删、任意位置插入删除均为
O(1) - 可直接找前驱,解决单链表最大缺陷
- 无扩容、无空间浪费,是最常用链表
⚠️易错点
- 混淆哨兵位与有效节点
- 插入或删除时指针顺序错误导致断链
- 空链表执行删除,程序崩溃
- 销毁不彻底,造成内存泄漏
- 遍历条件错误,出现死循环
- 不判断
pos合法性,导致空指针崩溃
👀 关注 我们一路同行,从入门到大师,慢慢沉淀、稳步成长
❤️ 点赞 鼓励原创,让优质内容被更多人看见
⭐ 收藏 收好核心知识点与实战技巧,需要时随时查阅
💬 评论 分享你的疑问或踩坑经历,一起交流避坑、共同进步