当前栏目
双链表,这回彻底搞dong了
前言
前面有很详细的讲过线性表(顺序表和链表),当时讲的链表以单链表为主,但实际上在实际应用中双链表的应用多一些就比如LinkedList。
![](https://s4.51cto.com/oss/202103/16/8f7438684f47fbf1a9ff7ff7a7d72204.png)
双链表与单链表区别
逻辑上它们均是线性表的链式实现,主要的区别是节点结构上的构造有所区别,这个区别从而引起操作的一些差异。
单链表:
单链表的一个节点,有储存数据的data,还有后驱节点next(指针)。也就是这个单链表想要一些遍历的操作都得通过前节点—>后节点。
双链表:
双链表的一个节点,有存储数据的data,也有后驱节点next(指针),这和单链表是一样的,但它还有一个前驱节点pre(指针)。
![](https://s5.51cto.com/oss/202103/16/184e2bcd57cefaaa3a468c855dceb8d0.png)
双链表结构的设计
上文讲单链表的时候,我们当时设计的是一个带头结点的链表就错过了不带头结点操作方式,这里双链表咱们就不带头结点设计实现。并且上文单链表实现的时候是没有尾指针tail的,在这里我们设计的双链表带尾指针。
所以我们构造的这个双链表是:不带头节点、带尾指针(tail)、双向链表。
对于node节点:
- class node<T> {
- T data;
- node<T> pre;
- node<T> next;
- public node() {
- }
- public node(T data) {
- this.data = data;
- }
- }
对于链表:
- public class doubleList<T> {
- private node<T> head;// 头节点
- private node<T> tail;// 尾节点
- private int length;
- //各种方法
- }
具体操作分析
对于一个链表主要的操作还是增删。增删的话不光需要考虑链表是否带头节点,还有头插、尾插、中间插等多种插入删除形式,其中的一些细节处理也是比较重要的(防止链表崩掉出错),如果你对这块理解不够深入很容易写错也很难排查出来。当然,链表的查找、按位修改操作相比增删操作还是容易很多。
初始化
双链表在最初的时候头指针指向为null。那么对于这个不带头节点的双链表而言。它的head始终指向第一个真实有效的节点。tail也指向最后一个有效的节点。在最初的时候head=null,并且tail=head,此时链表为空,等待节点插入。
- public doubleList() {
- head = null;
- tail = head;
- length = 0;
- }
插入
空链表插入
对于空链表来说。增加第一个元素可以特殊考虑。因为在链表为空的时候head和tail均为null。但head和tail又需要实实在在指向链表中的真实数据(带头指针就不需要考虑)。所以这时候就新建一个node让head、tail等于它。
- node<T> teamNode = new node(data);
- if (isEmpty()) {
- head = teamNode;
- tail = teamNode;
- }
头插
对于头插入来说。步骤很简单,只需考虑head节点的变化。
- 新建插入节点node
- head前驱指向node
- node后驱指向head
- head指向node。(这时候head只是表示第二个节点,而head需要表示第一个节点故改变指向)
![图片](https://s2.51cto.com/oss/202103/16/fd39c643849e25109b50d07f6029948a.jpg)
尾插:
对于尾插入来说。只需考虑尾节点tail节点的变化。
- 新建插入节点node
- node前驱指向tail
- tail后驱指向node
- tail指向node。(这时候tail只是表示倒数第二个节点,而tail需要表示最后节点故指向node)
![图片](https://s6.51cto.com/oss/202103/16/9643ea6f271644abe7699e5ad6be1909.jpg)
按编号插入
对于编号插入来说。要考虑查找和插入两步,而插入既和head无关也和tail无关。
1 新建插入节点node
2 找到欲插入node的前一个节点preNode。和后一个节点nextNode
3 node后驱指向nextNode,nextNode前驱指向node(此时node和后面与链表已经联立,但是和前面处理分离状态)
4 preNode后驱指向node。node前驱指向preNode(此时插入完整操作完毕)
![](https://s4.51cto.com/oss/202103/16/ac4115a8f4c8cb50d4299537a0526d65.png)
整个流程的动态图为:
![图片](https://s4.51cto.com/oss/202103/16/a15cc4b62260ff87d2d4c0217aed1262.jpg)
删除
只有单个节点删除
无论头删还是尾删,遇到单节点删除的需要将链表从新初始化!
- if (length == 1)// 只有一个元素
- {
- head = null;
- tail = head;
- length--;
- }
头删除
头删除需要注意的就是删除不为空时候头删除只和head节点有关
流程大致分为:
1 head节点的后驱节点的前指针pre改为null。(head后面节点本指向head但是要删除第一个先让后面那个和head断绝关系)
![](https://s5.51cto.com/oss/202103/16/f2dd6f19188c3585b465a3b7ddd1e432.png)
2 head节点指向head.next(这样head就指向我们需要的第一个节点了,前面节点就被删除成功,如果有c++等语言第一个被孤立的节点删除释放即可,但Java会自动释放)
![](https://s4.51cto.com/oss/202103/16/8075610b29d26dfe4806baee3aa1cd33.png)
尾删除
尾删除需要注意的就是删除不为空时候尾删除只和tail节点有关。记得在普通链表中,我们删除尾节点需要找到尾节点的前驱节点。需要遍历整个表,而双向链表可以直接从尾节点遍历到前面。
尾删除就是删除双向链表中的最后一个节点,也就是尾指针所指向的那个节点,思想和头删除的思想一致,具体步骤为:
- tail.pre.next=null尾节点的前一个节点(pre)的后驱节点等于null
- tail=tail.pre尾节点指向它的前驱节点,此时尾节点由于步骤1next已经为null。完成删除
![图片](https://s4.51cto.com/oss/202103/16/e9da0eec2f33aec8aca2700483039135.jpg)
普通删除
普通删除需要重点掌握,普通删除要妥善处理好待删除节点的前后关系,具体流程如下:
1:找到待删除节点node的前驱节点prenode(prenode.next是要删除的节点)
2 : prenode.next.next.pre=prenode.(将待删除node的后驱节点aftnode的pre指针指向prenode,等价于aftnode.pre=prenode)
3: prenode.next=prenode.next.next;此时node被跳过成功删除。
![](https://s4.51cto.com/oss/202103/16/a1ac2b3922a6dd6b2bfca56236a45dfe.png)
完成删除整个流程的动态图为:
![图片](https://s5.51cto.com/oss/202103/16/cea98ada7bd4d32a5c0ddb329e96d03e.jpg)
实现与测试
通过上面的思路简单的实现一下双链表,当然有些地方命名不太规范,实现效率有待提升,主要目的还是带着大家理解。
代码(代码以图片方式贴出,如需源码可阅读原文或者加我好友发你):
![](https://s2.51cto.com/oss/202103/16/74cff556b734140b77fa768df0865fe7.png)
测试:
![](https://s3.51cto.com/oss/202103/16/7e3816432abeed6859d492177094d1c6.png)
结语
在插入删除的步骤,很多人可能因为繁琐的过程而弄不明白,但实际上这个操作的写法可能是多样的,但本质操作都是一致的,所以看到其他不同版本有差距也是正常的。
还有很多人可能对一堆next.next搞不清楚,那我教你一个技巧,如果在等号右侧,那么它表示一个节点,如果在等号左侧,那么除了最后一个.next其他的表示节点。例如node.next.next.next可以看成(node.next.next).next。
在做数据结构与算法链表相关题的时候,不同题可能给不同节点去完成插入、删除操作。这种情况操作时候要谨慎先后顺序防止破坏链表结构。
代码操作可能有些优化空间,还请各位大佬指正!如有收获
相关文章
- 鲜为人知但很有用的 HTML 属性
- 翻转再翻转!有意思的水平横向溢出滚动
- 自定义计数器小技巧!CSS 实现长按点赞累加动画
- 过五关!React高频面试题指南
- 软件开发中的十个认知偏差
- 不需要 JS!仅用 CSS 也能达到监听页面滚动的效果!
- 一文读懂TypeScript类型兼容性
- Vue 的响应式原则与双向数据绑定
- 快速掌握 TypeScript 新语法:Infer Extends
- JWT教你如何证明你是我的人!
- 一篇带给你 V8 GC 的实现
- 面试官:请使用JS完成一个LRU缓存?
- 通过可视化来学习JavaScript事件循环
- 新的跨域策略:使用 COOP、COEP 为浏览器创建更安全的环境
- 为什么有人说 vite 快,有人却说 vite 慢?
- 种草 Vue3 中几个好玩的插件和配置
- 超全面的前端工程化配置指南
- Vue 状态管理未来样子
- Volatile关键字能保证原子性么?
- 面试突击:SpringBoot 有几种读取配置文件的方法?