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

双链表的每个节点都保存两个方向的引用: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 关系同步更新,并补上空链表与单节点判断,代码就更稳定,也更不容易在边界场景中出错。

相关推荐
旖旎夜光16 小时前
LeetCode 738:单调递增的数字(贪心算法) —— 题解
数据结构·c++·算法·leetcode·贪心算法
旖旎夜光17 小时前
LeetCode 1262:可被三整除的最大和 (贪心算法)—— 题解
数据结构·c++·算法·leetcode·贪心算法
影视飓风TIM17 小时前
C++ STL 完整入门笔记[9]:C++ 仿函数、priority_queue与STL算法底层笔记
数据结构·c++·笔记
依然鸣18 小时前
蓝桥杯多校模拟赛(第二场)题解
数据结构·c++·经验分享·学习·算法·图论
依然鸣18 小时前
蓝桥杯多校模拟赛 题解
数据结构·c++·经验分享·学习·算法·蓝桥杯
wabs66618 小时前
关于图论【Kruskal算法|卡码网53.寻宝的思考】
数据结构·算法·图论·卡码网
小星星闪亮登场1 天前
2026萌新联赛第三场-- (郑州轻工业大学)
数据结构·c++·经验分享·算法·贪心算法·排序算法·深度优先
冻柠檬飞冰走茶1 天前
PTA基础编程题目集 7-8超速判断(C++语言实现)
开发语言·数据结构·c++·算法
2501_942389551 天前
Epoch AI旗下FrontierMath的负责人Elliot Glazer
数据结构·决策树·动态规划·散列表
shylyly_2 天前
104.二叉树的最大深度
数据结构·算法·104.二叉树的最大深度