双向链表
最后更新
双向链表是一种每个节点都有两个指针的链表:next(指向后一个节点)和 prev(指向前一个节点)。多出来的这个反向链接让你可以双向遍历;当你已经持有某个节点的引用时,还能以 O(1) 删除它,因为可以直接找到它的前驱,而不必从头节点一路走过去。点击上方的“播放”,观察节点如何双向连接和重新连接。
代价是每个节点多一个指针,维护工作也更多:每次插入和删除都必须同时更新相邻节点的 next 和 prev。很多双端队列和 LRU 缓存都基于这种结构,因为它们需要从两端快速移除元素。
时间复杂度
| 操作 | 复杂度 | 说明 |
|---|---|---|
| 在头部或尾部插入 | O(1) | 需要头指针和尾指针 |
| 删除已知节点 | O(1) | 直接找到 prev,无需遍历 |
| 查找 | O(n) | 仍然是线性扫描 |
| 按下标访问 | O(n) | 不支持随机访问 |
单向链表与双向链表对比
| 方面 | 单向链表 | 双向链表 |
|---|---|---|
| 每个节点的指针数 | 1 (next) | 2 (next + prev) |
| 反向遍历 | 不支持 | 支持 |
| 删除已知节点 | O(n)(要找 prev) | O(1) |
| 内存开销 | 较低 | 较高 |
示例演算
通过追加构建 [10, 20, 30],然后删除节点 20:
| 步骤 | 结构 | 操作 |
|---|---|---|
| 开始 | null | 空链表:head 和 tail 都是 null |
| 追加 10 | 10 | 第一个节点;head 和 tail 都指向它,prev = next = null |
| 追加 20 | 10 <-> 20 | 令 10.next = 20,20.prev = 10;tail = 20 |
| 追加 30 | 10 <-> 20 <-> 30 | 令 20.next = 30,30.prev = 20;tail = 30 |
| 删除 20 | 10 <-> 30 | 借助 20.prev 和 20.next:以 O(1) 令 10.next = 30,30.prev = 10 |
什么时候使用双向链表
| 适合使用的情况 | 不适合使用的情况 |
|---|---|
| 需要双向遍历或拼接 | 只会向前移动,单向链表更轻量 |
要删除已持有引用的任意节点(O(1)) | 主要按位置访问,应使用数组以获得 O(1) 访问 |
| 要实现双端队列、LRU 缓存或屏幕上的撤销历史 | 内存紧张,额外的 prev 指针会让链接开销翻倍 |
| 频繁在两端插入和移除元素 | 数据量小且很少变化,复制数组更省事 |
Doubly Linked List 代码
简洁、可运行的 Doubly Linked List 实现,提供 Python, JavaScript, Java, C++, C 版本。选择一种语言,复制代码,或在 Coddy 在线编辑器中直接打开。
Doubly Linked List 的 Python 代码
Python
1class Node:2 def __init__(self, value):3 self.value = value4 self.prev = None5 self.next = None6
7
8class DoublyLinkedList:9 def __init__(self):10 self.head = None11 self.tail = None12
13 def append(self, value):14 node = Node(value)15 if self.tail is None:16 self.head = self.tail = node17 return18 # Wire both directions: old tail <-> new node19 node.prev = self.tail20 self.tail.next = node21 self.tail = node22
23 def prepend(self, value):24 node = Node(value)25 if self.head is None:26 self.head = self.tail = node27 return28 node.next = self.head29 self.head.prev = node30 self.head = node31
32 def forward(self):33 values, current = [], self.head34 while current:35 values.append(current.value)36 current = current.next37 return values38
39 def backward(self):40 values, current = [], self.tail41 while current:42 values.append(current.value)43 current = current.prev44 return values45
46
47lst = DoublyLinkedList()48for value in [2, 3, 4]:49 lst.append(value)50lst.prepend(1)51
52print("Forward: ", lst.forward())53print("Backward:", lst.backward())Doubly Linked List 的 JavaScript 代码
JavaScript
1class Node {2 constructor(value) {3 this.value = value;4 this.prev = null;5 this.next = null;6 }7}8
9class DoublyLinkedList {10 constructor() {11 this.head = null;12 this.tail = null;13 }14
15 // Tail pointer makes append O(1)16 append(value) {17 const node = new Node(value);18 if (!this.tail) {19 this.head = node;20 this.tail = node;21 return;22 }23 node.prev = this.tail;24 this.tail.next = node;25 this.tail = node;26 }27
28 forward() {29 const out = [];30 for (let n = this.head; n; n = n.next) out.push(n.value);31 return out;32 }33
34 backward() {35 const out = [];36 for (let n = this.tail; n; n = n.prev) out.push(n.value);37 return out;38 }39}40
41const list = new DoublyLinkedList();42for (const value of [10, 20, 30, 40]) list.append(value);43console.log("Forward: ", list.forward().join(" <-> "));44console.log("Backward:", list.backward().join(" <-> "));Doubly Linked List 的 Java 代码
Java
1public class Main {2 static class Node {3 int value;4 Node prev, next;5 Node(int value) { this.value = value; }6 }7
8 static Node head, tail;9
10 static void append(int value) {11 Node node = new Node(value);12 if (head == null) { head = tail = node; return; }13 node.prev = tail;14 tail.next = node;15 tail = node;16 }17
18 // Unlink in O(1) once found: fix both neighbor pointers19 static void delete(int value) {20 for (Node cur = head; cur != null; cur = cur.next) {21 if (cur.value != value) continue;22 if (cur.prev != null) cur.prev.next = cur.next; else head = cur.next;23 if (cur.next != null) cur.next.prev = cur.prev; else tail = cur.prev;24 return;25 }26 }27
28 public static void main(String[] args) {29 append(1);30 append(2);31 append(3);32 append(4);33
34 StringBuilder forward = new StringBuilder("Forward:");35 for (Node cur = head; cur != null; cur = cur.next) forward.append(" ").append(cur.value);36 System.out.println(forward);37
38 StringBuilder backward = new StringBuilder("Backward:");39 for (Node cur = tail; cur != null; cur = cur.prev) backward.append(" ").append(cur.value);40 System.out.println(backward);41
42 delete(3);43 StringBuilder after = new StringBuilder("After delete 3:");44 for (Node cur = head; cur != null; cur = cur.next) after.append(" ").append(cur.value);45 System.out.println(after);46 }47}Doubly Linked List 的 C++ 代码
C++
1#include <iostream>2
3struct Node {4 int value;5 Node* prev = nullptr;6 Node* next = nullptr;7 explicit Node(int v) : value(v) {}8};9
10struct DoublyLinkedList {11 Node* head = nullptr;12 Node* tail = nullptr;13
14 void append(int value) {15 Node* node = new Node(value);16 if (tail == nullptr) {17 head = tail = node;18 return;19 }20 node->prev = tail; // link both directions21 tail->next = node;22 tail = node;23 }24
25 void prepend(int value) {26 Node* node = new Node(value);27 if (head == nullptr) {28 head = tail = node;29 return;30 }31 node->next = head;32 head->prev = node;33 head = node;34 }35
36 void printForward() const {37 for (Node* cur = head; cur != nullptr; cur = cur->next) {38 std::cout << cur->value << " <-> ";39 }40 std::cout << "null\n";41 }42
43 void printBackward() const {44 for (Node* cur = tail; cur != nullptr; cur = cur->prev) {45 std::cout << cur->value << " <-> ";46 }47 std::cout << "null\n";48 }49};50
51int main() {52 DoublyLinkedList list;53 for (int value : {10, 20, 30}) list.append(value);54 list.prepend(5);55 std::cout << "Forward: ";56 list.printForward();57 std::cout << "Backward: ";58 list.printBackward();59 return 0;60}Doubly Linked List 的 C 代码
C
1#include <stdio.h>2#include <stdlib.h>3
4typedef struct Node {5 int value;6 struct Node* prev;7 struct Node* next;8} Node;9
10Node* head = NULL;11Node* tail = NULL;12
13Node* newNode(int value) {14 Node* n = calloc(1, sizeof(Node));15 n->value = value;16 return n;17}18
19void append(int value) {20 Node* node = newNode(value);21 if (tail == NULL) {22 head = tail = node;23 return;24 }25 node->prev = tail; // link both directions26 tail->next = node;27 tail = node;28}29
30void prepend(int value) {31 Node* node = newNode(value);32 if (head == NULL) {33 head = tail = node;34 return;35 }36 node->next = head;37 head->prev = node;38 head = node;39}40
41void printForward(void) {42 for (Node* cur = head; cur != NULL; cur = cur->next) {43 printf("%d <-> ", cur->value);44 }45 printf("NULL\n");46}47
48void printBackward(void) {49 for (Node* cur = tail; cur != NULL; cur = cur->prev) {50 printf("%d <-> ", cur->value);51 }52 printf("NULL\n");53}54
55int main(void) {56 int values[] = {10, 20, 30};57 for (int i = 0; i < 3; i++) append(values[i]);58 prepend(5);59 printf("Forward: ");60 printForward();61 printf("Backward: ");62 printBackward();63 return 0;64}双向链表常见问题
单向链表和双向链表有什么区别?
单向链表的每个节点只有一个指针(
next),所以只能向前移动。双向链表多了一个 prev 指针,可以反向遍历,并以 O(1) 删除已持有引用的节点,代价是每个节点多一个指针,每次修改都要更新两个链接。什么时候值得为双向链表付出额外的内存?
当你需要反向遍历,或者需要以
O(1) 删除已经引用的任意节点时。典型场景是双端队列和 LRU 缓存,其中的条目会频繁地从两端移动和淘汰。为什么双向链表能以 O(1) 删除节点?
删除一个节点时,必须把它的前驱和后继连起来。在单向链表中,你得从头节点一路走过去才能找到前驱(
O(n));在双向链表中,节点的 prev 指针直接给出前驱,所以重新连接只需 O(1)。双向链表和数组,该用哪个?
如果需要按下标
O(1) 访问、对缓存友好的遍历,并且插入大多发生在末尾,就用数组。如果经常在已有节点引用的前提下在中间或两端插入、删除,就用双向链表,因为这只需 O(1),而数组需要 O(n) 的移动。数组在内存和局部性上占优,链表在结构修改上占优。双向链表中的哨兵节点是什么?
哨兵节点(也叫虚拟节点)是放在头节点之前和/或尾节点之后的占位节点,让链表永远不会真正为空。它省去了对
head、tail 以及边界插入和删除的 null 检查,让每个操作都走同一套重新连接的代码。很多生产环境中的 LRU 缓存实现都用两个哨兵,避免对两端做特殊处理。从双向链表中删除节点时,最常见的 bug 是什么?
删除第一个或最后一个节点时,忘了更新
head 或 tail,结果留下一个指向已释放节点的悬空指针。在重新连接相邻节点之前,一定要检查 node.prev 是否为 null(需要更新 head),以及 node.next 是否为 null(需要更新 tail)。