数据结构与算法—双链表
前言
前面有很详细的讲过线性表(顺序表和链表),当时讲的链表以单链表为主,但在实际应用中双链表有很多应用场景,例如大家熟知的LinkedList。

双链表与单链表区别
单链表和双链表都是线性表的链式实现,它们的主要区别在于节点结构。单链表的节点包含数据字段 data 和一个指向下一个节点的指针 next,而双链表的节点除了 data 和 next,还包含指向前一个节点的指针 pre。这个区别会导致它们在操作上有些差异。
单链表:
单链表的一个节点,有储存数据的data,还有后驱节点next(指针)。单链表想要遍历的操作都得从前节点—>后节点。

双链表:
双链表的一个节点,有存储数据的data,也有后驱节点next(指针),这和单链表是一样的,但它还有一个前驱节点pre(指针)。

双链表结构的设计
上一篇讲单链表的时候,当时设计一个带头结点的链表就错过了不带头结点操作方式,这里双链表就不带头结点设计实现。所以本文构造的这个双链表是:不带头节点、带尾指针(tail)的双向链表。
对于链表主体:
public class DoubleLinkedList<T> {private Node<T> head;private Node<T> tail;private int size;public DoubleLinkedList(){this.head = null;this.tail = null;size = 0;}public void addHead(T data){}public void add(T data, int index){}public void addTail(T data){}public void deleteHead(){}public void delete(int index){}public void deleteTail(int index){}public T get(int index){}public int getSize() {return size;}private static class Node<T> {T data;Node<T> pre;Node<T> next;public Node() {}public Node(T data) {this.data = data;}}
}
具体操作分析
对于一个链表主要的操作还是增删,查询的话不做详细解释。
剖析增删其实可以发现大概有头插入、编号插入、末尾插入、头删除、编号删除、尾删除几种情况。然而这几种关于头尾操作的可能会遇到临界点比如链表为空时插入删除、或者删除节点链表为空。
这个操作是不带头结点的操作,所以复杂性会高一些!
头插入
头插入区分头为空和头不为空两种情况
头为空:这种情况head和tail都指向新节点
头不为空:
- 新节点的next指向head
- head的pre指向新节点
- head指向新节点(认新节点为head)

尾插入
尾插需要考虑tail为null和不为null的情况。流程和头插类似,需要考虑tail指针最后的指向。
tail为null:此时head也为null,head和tail指向新节点。
tail不为null:
- 新节点的pre指向tail
- tail的next指向新节点
- tail指向新节点
编号插入
按编号插入分情况讨论,如果是头插或者尾插就直接调用对应的方法。普通方法的实现方式比较灵活,可以找到前驱节点和后驱节点,然后进行指针插入,但是往往很多时候只用一个节点完成表示和相关操作,就非常考验对表示的理解,这里假设只找到preNode节点。
index为0:调用头插
index为size:调用尾插
index在(0,size):
- 找到前驱节点preNode
- 新节点next指向nextNode(此时用preNode.next表示)
- nextNode(此时新节点.next和preNode.next都可表示)的pre指向新节点
- preNode的next指向新节点
- 新节点的pre指向preNode

头删除
头删除需要注意的就是删除不为空时候头删除只和head节点有关
head不为null:
- head = head.next 表示头指针指向下一个节点
- head 如果不为null(有可能就一个节点),head.pre = null 断掉与前一个节点联系 ;head如果为null,说明之前就一个节点head和pre都指向第一个节点,此时需要设置tail为null。

尾删除
尾删除和头删除类似,考虑好tail节点情况
如果tail不为null:
- tail = tail.pre
- 如果tail不为null,那么tail.next = null 表示删除最后一个,如果tail为null,说明之前head和tail都指向一个唯一节点,这时候需要head = null。
编号删除
编号删除和编号插入类似,先考虑是否为头尾操作,然后再进行正常操作。
index为0:调用头删
index为size:调用尾删
index在(0,size):
- 找到待删除节点current
- 前驱节点(current.pre)的next指向后驱节点(current.next)
- 后驱节点的pre指向前驱节点

完整代码
根据上面的流程,实现一个不带头结点的双链表,在查找方面,可以根据靠头近还是尾近,选择从头或者尾开始遍历。
代码:
/** 不带头节点的*/
package code.linearStructure;/*** @date 2023.11.02* @author bigsai* @param <T>*/
public class DoubleLinkedList<T> {private Node<T> head;private Node<T> tail;private int size;public DoubleLinkedList() {this.head = null;this.tail = null;size = 0;}// 在链表头部添加元素public void addHead(T data) {Node<T> newNode = new Node<>(data);if (head == null) {head = newNode;tail = newNode;} else {newNode.next = head;head.pre = newNode;head = newNode;}size++;}// 在指定位置插入元素public void add(T data, int index) {if (index < 0 || index > size) {throw new IndexOutOfBoundsException("Index is out of bounds");}if (index == 0) {addHead(data);} else if (index == size) {addTail(data);} else {Node<T> newNode = new Node<>(data);Node<T> preNode = getNode(index-1);//step 1 2 新节点与后驱节点建立联系newNode.next = preNode;preNode.next.pre = newNode;//step 3 4 新节点与前驱节点建立联系preNode.next = newNode;newNode.pre = preNode;size++;}}// 在链表尾部添加元素public void addTail(T data) {Node<T> newNode = new Node<>(data);if (tail == null) {head = newNode;tail = newNode;} else {newNode.pre = tail;tail.next = newNode;tail = newNode;}size++;}// 删除头部元素public void deleteHead() {if (head != null) {head = head.next;if (head != null) {head.pre = null;} else { //此时说明之前head和tail都指向唯一节点,链表删除之后head和tail都应该指向nulltail = null;}size--;}}// 删除指定位置的元素public void delete(int index) {if (index < 0 || index >= size) {throw new IndexOutOfBoundsException("Index is out of bounds");}if (index == 0) {deleteHead();} else if (index == size - 1) {deleteTail();} else {Node<T> current = getNode(index);current.pre.next = current.next;current.next.pre = current.pre;size--;}}// 删除尾部元素public void deleteTail() {if (tail != null) {tail = tail.pre;if (tail != null) {tail.next = null;} else {//此时说明之前head和tail都指向唯一节点,链表删除之后head和tail都应该指向nullhead = null;}size--;}}// 获取指定位置的元素public T get(int index) {if (index < 0 || index >= size) {throw new IndexOutOfBoundsException("Index is out of bounds");}Node<T> node = getNode(index);return node.data;}// 获取链表的大小public int getSize() {return size;}private Node<T> getNode(int index) {if (index < 0 || index >= size) {throw new IndexOutOfBoundsException("Index is out of bounds");}if (index < size / 2) {Node<T> current = head;for (int i = 0; i < index; i++) {current = current.next;}return current;} else {Node<T> current = tail;for (int i = size - 1; i > index; i--) {current = current.pre;}return current;}}private static class Node<T> {T data;Node<T> pre;Node<T> next;public Node(T data) {this.data = data;}}
}
结语
在插入删除的步骤,很多人可能因为繁琐的过程而弄不明白,这个操作的写法可能是多样的,但本质操作都是一致的,要保证能成功表示节点并操作,这个可以画个图一步一步捋一下,看到其他不同版本有差距也是正常的。
还有很多人可能对一堆next.next搞不清楚,那我教你一个技巧,如果在等号右侧,那么它表示一个节点,如果在等号左侧,那么除了最后一个.next其他的表示节点。例如node.next.next.next可以看成(node.next.next).next。
在做数据结构与算法链表相关题的时候,不同题可能给不同节点去完成插入、删除操作。这种情况操作时候要谨慎先后顺序防止破坏链表结构。
系列仓库地址:https://github.com/javasmall/bigsai-algorithm
csdn专栏:数据结构与算法
原创不易,还请三连支持一下!
相关文章:
数据结构与算法—双链表
前言 前面有很详细的讲过线性表(顺序表和链表),当时讲的链表以单链表为主,但在实际应用中双链表有很多应用场景,例如大家熟知的LinkedList。 双链表与单链表区别 单链表和双链表都是线性表的链式实现,它们的主要区别在于节点结构…...
linux继续循环案例测试ping网络,目录下的文件权限循环输出
第一:查看本机ip #ip addr 通过脚本访问本机ip1-100,是否可以ping通,并显示结果,上图 知识点 ping -c 数字1 -w 数字1,向目的ip发送1个数据包,等待1秒,无回复中止 &>/dev/null 知…...
关于SSP3D复现
关于SSP3D复现的问题 准备工作 下载Xshell和XFTP:家校免费版下载链接连接服务器(可能需要与服务器处在相同网络下)GitHub上下载源码:SSP3D 左上角新建会话,输入名称和主机 点击左侧菜单“用户身份验证”,…...
在直播系统中使用RTSP协议传递视频
目录 概述 1、环境准备 2、拉流URL地址 3、导播软件取流 (1)OBS中拉取RTSP流 (2)芯象中拉取RTSP流 (3)vMix中拉取RTSP流 写在最后 概述 提到RTSP协议,很容易想到RTMP协议,它…...
Notion汉化
Notion真无语,汉化版都没有。真的无力吐槽。 2023.11.7汉化经历 教程链接:github Reamd7/notion-zh_CN at 2.4.20-handmade (github.com) 网页版: 油猴下载插件。 Notion中文汉化 浏览器插件下载 windows: github realse 这…...
echarts有背景的柱状图,鼠标滑过提示信息都是展示背景柱状图的值
// 上一篇文章介绍了如何实现有背景的柱状图,现在又遇到一个问题,鼠标滑过柱子,提示信息是背景柱子的值,解决方案,自定义tooltip的formatter,上代码tooltip: {//鼠标悬浮提示数据formatter: function (para…...
华为防火墙基本原理工作方法总结
防火墙只会对tcp首包syn建立会话表,其它丢掉,如synack,ack udp直接建立会话表 icmp只对首包请求包建立会话表,其它包,如应答的不会建立直接丢掉 防火墙状态查看: rule name trust_untrust source-zone tru…...
Spring Cloud之多级缓存
目录 传统缓存 多级缓存 JVM进程缓存 Caffeine 缓存驱逐策略 实现进程缓存 常用Lua语法 数据类型 变量声明 循环使用 定义函数 条件控制 安装OpenResty 实现Nginx业务逻辑编写 请求参数解析 实现lua访问tomcat JSON的序列化和反序列化 Tomcat的集群负载均衡 …...
融云荣登「2023 年度 PaaS 企业排行榜」
11 月 2 日,中国科学院旗下《互联网周刊》颁布“2023 年度 PaaS 企业排行榜”,融云荣登榜单。关注【融云全球互联网通信云】了解更多 根据中国信息通信研究院《云计算白皮书 2023》:2022 年,PaaS 增长强势,总收入 342 …...
YOLOv8轻量化模型:模型轻量化设计 | 轻量级可重参化EfficientRep| 来自YOLOv6思想
💡💡💡本文解决什么问题:在几乎不保证精度下降的前提下,轻量级模型创新设计 EfficientRep 在关键点检测任务中 | GFLOPs从9.6降低至8.5, mAP50从0.921下降至0.912,mAP50-95从0.697提升至0.779 YOLO轻量化模型专栏:http://t.csdnimg.cn/AeaEF 1.YOLOv6介绍 论文…...
【JavaSE】基础笔记 - 类和对象(下)
目录 1、this引用 1.1、为什么要有this引用 1.2、什么是this引用 1.3、 this引用的特性 2、 对象的构造及初始化 2.1、 如何初始化对象 2.2、构造方法 2.2.1、概念 2.2.2、特性 2.3、默认初始化 2.4、就地初始化 上篇:【JavaSE】基础笔记 - 类和对象&#…...
浅析刚入门Python初学者的注意事项
文章目录 一、注意你的Python版本1.print()函数2.raw_input()与input()3.比较符号,使用!替换<>4.repr函数5.exec()函数 二、新手常遇到的问题1、如何写多行程序?2、如何执行.py文件?3、and,or,not4、True和False…...
2023NOIP A层联测26 总结
T1 求 ∑ i 1 n ∑ j i n ( ⨁ k i j a k ) 2 \sum\limits_{i1}^n\sum\limits_{ji}^n\left(\bigoplus\limits_{ki}^{j}a_k\right)^2 i1∑nji∑n(ki⨁jak)2, n , a i ≤ 2 1 0 5 n,a_i\le2\times10^5 n,ai≤2105。先转成前缀和,然后就没思…...
响应式编程-Project Reactor Mono 介绍
响应式编程-Project Reactor Mono 介绍 本文以Mono的角度来介绍Reactor编程,Flux的使用同理。 初体验 Web应用 controller 方法在Spring webmvc 和 Spring webFlux下Controller方法实现示例如下: Spring webmvc: GetMapping("/test1") …...
R语言实操记录——导出高清图片(矢量图)
R语言 R语言实操记录——导出高清图片(矢量图) 文章目录 R语言一、起因(闲聊,可跳过)二、如何在R中导出高清图片(矢量图)2.1、保存为EPS图片格式后转AI编辑2.2、保存为PDF格式(推荐…...
Apache Doris 开源最顶级基于MPP架构的高性能实时分析数据库
背景介绍 Apache Doris是一个基于MPP架构的易于使用,高性能和实时的分析数据库,以其极高的速度和易用性而闻名。海量数据下返回查询结果仅需亚秒级响应时间,不仅可以支持高并发点查询场景,还可以支持高通量复杂分析场景。 这些都…...
webgoat-Request Forgeries 请求伪造
(A8:2013) Request Forgeries Cross-Site Request Forgeries 跨站请求伪造,又称一键攻击或会话骑乘,简称CSRF (有时发音为 sea-surf)或 XSRF,是一种恶意利用网站,其中传输未经授权的命令 来自网站信任的用…...
【flask跨域问题】解决它
大概7-8年前,前后端还没开始分离或者刚开始分离的之前,跨域问题很多。 后来我就没在遇到过了,这次做一个小项目,又遇到了,记录下。 现在前端的脚手架都自己能解决了。 1. 跨域 是因为出于浏览器的同源策略限制。同源…...
虚幻引擎:如何在工程里面添加插件
1.在自己的项目中安装插件 在content目录下创建一个Plugins的文件,将插件文件放进去即可 2.在软件上安装,这样所有创建的项目都会带有此插件 将插件放在自己软件的这个目录下就好了...
SpringCloud Alibaba 【四】Openfeign
Openfeign配置与使用 前言介绍openfeign使用openfeign导入依赖启动类正式使用测试结果 前言 在springcloud中消费者项目需要调用提供者项目的接口,一开始用的是RestTemplate中的方法。但是RestTemplate进行远程调用时,直接调用controller层的接口&#…...
告别手动建模!用Blender GIS插件5分钟搞定CARLA地图(附OSM数据源)
告别手动建模!用Blender GIS插件5分钟搞定CARLA地图(附OSM数据源) 在自动驾驶仿真领域,快速构建高精度地图一直是开发者的痛点。传统手动建模方式不仅耗时费力,还难以保证道路网络的拓扑准确性。现在,通过…...
GTE文本向量助力智能写作:文本分类与情感倾向双重把关
GTE文本向量助力智能写作:文本分类与情感倾向双重把关 1. 智能写作的核心挑战:内容质量的多维评估 在内容创作领域,我们常常面临一个基本矛盾:如何同时保证文本的专业性和情感表达?传统写作辅助工具往往只能解决单一…...
【CDA干货】三个部门三个营收数:1200 万、1150 万、1280 万?企业指标口径不一致,三步破局
财务部报的Q3营收是1200万,运营部那边却是1150万,更离谱的是CEO给投资人看的PPT上写着1280万。这种事儿听起来是不是很离谱?但实际上,数据对不上,这事儿太常见了。表面看是数字打架,实际上是人跟人较劲——…...
【Java】UTF-8变长编码及其3字节存储奥秘
UTF-8 是一种变长编码,一个字符可能由 1 到 4 个字节组成。 解码时(将字节数组转回 String),计算机并不需要“猜”或者去查表,因为长度信息本身就包含在字节的“头部”里。这就是 UTF-8 设计的精妙之处:它是…...
MD_DS3231库:工业级DS3231 RTC全功能驱动设计与实践
1. MD_DS3231库深度解析:面向工业级RTC应用的DS3231全功能驱动设计与工程实践DS3231是Maxim(现属Analog Devices)推出的高精度IC实时时钟芯片,其2ppm温漂特性、内置温度补偿晶振(TCXO)、独立电池供电备份、…...
3步搞定ERPNext自动化部署:让企业管理系统安装变得简单
3步搞定ERPNext自动化部署:让企业管理系统安装变得简单 【免费下载链接】erpnext_quick_install Unattended install script for ERPNext Versions, 13, 14 and 15 项目地址: https://gitcode.com/gh_mirrors/er/erpnext_quick_install 还在为复杂的ERPNext安…...
SEO_详解SEO核心关键词的研究与布局方法(455 )
<h2>SEO核心关键词的研究与布局方法详解</h2> <p>在当前的互联网时代,搜索引擎优化(SEO)已经成为了各个企业和网站提升网络曝光率、吸引更多流量的重要手段。其中,核心关键词的研究与布局是SEO的重要组成部分。…...
百川2-13B-4bits量化版对比测试:OpenClaw日常任务执行效率报告
百川2-13B-4bits量化版对比测试:OpenClaw日常任务执行效率报告 1. 测试背景与动机 最近在折腾OpenClaw自动化工作流时,发现一个棘手问题:当任务链条较长时,本地部署的大模型显存占用会飙升到16GB以上,导致我的RTX 30…...
STM32智慧停车场系统设计与SQLite应用
基于STM32的智慧停车场管理系统设计与实现(SQLite版)1. 项目概述1.1 系统架构本智慧停车场管理系统采用分布式架构设计,由以下核心组件构成:下位机控制单元:STM32F103ZET6微控制器作为主控芯片感知层:OV772…...
VOOHU沃虎xJLSemi景略:智造时代通信基石-以太网接口PHY芯片
随着智能制造和工业物联网的高速发展,工业通信正朝着高速化、智能化的方向迈进。工业自动化设备需要实时、高效地传输大量数据,以实现精准控制和协同作业。 工业以太网现场总线凭借其高速率、高可靠性、兼容性强等优势成为工业通信的主流选择࿰…...
