数据结构:图文详解双向链表的各种操作(头插法,尾插法,任意位置插入,查询节点,删除节点,求链表的长度... ...)


目录

一.双向链表的概念

二.双向链表的数据结构

三.双向链表的实现

节点的插入

头插法

尾插法

任意位置插入

节点的删除

删除链表中第一次出现的目标节点

删除链表中所有与关键字相同的节点

节点的查找

链表的清空

链表的长度

四.模拟实现链表的完整代码


前言:在上一篇文章中,我们认识了链表中的单链表,而本篇文章则是介绍线链表中的另一个结构双向链表,有兴趣的朋友们可以点击了解:图文详解单链表的各种操作

一.双向链表的概念

双向链表(Doubly Linked List)是一种数据结构,它与单向链表相似,但每个节点不仅包含指向下一个节点的指针,还包含指向上一个节点的指针。

双向链表的每个节点通常包含以下两个指针:

  • prev:指向上一个节点
  • next:指向下一个节点

通过这两个指针,可以在双向链表中沿着两个方向遍历。

相比于单向链表,双向链表能够更方便地进行插入和删除操作。因为每个节点包含指向前一个节点的指针,所以在删除或插入一个节点时,只需要修改该节点前后节点的指针即可。而在单向链表中,则需要在删除或插入节点时,找到该节点的前一个节点,以便进行指针修改,显得相对麻烦。


二.双向链表的数据结构

双向俩表有俩个指针,分别存放前驱节点的地址和后继节点的地址,如图所示

对于其中每一个节点,我们可以如下存储

    public class Node{public int data;public Node prev;public Node next;//构造方法,给每个节点赋值public Node(int data) {this.data = data;}}

而我们的链表则是需要将所有的节点封装起来,放进一个数据结构中,因此将刚才定义的节点类作为链表的内部类即可,而链表又需要实现各种各样的功能,我们可以将所有的链表的功能抽象出一个接口,然后通过链表去具体的实现那些功能

public class MyLinkList implements Ilst{//节点的数据结构public class Node{public int data;public Node prev;public Node next;//构造方法public Node(int data) {this.data = data;}}public Node head;//头节点public Node last;//尾节点
}

接口:

public interface Ilst {//头插法public void addFirst(int data);//尾插法public void addLast(int data);//任意位置插入public void addIndex(int index,int data);//查找是否包含关键字key是否在链表当中public boolean contains(int key);//删除第一次出现关键字为key的节点public void remove(int key);//删除所有值为key的节点public void removeAllKey(int key);//得到链表的长度public int size();//展示链表public void display();//清空链表public void clear();
}

三.双向链表的实现

节点的插入

节点的插入分为三种情况,一是在链表最前面进行插入也就是头插法,二是在链表末尾进行插入,也就是尾插法,三是在链表中间位置插入

头插法

如图所示有一个新的节点,我们需要将其插入头节点的位置

第一步:将目标节点后继指针指向头节点位置的节点

第二步,将头节点前驱指针指向目标节点

在使用代码具体实现的时候,需要对异常做出判断,假如头节点为空,也就是链表里面没有节点的时候,我们就直接让我们要新加的节点既是头节点,又是尾节点;在做出异常处理后,我们就可以按照刚才图示的过程进行头插了,但是需要注意的是,在完成头插后需要更新头节点的位置 

    @Override//头插法public void addFirst(int data) {Node newNode = new Node(data);if (head == null){head = newNode;last = newNode;}else {newNode.next = head;head.prev = newNode;//更新头节点head = newNode;}}

尾插法

如图所示有一个新的节点,我们需要将其插入链表的末尾

第一步:将目标节点前驱指针指向尾部节点

第二步:将尾部节点后继指针指向目标节点

在使用代码具体实现的时候,需要对异常做出判断,假如头节点为空,也就是链表里面没有节点的时候,我们就直接让我们要新加的节点既是头节点,又是尾节点;在做出异常处理后,我们就可以按照刚才图示的过程进行尾插了,但是需要注意的是,在完成头插后需要更新尾部节点的位置

    @Override//尾插法public void addLast(int data) {Node newNode =  new Node(data);if (head == null){head = newNode;last = newNode;}else {newNode.prev = last;last.next = newNode;//更新尾部节点last = newNode;}}

任意位置插入

如图,假如想让新节点插入第三个节点的位置,该如何做呢?

第一步:先将目标节点后继指针指向要插入节点后一个节点

 

第二步:将目标节点前驱指针指向插入节点 

 

第三步:将插入节点后继指针指向目标节点

第四步:将插入节点的后一个节点前驱指针指向目标节点 

对于节点的插入,最难的一点便是这4个步骤的顺序,顺序不是一成不变也不必死背,只需要记住一个原则——保证链表不断,在这个原则的基础上进行操作就不会出现问题了,也就是说在我们插入的时候,不要因为先去处理前面的节点导致找不到后面的节点就可以,因此我们在对链表进行插入操作的时候,一般都习惯先对后面的节点进行操作。

对于输入的位置我们要进行合法性的判断,如果在最前面就刚好使用头插法,如果是最后面就使用尾插法,之后遍历找到我们要插入的位置

    @Override//任意位置插入public void addIndex(int index, int data) {//对输入位置进行判断if (index < 0 || index > size()) {System.out.println("输入位置不合法");return;}if (index == 0) {//如果插入位置在最前面就使用头插addFirst(data);return;}if (index == size()) {//这里的size方法在后文中有定义//如果插入位置在最后面就使用尾插addLast(data);return;}//在中间插入Node newNode = new Node(data);//找到要插入的位置Node cur = head;while (index != 1) {cur = cur.next;index--;}//将新节点插入到cur之前newNode.next = cur;newNode.prev = cur.prev;cur.prev.next = newNode;cur.prev = newNode;
//        //将新节点插入到cur之后
//        newNode.next = cur.next;
//        newNode.prev = cur;
//        cur.next = newNode;
//        newNode.next.prev = newNode;}

节点的删除

对于节点的删除我们分为俩种,一种的将单个节点进行删除,一种是将所有与目标值相同的节点进行删除

删除链表中第一次出现的目标节点

如图,我们假定我们要删除链表中第三个节点

第一步:将删除节点的前驱节点后继指针指向删除节点的后继节点 

第二步:将删除节点的后继节点前驱指针指向删除节点的前驱节点

对于上面俩个过程只是俩行代码就可以解决:

cur.next.prev = cur.prev;
cur.prev.next = cur.next;

删除的过程非常简单,但是要找到正确的位置并不是一件容易事,就算找到后也要进行合法性的判断,具体代码如下:

    @Override//删除第一次出现关键字为key的节点public void remove(int key) {Node cur = head;while (cur != null) {if(cur.data == key) {if(cur == head) {head = head.next;if(head != null) {head.prev = null;}else {//只有一个节点 且是需要删除的节点last = null;}}else {if(cur.next != null) {//删除中间节点cur.next.prev = cur.prev;cur.prev.next = cur.next;}else {//删除尾巴节点cur.prev.next = cur.next;last = last.prev;}}return;}cur = cur.next;}}

删除链表中所有与关键字相同的节点

对于和刚才唯一不同的点就是我们在删除一个点后不需要停止返回,继续遍历整个链表进行删除即可,这里就不再赘述

    @Override//删除所有值为key的节点public void removeAllKey(int key) {Node cur = head;while (cur != null) {if(cur.data == key) {if(cur == head) {head = head.next;if(head != null) {head.prev = null;}else {//只有一个节点 且是需要删除的节点last = null;}}else {if(cur.next != null) {//删除中间节点cur.next.prev = cur.prev;cur.prev.next = cur.next;}else {//删除尾巴节点cur.prev.next = cur.next;last = last.prev;}}}cur = cur.next;}}

节点的查找

对于节点的查找,只需要挨个遍历判断就可以

    @Override//查找是否包含关键字key是否在链表当中public boolean contains(int key) {Node cur = head;while (cur != null){if (cur.data == key){return true;}else {cur = cur.next;}}return false;}

链表的清空

清空链表需要对每个节点进行清空,因此我们遍历整个链表然后进行赋值为空就可以,但是有一点需要注意,我们在删除每一个节点的后继指针之前得先做临时的记录,不然我们删除了一个节点的后继指针后就无法通过它访问后一个节点了

    @Override//清空链表public void clear() {Node cur = head;while (cur != null){Node tempNode = cur.next;//记录当前节点的下一个节点的地址cur.prev = null;cur.next = null;cur = tempNode;}this.head = null;this.last = null;}

链表的长度

求链表的长度只需要使用计数器遍历累加就可以

    @Override//得到单链表的长度public int size() {int count = 0;Node cur = head;while (cur != null) {count++;cur = cur.next;}return count;}

四.模拟实现链表的完整代码

package MyLinkList;public class MyLinkList implements Ilst {public class Node {public int data;public Node prev;public Node next;//构造方法public Node(int data) {this.data = data;}}public Node head;public Node last;@Override//头插法public void addFirst(int data) {Node newNode = new Node(data);if (head == null) {head = newNode;last = newNode;} else {newNode.next = head;head.prev = newNode;//更新头节点head = newNode;}}@Override//尾插法public void addLast(int data) {Node newNode = new Node(data);if (head == null) {head = newNode;last = newNode;} else {newNode.prev = last;last.next = newNode;//更新尾部节点last = newNode;}}@Override//任意位置插入public void addIndex(int index, int data) {//对输入位置进行判断if (index < 0 || index > size()) {System.out.println("输入位置不合法");return;}if (index == 0) {//如果插入位置在最前面就使用头插addFirst(data);return;}if (index == size()) {//如果插入位置在最后面就使用尾插addLast(data);return;}//在中间插入Node newNode = new Node(data);//找到要插入的位置Node cur = head;while (index != 1) {cur = cur.next;index--;}//将新节点插入到cur之前newNode.next = cur;newNode.prev = cur.prev;cur.prev.next = newNode;cur.prev = newNode;
//        //将新节点插入到cur之后
//        newNode.next = cur.next;
//        newNode.prev = cur;
//        cur.next = newNode;
//        newNode.next.prev = newNode;}@Override//查找是否包含关键字key是否在链表当中public boolean contains(int key) {Node cur = head;while (cur != null){if (cur.data == key){return true;}else {cur = cur.next;}}return false;}@Override//删除第一次出现关键字为key的节点public void remove(int key) {Node cur = head;while (cur != null) {if(cur.data == key) {if(cur == head) {head = head.next;if(head != null) {head.prev = null;}else {//只有一个节点 且是需要删除的节点last = null;}}else {if(cur.next != null) {//删除中间节点cur.next.prev = cur.prev;cur.prev.next = cur.next;}else {//删除尾巴节点cur.prev.next = cur.next;last = last.prev;}}return;}cur = cur.next;}}@Override//删除所有值为key的节点public void removeAllKey(int key) {Node cur = head;while (cur != null) {if(cur.data == key) {if(cur == head) {head = head.next;if(head != null) {head.prev = null;}else {//只有一个节点 且是需要删除的节点last = null;}}else {if(cur.next != null) {//删除中间节点cur.next.prev = cur.prev;cur.prev.next = cur.next;}else {//删除尾巴节点cur.prev.next = cur.next;last = last.prev;}}}cur = cur.next;}}@Override//得到单链表的长度public int size() {int count = 0;Node cur = head;while (cur != null) {count++;cur = cur.next;}return count;}@Override//展示链表public void display() {Node cur = head;while (cur != null) {System.out.print(cur.data + " ");cur = cur.next;}System.out.println();}@Override//清空链表public void clear() {Node cur = head;while (cur != null){Node tempNode = cur.next;cur.prev = null;cur.next = null;cur = tempNode;}this.head = null;this.last = null;}
}



  本次的分享就到此为止了,希望我的分享能给您带来帮助,也欢迎大家三连支持,你们的点赞就是博主更新最大的动力!如有不同意见,欢迎评论区积极讨论交流,让我们一起学习进步!有相关问题也可以私信博主,评论区和私信都会认真查看的,我们下次再见

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/194747.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

检测下我的饺子皮擀的怎么样(圆度)

&#x1f604;&#x1f60a;&#x1f606;&#x1f603;&#x1f604;&#x1f60a;&#x1f606;&#x1f603; 各位老铁周末愉快。 快乐的时间做充实的事&#xff0c;好久没有吃饺子了&#xff0c;俗话说好吃不过饺子。 我个人觉得会包饺子不算本事&#xff0c;会擀饺子皮…

Linux 上的容器技术

容器实现封闭的环境主要要靠两种技术&#xff0c;一种是看起来是隔离的技术&#xff0c;称为 namespace&#xff08;命名空间&#xff09;。在每个 namespace 中的应用看到的&#xff0c;都是不同的 IP 地址、用户空间、进程 ID 等。另一种是用起来是隔离的技术&#xff0c;称为…

【csdn默认使用操作详解】

这里写自定义目录标题 欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants 创建一个自定义列表如何创建一个…

使用PCSS实现的实时阴影效果

PCSS的技术可以使得阴影呈现出近硬远软的效果&#xff0c;并且能够实时实现。 其核心理念是通过模拟光源的面积来产生更自然、更柔和的阴影边缘。 具体步骤&#xff1a; 1、生成shadowmap 2、在进行阴影的比较时候进行平均&#xff0c;并非之前的shadow map 或者之后完全的阴影…

YOLOv8创新魔改教程(二)如何添加注意力机制

YOLOv8创新魔改教程&#xff08;二&#xff09;如何添加注意力机制 &#xff08;一&#xff09;找代码 github找各种注意力机制的代码 &#xff08;二&#xff09;融合 1.创建文件 在ultralytics/nn/attention.py创建attention.py 文件 将找到的代码粘贴进来 2.修改task…

【计算机组成原理】存储器知识

目录 1、存储器分类 1.1、按存储介质分类 1.2、按存取方式分类 1.3、按信息的可改写性分类 1.4、按信息的可保存性分类 1.5、按功能和存取速度分类 2、存储器技术指标 2.1、存储容量 2.2、存取速度 3、存储系统层次结构 4、主存的基本结构 5、主存中数据的存放 5.…

prometheus|云原生|轻型日志收集系统loki+promtail的部署说明

一&#xff0c; 日志聚合的概念说明 日志------ 每一个程序&#xff0c;服务都应该有保留日志&#xff0c;日志的作用第一是记录程序运行的情况&#xff0c;在出错的时候能够记录错误情况&#xff0c;简单来说就是审计工作&#xff0c;例如nginx服务的日志&#xff0c;kuber…

C++-模板

目录 一.泛型编程 二.模板的分类 三.函数模板 1.函数模板的概念 2.函数模板格式 3.函数模板的原理 4.函数模板的实例化 a.隐式实例化 b.显式实例化 5.模板参数的匹配原则 四.类模板 1.类模板的定义格式 2.类模板的实例化 五.class和typename的区别 六.非类型模板…

docker配置redis插件

docker配置redis插件 运行容器redis_6390 docker run -it \ --name redis_6390 \ --privileged \ -p 6390:6379 \ --network wn_docker_net \ --ip 172.18.12.19 \ --sysctl net.core.somaxconn1024 \ -e TIME_ZONE"Asia/Shanghai" -e TZ"Asia/Shanghai"…

Nacos源码解读04——服务发现

Nacos服务发现的方式 1.客户端获取 1.1:先是故障转移机制判断是否去本地文件中读取信息&#xff0c;读到则返回 1.2:再去本地服务列表读取信息(本地缓存)&#xff0c;没读到则创建一个空的服务&#xff0c;然后立刻去nacos中读取更新 1.3:读到了就返回&#xff0c;同时开启定时…

Linux下activemq的安装与安装成功确认

一、下载 apache-activemq-5.14.0-bin.tar.gz 二、安装 将压缩包拷入linux内&#xff0c;进行解压tar -zxvf apache-activemq-5.14.0-bin.tar.gz&#xff0c;与redis、nginx不同的是&#xff0c;active解压不需要安装就可以直接启动&#xff01; 启动命令&#xff1a;./bin…

1949-2021年全国31省公路里程数据

1949-2021年全国31省公路里程数据 1、指标&#xff1a;公路里程 2、范围&#xff1a;包括31省 1978-2021年期间无缺失 3、来源&#xff1a;各省NJ、产业NJ、各省统计GB 4、指标解释&#xff1a;公路里程指报告期末公路的实际长度。 统计范围&#xff1a;包括城间、城乡间、乡…

Rocketmq架构

NameServer&#xff1a;作为注册中心&#xff0c;提供路由注册、路由踢出、路由发现功能&#xff0c;舍弃强一致&#xff0c;保证高可用&#xff0c;集群中各个节点不会实时通讯&#xff0c;其中一个节点下线之后&#xff0c;会提供另外一个节点保证路由功能。 Rocket mq name…

java学习part32StringBuffer和StringBuilder

Java中的值传递和引用传递&#xff08;详解&#xff09; - 知乎 (zhihu.com) 146-常用类与基础API-StringBuffer与StringBuilder的源码分析、常用方法_哔哩哔哩_bilibili 1. 2.扩容机制 不够用&#xff1a;长度为 原长度*22&#xff1b;如果还不够&#xff0c;那么就扩容到目…

多文件夹图片预处理:清除空值、重置大小、分割训练集

→ 清理空值 防止出现cannot identify image file 参考Python数据清洗----删除读取失败图片__简单版_python用pil读取图片出错删除掉-CSDN博客 #%pip install -i https://pypi.tuna.tsinghua.edu.cn/simple opencv-python #可能需要重启jupyter import os import shutil imp…

【Unity动画】状态机添加参数控制动画切换(Animator Controller)

Unity - 手册&#xff1a;动画参数 在Unity中&#xff0c;动画状态的切换是通过Animator Controller中的过渡&#xff08;Transition&#xff09;来实现的。过渡是状态之间的连接&#xff0c;控制过渡一般都是靠调用代码参数 我们来实现一个案例&#xff1a; 创建动画状态机&a…

leetcode 142.环形链表2

我来更新 leetcode 题目了&#xff0c;接着上一次&#xff0c;这一次是上一道题目的提升&#xff08;有点数学题的感觉&#xff09; 142.环形链表2 题目 给定一个链表的头节点 head &#xff0c;返回链表开始入环的第一个节点。 如果链表无环&#xff0c;则返回 null。 如果链表…

识KDJ指标,看懂超买超卖信号

一、认识KDJ 1、KDJ的含义 KDJ分析股票中短期趋势的一个常用指标&#xff0c;中文名称“随机指标”。它是一个综合考虑股票最高价、最低价和收盘价的技术指标&#xff0c;能够帮助我们根据历史价格预测出股票未来的价格走势。在实际应用的过程中&#xff0c;它的短期预测功能要…

JAVA毕业设计113—基于Java+Springboot+Vue的体育馆预约系统(源代码+数据库+12000字论文)

基于JavaSpringbootVue的体育馆预约系统(源代码数据库12000字论文)113 一、系统介绍 本项目前后端分离&#xff0c;本系统分为管理员、用户两种角色 用户角色包含以下功能&#xff1a; 注册、登录、场地(查看/预订/收藏/退订)、在线论坛、公告查看、我的预订管理、我的收藏…

TimeGPT:时间序列预测模型实例

时间序列预测领域正在经历一个非常激动人心的时期。在过去的三年里&#xff0c;我们见证了许多重要的贡献&#xff0c;如N-BEATS、N-HiTS、PatchTST和TimesNet等。同时&#xff0c;大型语言模型&#xff08;LLM&#xff09;近来在流行度方面取得了很大的成功&#xff0c;例如Ch…