实用指南:链表-双向链表【node3】

news/2025/11/20 14:43:34/文章来源:https://www.cnblogs.com/ljbguanli/p/19247005

实用指南:链表-双向链表【node3】

双向链表的基本操作

基础结构

每个节点包含三个部分:

// 节点内部类
static class Node {int data;   // 节点存储的数据Node next;  // 指向下一个节点的引用Node prev;  // 指向前一个节点的引用// 节点构造方法public Node(int data) {this.data = data;this.next = null;this.prev = null;}
}
private Node head;  // 链表头节点
private Node tail;  // 链表尾节点
// 双向链表构造方法
public DoublyLinkedList() {this.head = null;this.tail = null;
}

单链表VS双链表

单链表

  • 每个节点只有一个指向下一个节点的指针
  • 只能从头到尾遍历
  • 无法直接访问前一个节点
  • 内存占用较少

双向链表

  • 每个节点有两个指针:一个指向前一个节点,一个指向后一个节点
  • 可以从头到尾或从尾到头遍历
  • 可以直接访问前一个节点
  • 内存占用较多

插入

双向链表的插入操作需要维护前向和后向指针,但可以在任意位置高效地插入新节点。

头部插入

算法步骤:

  1. 创建新节点
  2. 将新节点的next指向当前的头节点
  3. 将当前头节点的prev指向新节点
  4. 更新头节点为新节点
/*** 在链表头部插入节点* @param data 插入的节点数据*/
public void prepend(int data) {Node newNode = new Node(data);// 如果链表为空if (head == null) {head = newNode;tail = newNode;return;}// 将新节点链接到头部newNode.next = head;head.prev = newNode;head = newNode;
}
尾部插入

步骤

  1. 创建新节点
  2. 将新节点的prev指向当前的尾节点
  3. 将当前尾节点的next指向新节点
  4. 更新尾节点为新节点
/*** 在链表末尾插入节点* @param data 插入的节点数据*/
public void append(int data) {Node newNode = new Node(data);// 如果链表为空if (head == null) {head = newNode;tail = newNode;return;}// 将新节点链接到尾部newNode.prev = tail;tail.next = newNode;tail = newNode;
}
指定位置插入

算法**步骤**

  1. 创建新节点
  2. 将新节点的next指向目标节点
  3. 将新节点的prev指向目标节点的prev
  4. 如果目标节点不是头节点,将目标节点之前的节点的next指向新节点
  5. 将目标节点的prev指向新节点
  6. 如果目标节点是头节点,更新头节点为新节点
/*** 在指定数据的节点前插入新节点* @param targetData 目标节点的数据* @param data 新节点的数据* @return 插入成功返回true,失败返回false*/
public boolean insertBefore(int targetData, int data) {// 如果链表为空if (head == null) {return false;}// 如果目标是头节点if (head.data == targetData) {prepend(data);return true;}Node current = head;// 查找目标节点while (current != null && current.data != targetData) {current = current.next;}// 如果没有找到目标节点if (current == null) {return false;}// 创建新节点并插入Node newNode = new Node(data);newNode.next = current;newNode.prev = current.prev;current.prev.next = newNode;current.prev = newNode;return true;
}

删除

双向链表的删除操作需要更新节点的前向和后向指针,但相比单链表,它不需要遍历到要删除节点的前一个节点。

删除头节点

算法步骤

  1. 将头节点更新为当前头节点的next
  2. 如果新的头节点不为空,将其prev指向NULL
  3. 如果链表变为空,也更新尾节点为NULL
/*** 删除头节点* @return 删除成功返回true,失败返回false*/
public boolean deleteHead() {// 如果链表为空if (head == null) {return false;}// 更新头节点head = head.next;// 如果链表不为空,断开新头节点与旧头节点的连接if (head != null) {head.prev = null;}// 如果链表变为空,同步更新尾节点else {tail = null;}return true;
}
删除尾节点

算法**步骤**

  1. 将尾节点更新为当前尾节点的prev
  2. 如果新的尾节点不为空,将其next指向NULL
  3. 如果链表变为空,也更新头节点为NULL
/*** 删除尾节点* @return 删除成功返回true,失败返回false*/
public boolean deleteTail() {// 如果链表为空if (tail == null) {return false;}// 更新尾节点tail = tail.prev;// 如果链表不为空,断开新尾节点与旧尾节点的连接if (tail != null) {tail.next = null;}// 如果链表变为空,同步更新头节点else {head = null;}return true;
}
删除指定节点

算法**步骤**

  1. 查找要删除的节点
  2. 如果节点是头节点,调用delete_head()
  3. 如果节点是尾节点,调用delete_tail()
  4. 如果节点是中间节点:
    • 将前一个节点的next指向当前节点的next
    • 将后一个节点的prev指向当前节点的prev
/*** 删除指定数据的节点* @param data 要删除节点的数据* @return 删除成功返回true,失败返回false*/
public boolean deleteNode(int data) {// 如果链表为空if (head == null) {return false;}// 如果要删除头节点if (head.data == data) {return deleteHead();}// 如果要删除尾节点if (tail.data == data) {return deleteTail();}// 查找要删除的节点Node current = head;while (current != null && current.data != data) {current = current.next;}// 如果没有找到要删除的节点if (current == null) {return false;}// 删除中间节点(跳过当前节点)current.prev.next = current.next;current.next.prev = current.prev;return true;
}

查找

双向链表可以从头部或尾部开始搜索,这使得查找操作更加灵活

从头部开始查找

/*** 从头开始搜索指定数据的节点* @param data 要搜索的数据* @return 存在返回true,不存在返回false*/
public boolean search(int data) {Node current = head;while (current != null) {if (current.data == data) {return true;}current = current.next;}return false;
}
从尾部开始查找

/*** 从尾开始搜索指定数据的节点* @param data 要搜索的数据* @return 存在返回true,不存在返回false*/
public boolean searchFromTail(int data) {Node current = tail;while (current != null) {if (current.data == data) {return true;}current = current.prev;}return false;
}
从两端查找

优化查找的策略

对于长链表,我们可以同时从头部和尾部开始查找,这样可以减少查找时间:

  • 创建两个指针,一个从头开始,一个从尾开始
  • 两个指针同时向中间移动
  • 比较两个指针的数据和目标数据
  • 当找到目标数据或两个指针相遇或交叉时停止
/*** 优化搜索(从两端同时向中间搜索)* @param data 要搜索的数据* @return 存在返回true,不存在返回false*/
public boolean optimizedSearch(int data) {if (head == null) {return false;}Node headPointer = head;Node tailPointer = tail;// 两端指针未相遇/未交错时继续搜索while (headPointer != tailPointer && headPointer.prev != tailPointer) {// 检查头指针指向的节点if (headPointer.data == data) {return true;}// 检查尾指针指向的节点if (tailPointer.data == data) {return true;}// 移动指针headPointer = headPointer.next;tailPointer = tailPointer.prev;// 指针为空则终止(避免异常)if (headPointer == null || tailPointer == null) {break;}}// 检查最后一次指针相遇时的节点if (headPointer != null && headPointer.data == data) {return true;}return false;
}

更新

更新操作用于修改链表中特定节点的数据。

/*** 更新指定旧数据的节点为新数据* @param oldData 旧数据* @param newData 新数据* @return 更新成功返回true,失败返回false*/
public boolean updateNode(int oldData, int newData) {Node current = head;while (current != null) {if (current.data == oldData) {current.data = newData;return true;}current = current.next;}// 未找到目标节点return false;
}

遍历

双向链表可以从头到尾或从尾到头进行遍历,这是它相对于单链表的一个优势。

双向链表可以从任意一端开始遍历,这使得某些操作更加高效。例如,如果我们知道要访问的节点更靠近尾部,可以从尾部开始遍历,减少遍历的节点数量。

从头到尾遍历

/*** 遍历链表(从头到尾)* @return 存储节点数据的List*/
public List traverseForward() {List elements = new ArrayList<>();Node current = head;while (current != null) {elements.add(current.data);current = current.next;}return elements;
}
从尾到头遍历

/*** 遍历链表(从尾到头)* @return 存储节点数据的List*/
public List traverseBackward() {List elements = new ArrayList<>();Node current = tail;while (current != null) {elements.add(current.data);current = current.prev;}return elements;
}

完整代码

import java.util.ArrayList;
import java.util.List;
/*** @Author Stringzhua* @Date 2025/10/23 15:18* description:*/
public class DoublyLinkedList {// 节点内部类static class Node {int data;   // 节点存储的数据Node next;  // 指向下一个节点的引用Node prev;  // 指向前一个节点的引用// 节点构造方法public Node(int data) {this.data = data;this.next = null;this.prev = null;}}private Node head;  // 链表头节点private Node tail;  // 链表尾节点// 双向链表构造方法public DoublyLinkedList() {this.head = null;this.tail = null;}/*** 遍历链表(从头到尾)* @return 存储节点数据的List*/public List traverseForward() {List elements = new ArrayList<>();Node current = head;while (current != null) {elements.add(current.data);current = current.next;}return elements;}/*** 遍历链表(从尾到头)* @return 存储节点数据的List*/public List traverseBackward() {List elements = new ArrayList<>();Node current = tail;while (current != null) {elements.add(current.data);current = current.prev;}return elements;}/*** 在链表末尾插入节点* @param data 插入的节点数据*/public void append(int data) {Node newNode = new Node(data);// 如果链表为空if (head == null) {head = newNode;tail = newNode;return;}// 将新节点链接到尾部newNode.prev = tail;tail.next = newNode;tail = newNode;}/*** 在链表头部插入节点* @param data 插入的节点数据*/public void prepend(int data) {Node newNode = new Node(data);// 如果链表为空if (head == null) {head = newNode;tail = newNode;return;}// 将新节点链接到头部newNode.next = head;head.prev = newNode;head = newNode;}/*** 在指定数据的节点前插入新节点* @param targetData 目标节点的数据* @param data 新节点的数据* @return 插入成功返回true,失败返回false*/public boolean insertBefore(int targetData, int data) {// 如果链表为空if (head == null) {return false;}// 如果目标是头节点if (head.data == targetData) {prepend(data);return true;}Node current = head;// 查找目标节点while (current != null && current.data != targetData) {current = current.next;}// 如果没有找到目标节点if (current == null) {return false;}// 创建新节点并插入Node newNode = new Node(data);newNode.next = current;newNode.prev = current.prev;current.prev.next = newNode;current.prev = newNode;return true;}/*** 删除头节点* @return 删除成功返回true,失败返回false*/public boolean deleteHead() {// 如果链表为空if (head == null) {return false;}// 更新头节点head = head.next;// 如果链表不为空,断开新头节点与旧头节点的连接if (head != null) {head.prev = null;}// 如果链表变为空,同步更新尾节点else {tail = null;}return true;}/*** 删除尾节点* @return 删除成功返回true,失败返回false*/public boolean deleteTail() {// 如果链表为空if (tail == null) {return false;}// 更新尾节点tail = tail.prev;// 如果链表不为空,断开新尾节点与旧尾节点的连接if (tail != null) {tail.next = null;}// 如果链表变为空,同步更新头节点else {head = null;}return true;}/*** 删除指定数据的节点* @param data 要删除节点的数据* @return 删除成功返回true,失败返回false*/public boolean deleteNode(int data) {// 如果链表为空if (head == null) {return false;}// 如果要删除头节点if (head.data == data) {return deleteHead();}// 如果要删除尾节点if (tail.data == data) {return deleteTail();}// 查找要删除的节点Node current = head;while (current != null && current.data != data) {current = current.next;}// 如果没有找到要删除的节点if (current == null) {return false;}// 删除中间节点(跳过当前节点)current.prev.next = current.next;current.next.prev = current.prev;return true;}/*** 从头开始搜索指定数据的节点* @param data 要搜索的数据* @return 存在返回true,不存在返回false*/public boolean search(int data) {Node current = head;while (current != null) {if (current.data == data) {return true;}current = current.next;}return false;}/*** 从尾开始搜索指定数据的节点* @param data 要搜索的数据* @return 存在返回true,不存在返回false*/public boolean searchFromTail(int data) {Node current = tail;while (current != null) {if (current.data == data) {return true;}current = current.prev;}return false;}/*** 优化搜索(从两端同时向中间搜索)* @param data 要搜索的数据* @return 存在返回true,不存在返回false*/public boolean optimizedSearch(int data) {if (head == null) {return false;}Node headPointer = head;Node tailPointer = tail;// 两端指针未相遇/未交错时继续搜索while (headPointer != tailPointer && headPointer.prev != tailPointer) {// 检查头指针指向的节点if (headPointer.data == data) {return true;}// 检查尾指针指向的节点if (tailPointer.data == data) {return true;}// 移动指针headPointer = headPointer.next;tailPointer = tailPointer.prev;// 指针为空则终止(避免异常)if (headPointer == null || tailPointer == null) {break;}}// 检查最后一次指针相遇时的节点if (headPointer != null && headPointer.data == data) {return true;}return false;}/*** 更新指定旧数据的节点为新数据* @param oldData 旧数据* @param newData 新数据* @return 更新成功返回true,失败返回false*/public boolean updateNode(int oldData, int newData) {Node current = head;while (current != null) {if (current.data == oldData) {current.data = newData;return true;}current = current.next;}// 未找到目标节点return false;}/*** 打印链表(格式:data <-> data <-> ...)*/public void printList() {List elements = new ArrayList<>();Node current = head;while (current != null) {elements.add(String.valueOf(current.data));current = current.next;}System.out.println(String.join(" <-> ", elements));}public static void main(String[] args) {// 1. 创建双向链表实例DoublyLinkedList dll = new DoublyLinkedList();// 2. 向链表末尾插入元素dll.append(10);dll.append(20);dll.append(30);System.out.println("插入末尾后:");dll.printList();  // 输出: 10 <-> 20 <-> 30// 3. 向链表头部插入元素dll.prepend(5);System.out.println("插入头部后:");dll.printList();  // 输出: 5 <-> 10 <-> 20 <-> 30// 4. 在指定节点前插入元素dll.insertBefore(20, 15);System.out.println("在20前插入15后:");dll.printList();  // 输出: 5 <-> 10 <-> 15 <-> 20 <-> 30// 5. 删除头节点dll.deleteHead();System.out.println("删除头节点后:");dll.printList();  // 输出: 10 <-> 15 <-> 20 <-> 30// 6. 删除尾节点dll.deleteTail();System.out.println("删除尾节点后:");dll.printList();  // 输出: 10 <-> 15 <-> 20// 7. 删除中间节点dll.deleteNode(15);System.out.println("删除15后:");dll.printList();  // 输出: 10 <-> 20// 8. 更新节点数据dll.updateNode(20, 25);System.out.println("更新20为25后:");dll.printList();  // 输出: 10 <-> 25// 9. 搜索元素(从头开始)System.out.println("搜索10是否存在: " + dll.search(10));    // 输出: trueSystem.out.println("搜索25是否存在: " + dll.search(25));    // 输出: trueSystem.out.println("搜索15是否存在: " + dll.search(15));    // 输出: false// 10. 从尾部开始搜索System.out.println("从尾部搜索25是否存在: " + dll.searchFromTail(25));  // 输出: true// 11. 优化搜索(双向同时搜索)System.out.println("优化搜索10是否存在: " + dll.optimizedSearch(10));  // 输出: trueSystem.out.println("优化搜索30是否存在: " + dll.optimizedSearch(30));  // 输出: false// 12. 反向遍历链表System.out.println("反向遍历链表: " + dll.traverseBackward());  // 输出: [25, 10]// 13. 测试插入到不存在的节点前boolean success = dll.insertBefore(99, 100);System.out.println("尝试插入到不存在的节点99前: " + (success ? "成功" : "失败"));  // 输出: 失败}
}

时间空间复杂度分析

操作

  • 访问元素
  • 头部/尾部插入
  • 中间插入(已知位置)
  • 中间插入(未知位置)
  • 头部/尾部删除
  • 中间删除(已知位置)
  • 中间删除(未知位置)
  • 查找元素

时间复杂度

  • O(n)
  • O(1)
  • O(1)
  • O(n)
  • O(1)
  • O(1)
  • O(n)
  • O(n)

说明

  • 必须从头或尾遍历
  • 直接操作头尾指针
  • 只需更新前后指针
  • 需要先查找位置
  • 直接操作头尾指针
  • 只需更新前后指针
  • 需要先查找位置
  • 最坏情况需要遍历整个链表

双向链表的空间复杂度为O(n),其中n是链表中的节点数。相比于单链表,双向链表每个节点需要额外的空间来存储prev指针,这使得双向链表的空间效率略低于单链表。

双向链表的主要优势在于:

  • 可以从两个方向遍历
  • 删除和插入操作更加高效(不需要找前驱节点)
  • 可以直接访问前一个节点

双向链表的主要劣势在于:

  • 每个节点需要额外的空间存储prev指针
  • 实现和维护相对复杂

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

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

相关文章

Vue中,list集合中包含实体(对象)的列表,存在某个特定的值在实体类属性是否存在常见的方法:

1、vue中代码// 假设你有一个对象列表 data() {return {items: [{ id: 1, name: Item 1 },{ id: 2, name: Item 2 },{ id: 3, name: Item 3 }]}; }, methods: {isItemPresent(id) {return this.items.some(item => …

2025年复合涤纶布优质厂家权威推荐榜单:涂层涤纶布/阻燃涤纶布/防水涤纶布源头厂家精选

复合涤纶布作为一种多功能复合材料,广泛应用于户外装备、防护服装、家居装饰和产业用纺织品等领域。其市场规模在全球化纤产业升级的推动下持续增长,预计到2031年全球涤纶复合丝市场将保持稳定增长态势。面对市场上众…

List相关知识点

数组(Array) ​ 是一种用连续的内存空间存储相同数据类型数据的线性数据结构,指向首地址​ 在根据数组索引获取元素的时候,会用索引和寻址公式来计算内存所对应的元素数据,寻址公式是:数组的首地址+索引乘以存储…

图文矩阵系统厂家综合测评推荐榜,抖音短视频矩阵/ai排名/短视频矩阵/ai排行榜/ai数字人矩阵/图文矩阵厂家推荐

行业背景分析 随着数字营销进入深度发展期,图文矩阵作为企业内容营销的重要载体,正经历从单一内容输出到多元化矩阵布局的转型。据最新行业数据显示,2024年图文矩阵服务市场规模已达千亿级别,年复合增长率保持在25…

【山东省物联网协会主办,IEEE出版】2025年智慧物联与电子信息工程国际学术会议(IoTEIE 2025)

【山东省物联网协会主办,IEEE出版】2025年智慧物联与电子信息工程国际学术会议(IoTEIE 2025)2025年智慧物联与电子信息工程国际学术会议(IoTEIE 2025) 2025年12月5-7日 中国-青岛 截稿时间:多轮截稿,官网为准 组织单…

ubuntu22.04 安装OpenSSH-server 支持vscode 远程

在 Ubuntu 22.04 上安装 OpenSSH 服务器(openssh-server)的步骤如下: 1. 更新软件包列表 在安装前,建议先更新系统的软件包列表以确保获取最新的软件信息: bashCopy Code sudo apt update 2. 安装 OpenSSH 服务器…

2025年工业溶氧仪实力厂家权威推荐榜单:溶氧分析仪/溶解氧分析仪/在线溶解氧分析仪源头厂家精选

在工业环保要求持续提升与智能监测技术快速发展的背景下,工业溶氧仪作为水质监测的核心设备,其测量精度、稳定性和智能化水平已成为水务管理、污染治理和工业生产过程控制的关键考量。据行业报告显示,全球水质分析仪…

2025年氮气增压泵源头厂家权威推荐榜单:气动增压泵/氦气增压泵/氦气增压泵源头厂家精选

在工业自动化与新能源技术快速发展的背景下,氮气增压泵作为关键流体压力转换设备,其输出压力范围、稳定性与介质兼容性直接影响氢能源、石油化工、航空航天等高端领域的生产安全与效率。据行业报告预测,2025年中国气…

ps2025永久免费破解版下载安装教程(附安装包)ps2025

一.Photoshop2025 下载 在开始安装前,请确保已下载Photoshop2025 安装包,同时,确认计算机满足软件的最低系统要求,以保证安装和运行的流畅性。 Photoshop2025下载链接:https://pan.quark.cn/s/ac9117573682 二.Ph…

vxe-table 如何实现拖拽行数据排序,并对拖拽后进行提示框二次确认是否允许拖拽

vxe-table 如何实现拖拽行数据排序,并对拖拽后进行提示框二次确认是否允许拖拽,通过 row-drag-config.dragStartMethod 可以自定义处理拖拽开始时的拖动 查看官网:https://vxetable.cn gitbub:https://github.com/…

nacos单机版安装

环境:OS:Centos 7nacos:2.5.2 mysql:5.7jdk:21.0.41.下载地址地址https://github.com/alibaba/nacos/releases/tag/2.5.2下载介质nacos-server-2.5.2.tar.gz2.解压安装每个机器都要执行如下操作[root@master soft]# ta…

linux top命令配置重置还原

ubuntu24.04lts top命令修改配置后还原: top命令配置所在 /home/你的用户名/.config/procps/toprc,删除此文件即可还原初始配置。

Linux中: 通过 iostat 怎么判断硬盘是否存在I/O瓶颈

Linux中: "通过 iostat 怎么判断硬盘是否存在I/O瓶颈"硬盘 I/O 指的是对硬盘的读和写操作,使用 IOPS 来衡量硬盘的 IO 能力,假设某块硬盘每秒最多能处理100和写请求,如果单位时间内的写请求数接近这个数…

2025 年便携式 VOC 气体检测仪、气体检测仪厂家十大品牌推荐:精准监测筑牢安全防线,智能传感赋能行业发展

在工业生产、环境监测、市政安全等领域,气体检测设备是守护人员生命安全、保障生产稳定、维护生态环境的核心工具。其中,便携式 VOC 气体检测仪凭借灵活便捷、响应迅速的特点,成为化工园区巡检、燃气泄漏排查、应急…

第九章 顺序容器

9.1 顺序容器的定义 9.2 迭代器和迭代器范围 9.3 顺序容器的操作 9.4 vector容器的自增长 9.5 容器的选用 9.6 string类型 9.7 容器适配器

RustFS vs MinIO:谁才是国产高性能对象存储之光?

RustFS vs MinIO:谁才是国产高性能对象存储之光?2025年,当MinIO社区版突然移除Web管理界面强推商业版时,一款基于Rust语言的国产存储系统RustFS正以4K随机读1,580K IOPS的性能表现和Apache 2.0协议的开放性,向存储…

SOLID原则在React中的应用实践

本文详细探讨了如何将面向对象编程中的SOLID原则应用于React开发,包括单一职责、开闭原则、里氏替换、接口隔离和依赖倒置原则的具体实现方法和代码示例。在React中应用SOLID原则 随着软件行业的发展和在错误中学习,…

绘图工具

https://zhuanlan.zhihu.com/p/531619117

2025 年 11 月离心机厂家推荐排行榜,台式低速大容量离心机,血液离心机,台式低速离心机,台式指针式离心机,台式离心机,小高速离心机,低速微电脑控制离心机,六乘五十毫升离心机,高速离心机公司推荐

2025年11月离心机厂家推荐排行榜:专业选购指南与权威品牌解析 行业背景与发展现状 离心机作为实验室核心设备之一,在生物医药、临床诊断、环境监测、食品安全等领域的应用日益广泛。随着科研水平的不断提升和检测需求…

2025年岩棉板厂家权威推荐榜单:防排烟岩棉板/岩棉条/岩棉隔离带源头厂家精选

在建筑节能与安全标准日益提升的今天,一批深耕于保温材料领域的企业,正凭借其技术实力与严格品控,为建筑穿上高效节能的“外衣”。 岩棉板作为一种重要的建筑保温材料,其防火性能、保温效果及稳定性直接关系到建筑…