当前位置: 首页 > news >正文

给链表装上“后视镜”:深入理解双向链表的自由与高效

前言:解锁数据结构新维度——深入探索双向链表

在日常的编程中,我们早已习惯了数组的简单直接,也领略了单链表的轻盈灵活。但你是否曾遇到过这样的困境:

  • 想要删除单链表中的某个节点,却不得不从表头开始苦苦遍历,寻找它那“失联”的前驱节点?

  • 在需要逆向遍历链表时,只能无奈地感叹“假如能倒着来该多好”?

  • 面对需要频繁前后移动、插入删除的复杂数据关系时,感觉单链表有些“力不从心”?

如果你曾有过上述任何一丝念头,那么恭喜你,你即将打开一扇新世界的大门。

单链表如同一条只能向前的单行道,高效但缺乏回旋的余地。而​​双向链表​​则为我们提供了更优雅的解决方案。它像是为每个节点都配备了“前视镜”和“后视灯”,不仅知道下一个节点在哪,还清楚地记得上一个节点是谁。这种设计的巧妙,瞬间化解了单链表的诸多尴尬,让数据的穿梭游走变得前所未有的自由。

在这篇博客中,我们将一起:

  • ​剖析​​双向链表的内在结构与核心思想,看它如何用微小的空间代价换来巨大的操作便利。

  • ​手把手​​实现一个功能完整的双向链表,并探讨其中需要注意的细节与边界条件。

  • 分析比较顺序表和链表之间的差异。

无论你是正在准备技术面试,还是渴望优化手头项目的性能,亦或是单纯地对数据结构充满好奇,掌握双向链表都将极大提升你对“数据链接”的理解深度。

让我们一起,告别单向的束缚,拥抱双向的自由。开启这段旅程,你会发现,数据操作的效率与优雅,原来可以兼得。

一,双向链表的结构

如图所示,便是一个简单双向链表的结构图。

它由头结点+结点组成。

相邻结点之间是互通的。

值得注意的是:头结点的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)只需修改指针指向
插⼊动态顺序表,空间不够时需要扩容和空间浪费没有容量的概念,按需申请释放,不存在空间浪费
应⽤场景元素⾼效存储+频繁访问任意位置⾼效插⼊和删除

http://www.dtcms.com/a/393221.html

相关文章:

  • Off-Grid Direction of Arrival Estimation Using Sparse Bayesian Inference (II)
  • Unity中的渲染管线
  • PyMuPDF 库
  • 【故障】windows7开机后能出现windows启动界面,但停在黑屏很久才进入系统界面
  • tqdm 库
  • 模块化编程规范与分层设计指南
  • Photoshop - Photoshop 调整照片的颜色强度
  • 【ROS2】通讯机制 Topic 常用命令行
  • DeepSeek的GPU优化秘籍:解锁大规模AI训练的底层效率
  • Gemini in Chrome深度解析:反垄断胜诉后,Chrome开启AI智能浏览时代!
  • 如何修复 Google Chrome 上的白屏问题
  • Camera2原生api级 Demo答
  • 【Redis】Redis缓存与数据库DB数据如何保持同步?
  • 考研408---C语言复习
  • 批量抓取图片
  • WinDivert学习文档之五-————编程API(十一)
  • 【打印菱形】
  • XC7Z100-2FFG900I Xilinx AMD Zynq-7000 FPGA SoC
  • 成本价的SEO优化服务供应链
  • dock生命周期体验-生到死的命令
  • 软件测试方案-模板一
  • 防火墙WEB方式登录配置【HCL模拟】-学习篇(1)
  • Ceph用户管理与cephFS分布式存储实战
  • AgenticSeek:重新定义AI助手的边界 - 100%本地化智能代理系统深度解析
  • 701. 二叉搜索树中的插入操作
  • Spring AI(六)Tool Calling本地回调方法
  • 《2511系统分析师第二遍阅读总结3》
  • 【Linux】系统部分——线程同步与生产者消费者模型
  • No008:共建产业知识生态——DeepSeek如何助力中小企业数字化转型
  • 3.8 开发环境 - IntelliJ IDEA