【数据结构与算法 | 第二篇】 双链表的增删改查

双链表的每个节点都保存两个方向的引用:next 指向后继节点,prev 指向前驱节点。它支持从头向尾遍历,也支持拿到尾节点后反向遍历。

这类题的关键不在于记住多少段代码,而在于每次修改节点关系时,都同时维护前后两个方向的指针。

一、先定义双链表节点

java 复制代码
class DoublyListNode {
    int val;
    DoublyListNode next;
    DoublyListNode prev;

    DoublyListNode(int val) {
        this.val = val;
    }
}

val 保存节点数据;next 和 prev 分别保存后继、前驱节点。头节点的 prev 为 null,尾节点的 next 为 null。

二、根据数组创建双链表

下面把数组 {1, 2, 3, 4, 5} 组装成一条双链表。

java 复制代码
DoublyListNode createDoublyLinkedList(int[] arr) {
    if (arr == null || arr.length == 0) {
        return null;
    }

    DoublyListNode head = new DoublyListNode(arr[0]);
    DoublyListNode cur = head;

    for (int i = 1; i < arr.length; i++) {
        DoublyListNode newNode = new DoublyListNode(arr[i]);
        cur.next = newNode;
        newNode.prev = cur;
        cur = newNode;
    }
    return head;
}

循环中的两次赋值必须配对出现:cur.next = newNode 建立从当前节点到新节点的正向关系,newNode.prev = cur 建立反向关系。随后让 cur 移到新节点,继续拼接后续元素。

执行后,链表关系为:

text 复制代码
null <- 1 <-> 2 <-> 3 <-> 4 <-> 5 -> null

三、双向遍历:查找和读取节点

从头向后遍历时,可以顺手保存最后访问到的节点;该节点就是尾节点,接着便可从尾向前遍历。

java 复制代码
DoublyListNode head = createDoublyLinkedList(new int[]{1, 2, 3, 4, 5});
DoublyListNode tail = null;

for (DoublyListNode p = head; p != null; p = p.next) {
    System.out.println(p.val);
    tail = p;
}

for (DoublyListNode p = tail; p != null; p = p.prev) {
    System.out.println(p.val);
}

第一次循环依次输出 1 到 5,循环结束后 tail 指向节点 5;第二次循环沿 prev 依次输出 5 到 1。双链表的反向遍历能力,正是 prev 引用带来的直接收益。

四、插入节点

1. 在头部插入

java 复制代码
DoublyListNode newHead = new DoublyListNode(0);
newHead.next = head;

if (head != null) {
    head.prev = newHead;
}
head = newHead;

插入后链表变为 0 <-> 1 <-> 2 <-> 3 <-> 4 <-> 5。这里的判空不能省略:当原链表为空时,新节点直接成为头节点,不存在旧头节点可更新。

2. 在尾部插入

如果只有头节点引用,需要先找到尾节点:

java 复制代码
if (head == null) {
    head = new DoublyListNode(6);
} else {
    DoublyListNode tail = head;
    while (tail.next != null) {
        tail = tail.next;
    }

    DoublyListNode newNode = new DoublyListNode(6);
    tail.next = newNode;
    newNode.prev = tail;
}

尾插的连接顺序很简单:原尾节点的 next 指向新节点,新节点的 prev 回指原尾节点。若业务中经常尾插,建议额外维护 tail 引用,这样无需每次从头遍历。

3. 在中间插入

下面将 66 插到节点 3 和节点 4 之间:

java 复制代码
DoublyListNode p = head;
for (int i = 0; i < 2; i++) {
    p = p.next;
}

DoublyListNode newNode = new DoublyListNode(66);
DoublyListNode nextNode = p.next;

newNode.prev = p;
newNode.next = nextNode;
p.next = newNode;
nextNode.prev = newNode;

此时 p 指向节点 3,nextNode 暂存节点 4。**先保存原来的后继节点,再完成四条连接关系。**结果为 1 <-> 2 <-> 3 <-> 66 <-> 4 <-> 5。

中间插入前要保证 p 和 p.next 都不为 null。若插入位置是尾部,应转用尾插逻辑,避免访问 nextNode.prev 时触发空指针。

五、删除节点

删除的本质是让被删节点的前驱和后继重新连接,再将被删节点的引用断开。

1. 删除中间节点

删除节点 4,先定位到它的前一个节点 3:

java 复制代码
DoublyListNode p = head;
for (int i = 0; i < 2; i++) {
    p = p.next;
}

DoublyListNode toDelete = p.next;
DoublyListNode nextNode = toDelete.next;

p.next = nextNode;
nextNode.prev = p;

toDelete.next = null;
toDelete.prev = null;

执行后,3.next 改为指向 5,5.prev 改为指向 3,链表变为 1 <-> 2 <-> 3 <-> 5。

这段代码用于删除中间节点,因此 toDelete 和 toDelete.next 必须存在。删除尾节点时不能直接使用 nextNode.prev = p,应单独处理。

2. 删除头节点

java 复制代码
if (head != null) {
    DoublyListNode toDelete = head;
    head = head.next;

    if (head != null) {
        head.prev = null;
    }

    toDelete.next = null;
}

若原链表是 1 <-> 2 <-> 3,删除后头节点变为 2,并将 2.prev 清空。只有一个节点时,head 会变为 null,判空正好覆盖该情况。

将 toDelete.next 置空不是 Java 垃圾回收的必要条件,但能让已删除节点脱离原链表,代码调试时也更容易看清节点已经被摘除。

3. 删除尾节点

java 复制代码
if (head != null) {
    DoublyListNode tail = head;
    while (tail.next != null) {
        tail = tail.next;
    }

    if (tail.prev == null) {
        head = null;
    } else {
        tail.prev.next = null;
        tail.prev = null;
    }
}

当链表有多个节点时,尾节点前一个节点的 next 置为 null,尾节点的 prev 也置为 null。当链表只有一个节点时,tail.prev 为 null,此时直接令 head = null,否则访问 tail.prev.next 会报空指针异常。

六、写双链表代码时的检查顺序

每次插入或删除节点后,可以按下面四项检查:

  1. 头节点的 prev 是否为 null;
  2. 尾节点的 next 是否为 null;
  3. 从头沿 next 遍历和从尾沿 prev 遍历,节点顺序是否互为反向;
  4. 空链表、单节点、头部和尾部这几个边界位置是否单独处理。

总结

双链表操作始终围绕前驱和后继两个方向展开。创建、插入和删除后,只要保证相邻节点之间的 next、prev 关系同步更新,并补上空链表与单节点判断,代码就更稳定,也更不容易在边界场景中出错。

相关推荐
牧以南歌〆3 小时前
数据结构<一>顺序表
c语言·数据结构·算法
玖玥拾17 小时前
C++ 数据结构 八大基础排序算法专题
数据结构·c++·算法·排序算法
Tim_1018 小时前
【C++】017、new/delete与malloc/free的区别
java·数据结构·算法
岑梓铭20 小时前
《考研408数据结构》第六章(6.3 图的遍历)复习笔记
数据结构·笔记·考研··拓扑排序·408
noipp1 天前
推荐题目:洛谷 P13554 【MX-X15-T1】奶龙龙
c语言·数据结构·c++·算法·编程·洛谷
AI科技星1 天前
《全域数学·工程应用大典》113–200讲完整总目录
数据结构·人工智能·算法·机器学习·线性回归·乖乖数学·全域数学
米尔的可达鸭1 天前
深入操作系统 Socket 底层:EPOLLOUT 可写事件管理 + 非阻塞异步
开发语言·网络·数据结构·经验分享·websocket·网络协议
Irissgwe1 天前
算法之二分查找
数据结构·c++·算法·二分查找
小蒋学算法1 天前
算法-滑动窗口最大值
数据结构·算法