链表
最后更新
链表把一个序列存成一串节点,每个节点保存一个值和一个指向下一个节点的指针。和数组不同,这些节点在内存中并不连续,要沿着 next 指针才能遍历整个链表。点击上方的“播放”,观看节点如何连接到头部和尾部,如何沿着链条查找一个值,以及如何通过改接一个指针删除节点。
在头部插入或删除是 O(1),因为只需让 head 指向新的位置。到达中间或尾部的某个位置则是 O(n),因为必须先一路走过去。两端操作便宜、不能随机访问,这一取舍正是链表和数组的区别所在。
时间复杂度
| 操作 | 复杂度 | 说明 |
|---|---|---|
| 在头部插入 | O(1) | 改变 head 的指向 |
| 在尾部插入 | O(n) | 先走到末尾(有尾指针时为 O(1)) |
| 查找 | O(n) | 沿着 next 指针前进 |
| 删除头节点 | O(1) | 让 head 越过它 |
| 按下标访问 | O(n) | 不支持随机访问 |
链表与数组对比
| 方面 | 链表 | 数组 |
|---|---|---|
| 内存 | 分散的节点 + 指针 | 连续的内存块 |
| 随机访问 | O(n) | O(1) |
| 在开头插入/删除 | O(1) | O(n)(需要移动元素) |
| 缓存友好性 | 差 | 好 |
实例演示
建立链表 [10, 20],在开头加入 5,再删除 20:
| 步骤 | 结构 | 操作 |
|---|---|---|
| 开始 | head -> null | 空链表 |
| 在头部插入 10 | head -> 10 -> null | 让 head 指向一个新节点,新节点的 next 是原来的 head(null) |
| 在尾部插入 20 | head -> 10 -> 20 -> null | 走到节点 10,把它的 next 设为新节点 20 |
| 在头部插入 5 | head -> 5 -> 10 -> 20 -> null | 新节点 5 指向原来的头节点 10;head 现在指向 5 |
| 删除 20 | head -> 5 -> 10 -> null | 走到 10,把它的 next 从 20 改为 null;节点 20 被断开 |
什么时候用链表
| 适合使用 | 不适合使用 |
|---|---|
| 在两端(或已持有的节点处)插入、删除的次数远多于按下标访问 | 你需要按位置快速随机访问(O(1) 下标访问) |
| 数据量变化频繁,你不想承担扩容和复制的开销 | 你在跑紧凑的循环,缓存局部性决定性能 |
| 你在实现队列、栈或邻接表 | 内存紧张:每个元素都要多付出一个指针的空间 |
| 你需要稳定的节点引用,不受其他位置插入的影响 | 你主要是读取、很少修改,这时数组更简单也更快 |
Linked List 代码
简洁、可运行的 Linked List 实现,提供 Python, JavaScript, Java, C++, C 版本。选择一种语言,复制代码,或在 Coddy 在线编辑器中直接打开。
Linked List 的 Python 代码
Python
1class Node:2 def __init__(self, value):3 self.value = value4 self.next = None5
6
7class LinkedList:8 def __init__(self):9 self.head = None10
11 def append(self, value):12 node = Node(value)13 if self.head is None:14 self.head = node15 return16 current = self.head17 while current.next:18 current = current.next19 current.next = node20
21 def find(self, value):22 current = self.head23 while current:24 if current.value == value:25 return True26 current = current.next27 return False28
29 def delete(self, value):30 # Re-link the previous node around the match31 current, prev = self.head, None32 while current:33 if current.value == value:34 if prev is None:35 self.head = current.next36 else:37 prev.next = current.next38 return True39 prev, current = current, current.next40 return False41
42 def __str__(self):43 values, current = [], self.head44 while current:45 values.append(str(current.value))46 current = current.next47 return " -> ".join(values) + " -> None"48
49
50lst = LinkedList()51for value in [3, 7, 1, 9]:52 lst.append(value)53
54print("List: ", lst)55print("find(7): ", lst.find(7))56lst.delete(1)57print("After delete(1):", lst)Linked List 的 JavaScript 代码
JavaScript
1class Node {2 constructor(value) {3 this.value = value;4 this.next = null;5 }6}7
8class LinkedList {9 constructor() {10 this.head = null;11 }12
13 append(value) {14 const node = new Node(value);15 if (!this.head) {16 this.head = node;17 return;18 }19 let current = this.head;20 while (current.next) current = current.next;21 current.next = node;22 }23
24 find(value) {25 for (let n = this.head; n; n = n.next) {26 if (n.value === value) return n;27 }28 return null;29 }30
31 delete(value) {32 if (!this.head) return false;33 if (this.head.value === value) {34 this.head = this.head.next;35 return true;36 }37 // Walk to the node just before the one to remove38 for (let n = this.head; n.next; n = n.next) {39 if (n.next.value === value) {40 n.next = n.next.next;41 return true;42 }43 }44 return false;45 }46
47 toArray() {48 const out = [];49 for (let n = this.head; n; n = n.next) out.push(n.value);50 return out;51 }52}53
54const list = new LinkedList();55for (const value of [10, 20, 30, 40]) list.append(value);56console.log("List:", list.toArray().join(" -> "));57console.log("find(30):", list.find(30) !== null);58list.delete(20);59console.log("After delete(20):", list.toArray().join(" -> "));Linked List 的 Java 代码
Java
1public class Main {2 static class Node {3 int value;4 Node next;5 Node(int value) { this.value = value; }6 }7
8 static Node head;9
10 static void append(int value) {11 Node node = new Node(value);12 if (head == null) { head = node; return; }13 Node cur = head;14 while (cur.next != null) cur = cur.next;15 cur.next = node;16 }17
18 static boolean find(int value) {19 for (Node cur = head; cur != null; cur = cur.next) {20 if (cur.value == value) return true;21 }22 return false;23 }24
25 // Unlink the first node holding value26 static void delete(int value) {27 if (head == null) return;28 if (head.value == value) { head = head.next; return; }29 Node cur = head;30 while (cur.next != null && cur.next.value != value) cur = cur.next;31 if (cur.next != null) cur.next = cur.next.next;32 }33
34 static void print() {35 StringBuilder sb = new StringBuilder();36 for (Node cur = head; cur != null; cur = cur.next) {37 sb.append(cur.value).append(" -> ");38 }39 System.out.println(sb.append("null"));40 }41
42 public static void main(String[] args) {43 append(3); append(7); append(1); append(9);44 print();45 System.out.println("find 7: " + find(7));46 delete(7);47 print();48 System.out.println("find 7: " + find(7));49 }50}Linked List 的 C++ 代码
C++
1#include <iostream>2
3struct Node {4 int value;5 Node* next = nullptr;6 explicit Node(int v) : value(v) {}7};8
9struct LinkedList {10 Node* head = nullptr;11
12 void append(int value) {13 Node* node = new Node(value);14 if (head == nullptr) {15 head = node;16 return;17 }18 Node* cur = head;19 while (cur->next != nullptr) cur = cur->next;20 cur->next = node;21 }22
23 bool find(int value) const {24 for (Node* cur = head; cur != nullptr; cur = cur->next) {25 if (cur->value == value) return true;26 }27 return false;28 }29
30 void remove(int value) {31 if (head == nullptr) return;32 if (head->value == value) { // removing the head is a special case33 Node* old = head;34 head = head->next;35 delete old;36 return;37 }38 for (Node* cur = head; cur->next != nullptr; cur = cur->next) {39 if (cur->next->value == value) {40 Node* old = cur->next;41 cur->next = old->next;42 delete old;43 return;44 }45 }46 }47
48 void print() const {49 for (Node* cur = head; cur != nullptr; cur = cur->next) {50 std::cout << cur->value << " -> ";51 }52 std::cout << "null\n";53 }54};55
56int main() {57 LinkedList list;58 for (int value : {10, 20, 30, 40}) list.append(value);59 list.print();60 std::cout << std::boolalpha << "find(30): " << list.find(30) << "\n";61 list.remove(20);62 list.remove(10);63 list.print();64 return 0;65}Linked List 的 C 代码
C
1#include <stdbool.h>2#include <stdio.h>3#include <stdlib.h>4
5typedef struct Node {6 int value;7 struct Node* next;8} Node;9
10Node* head = NULL;11
12void append(int value) {13 Node* node = malloc(sizeof(Node));14 node->value = value;15 node->next = NULL;16 if (head == NULL) {17 head = node;18 return;19 }20 Node* cur = head;21 while (cur->next != NULL) cur = cur->next;22 cur->next = node;23}24
25bool find(int value) {26 for (Node* cur = head; cur != NULL; cur = cur->next) {27 if (cur->value == value) return true;28 }29 return false;30}31
32void deleteValue(int value) {33 if (head == NULL) return;34 if (head->value == value) { // removing the head is a special case35 Node* old = head;36 head = head->next;37 free(old);38 return;39 }40 for (Node* cur = head; cur->next != NULL; cur = cur->next) {41 if (cur->next->value == value) {42 Node* old = cur->next;43 cur->next = old->next;44 free(old);45 return;46 }47 }48}49
50void printList(void) {51 for (Node* cur = head; cur != NULL; cur = cur->next) {52 printf("%d -> ", cur->value);53 }54 printf("NULL\n");55}56
57int main(void) {58 int values[] = {10, 20, 30, 40};59 for (int i = 0; i < 4; i++) append(values[i]);60 printList();61 printf("find(30): %s\n", find(30) ? "true" : "false");62 deleteValue(20);63 deleteValue(10);64 printList();65 return 0;66}链表常见问题
链表和数组有什么区别?
数组把元素存放在一整块连续内存中,随机访问是
O(1),但插入和删除要移动元素,需要 O(n)。链表的节点可以分散在内存的任意位置,用指针相连,在已知位置插入和删除是 O(1),但访问需要 O(n),因为必须沿着链条走过去。什么时候应该用链表?
当你经常在两端(或在已持有引用的节点处)插入或删除,又很少需要随机访问时,链表最能发挥优势,例如队列、栈、邻接表和 LRU 缓存。如果你需要按下标快速访问,数组或动态数组通常更好。
链表的时间复杂度是多少?
在头部插入或删除是
O(1)。查找,或者在没有尾指针时到达尾部,是 O(n),因为要逐个跟随 next 指针。链表不支持 O(1) 的随机访问,按下标访问是 O(n)。单链表和双向链表有什么区别?
单链表的每个节点只保存一个
next 指针,所以只能单向遍历,删除节点时还需要拿到它的前驱节点。双向链表多了一个 prev 指针,可以反向遍历,也能以 O(1) 删除已持有的节点,代价是每个节点多一个指针,每次插入和删除都要多维护一些信息。既然在头部插入是 O(1),为什么在尾部插入是 O(n)?
在头部插入只需要改接
head 指针,是常数时间的工作。到达尾部则要从头开始沿着 next 指针一直走到末尾,需要 O(n)。额外维护一个 tail 指针,就能让尾部插入也变成 O(1),所以实际使用的链表往往会同时记录两端。链表在任何位置插入都是 O(1) 吗?
不是,这是一个常见误解。只有当你已经拿到要在其后插入的那个节点的指针时,插入本身才是
O(1)。按值或下标找到这个位置仍然要花 O(n),因为你必须沿着链条走过去。