双链表的每个节点都保存两个方向的引用: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 会报空指针异常。
六、写双链表代码时的检查顺序
每次插入或删除节点后,可以按下面四项检查:
- 头节点的 prev 是否为 null;
- 尾节点的 next 是否为 null;
- 从头沿 next 遍历和从尾沿 prev 遍历,节点顺序是否互为反向;
- 空链表、单节点、头部和尾部这几个边界位置是否单独处理。
总结
双链表操作始终围绕前驱和后继两个方向展开。创建、插入和删除后,只要保证相邻节点之间的 next、prev 关系同步更新,并补上空链表与单节点判断,代码就更稳定,也更不容易在边界场景中出错。