30 秒速览
- 什么时候用:链表数据结构上的操作
- 核心技巧:虚拟头节点、快慢指针、反转、画图
- 时间复杂度:通常 O(n)
- 空间复杂度:通常 O(1)
一、链表基础
链表 vs 数组
| 特性 | 数组 | 链表 |
|---|---|---|
| 随机访问 | O(1) | O(n) |
| 插入/删除 | O(n) | O(1)(已知位置) |
| 内存 | 连续 | 分散 |
| 大小 | 固定(或动态扩容) | 灵活 |
链表节点定义
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
核心操作:画图
链表题最大的坑是指针丢失。解决方案:
- 画图:在纸上画出节点和指针
- 保存引用:修改前先保存需要的信息
- 分步操作:一步一步来,不要跳步
原链表: 1 → 2 → 3 → 4 → 5
删除节点 3:
1. prev.next = cur.next (跳过 cur)
2. 节点 3 自动被回收
结果: 1 → 2 → 4 → 5
二、核心技巧
技巧 1:虚拟头节点
问题:在链表头部插入/删除需要特殊处理。
解决:添加一个虚拟头节点 dummy,统一所有操作。
def insert_at_head(head: ListNode, val: int) -> ListNode:
dummy = ListNode(0, head) # 虚拟头
new_node = ListNode(val)
new_node.next = dummy.next
dummy.next = new_node
return dummy.next # 返回真正的头
应用场景:
- 需要删除头节点
- 需要在头部插入
- 合并两个链表
技巧 2:快慢指针
应用场景:
- 检测环
- 找中点
- 找倒数第 k 个
# 检测环
def hasCycle(head: ListNode) -> bool:
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
# 找中点
def findMiddle(head: ListNode) -> ListNode:
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow
# 找倒数第 k 个
def findKthFromEnd(head: ListNode, k: int) -> ListNode:
fast = head
# 快指针先走 k 步
for _ in range(k):
if not fast:
return None
fast = fast.next
# 快慢指针同时走
slow = head
while fast:
slow = slow.next
fast = fast.next
return slow
技巧 3:反转链表
迭代法
def reverseList(head: ListNode) -> ListNode:
prev = None
cur = head
while cur:
next_temp = cur.next # 保存下一个
cur.next = prev # 反转
prev = cur # 移动 prev
cur = next_temp # 移动 cur
return prev
图解:
原链表: 1 → 2 → 3 → None
初始: prev=None, cur=1
第1步: 1 → None, prev=1, cur=2
第2步: 2 → 1 → None, prev=2, cur=3
第3步: 3 → 2 → 1 → None, prev=3, cur=None
结果: 3 → 2 → 1 → None
递归法
def reverseList(head: ListNode) -> ListNode:
if not head or not head.next:
return head
# 递归反转后面的部分
new_head = reverseList(head.next)
# 反转当前节点
head.next.next = head
head.next = None
return new_head
技巧 4:双指针合并
def mergeTwoLists(l1: ListNode, l2: ListNode) -> ListNode:
dummy = ListNode(0)
cur = dummy
while l1 and l2:
if l1.val <= l2.val:
cur.next = l1
l1 = l1.next
else:
cur.next = l2
l2 = l2.next
cur = cur.next
# 连接剩余部分
cur.next = l1 if l1 else l2
return dummy.next
三、典型例题
例题 1:LC 206. 反转链表
题目:给你单链表的头节点 head,请你反转链表,并返回反转后的链表。
代码
def reverseList(head: ListNode) -> ListNode:
prev = None
cur = head
while cur:
next_temp = cur.next
cur.next = prev
prev = cur
cur = next_temp
return prev
复杂度:时间 O(n),空间 O(1)
例题 2:LC 92. 反转链表 II
题目:给你单链表的头指针 head 和两个整数 left 和 right,其中 left <= right。请你反转从位置 left 到位置 right 的链表节点,返回反转后的链表。
思路
- 找到 left 前面的节点 pre
- 反转 left 到 right 的部分
- 连接回去
代码
def reverseBetween(head: ListNode, left: int, right: int) -> ListNode:
if left == 1:
return reverseN(head, right)
# 移动到 left 位置
head.next = reverseBetween(head.next, left - 1, right - 1)
return head
def reverseN(head: ListNode, n: int) -> ListNode:
"""反转前 n 个节点"""
if n == 1:
return head
# 反转后面的部分
new_head = reverseN(head.next, n - 1)
# 保存第 n+1 个节点
successor = head.next.next
# 反转当前节点
head.next.next = head
head.next = successor
return new_head
复杂度:时间 O(n),空间 O(n)(递归栈)
例题 3:LC 21. 合并两个有序链表
题目:将两个升序链表合并为一个新的升序链表并返回。
代码
def mergeTwoLists(l1: ListNode, l2: ListNode) -> ListNode:
dummy = ListNode(0)
cur = dummy
while l1 and l2:
if l1.val <= l2.val:
cur.next = l1
l1 = l1.next
else:
cur.next = l2
l2 = l2.next
cur = cur.next
cur.next = l1 if l1 else l2
return dummy.next
复杂度:时间 O(m + n),空间 O(1)
例题 4:LC 141. 环形链表
题目:给你一个链表的头节点 head,判断链表中是否有环。
代码
def hasCycle(head: ListNode) -> bool:
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
return True
return False
为什么快慢指针能检测环?
如果有环,快指针最终会追上慢指针(想象操场上跑步,快的人会套圈慢的人)。
复杂度:时间 O(n),空间 O(1)
例题 5:LC 142. 环形链表 II
题目:给定一个链表的头节点 head,返回链表开始入环的第一个节点。如果链表无环,则返回 null。
思路
- 快慢指针找到相遇点
- 一个指针从头部出发,一个从相遇点出发,再次相遇就是环入口
数学证明:
设:
- 头到环入口距离为 a
- 环入口到相遇点距离为 b
- 相遇点到环入口距离为 c
- 环长 = b + c
慢指针走了:a + b
快指针走了:a + b + k(b + c),其中 k ≥ 1
快指针速度是慢指针两倍:
a + b + k(b + c) = 2(a + b)
a = c + (k-1)(b + c)
所以:从头走 a 步 = 从相遇点走 c 步 + 可能绕几圈
即:两个指针分别从头和相遇点出发,会在环入口相遇
代码
def detectCycle(head: ListNode) -> ListNode:
# 快慢指针找相遇点
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow == fast:
break
else:
return None # 无环
# 两个指针分别从头和相遇点出发
ptr1 = head
ptr2 = slow
while ptr1 != ptr2:
ptr1 = ptr1.next
ptr2 = ptr2.next
return ptr1
复杂度:时间 O(n),空间 O(1)
例题 6:LC 19. 删除链表的倒数第 N 个结点
题目:给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
代码
def removeNthFromEnd(head: ListNode, n: int) -> ListNode:
dummy = ListNode(0, head)
fast = slow = dummy
# 快指针先走 n 步
for _ in range(n):
fast = fast.next
# 快慢指针同时走
while fast.next:
fast = fast.next
slow = slow.next
# 删除倒数第 n 个
slow.next = slow.next.next
return dummy.next
复杂度:时间 O(n),空间 O(1)
例题 7:LC 160. 相交链表
题目:给你两个单链表的头节点 headA 和 headB,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null。
思路
让两个指针分别走 A+B 和 B+A 的路径,如果有交点,会在交点相遇。
A: a1 → a2 → c1 → c2 → c3
B: b1 → b2 → b3 → c1 → c2 → c3
pA 路径: a1 → a2 → c1 → c2 → c3 → b1 → b2 → b3 → c1
pB 路径: b1 → b2 → b3 → c1 → c2 → c3 → a1 → a2 → c1
pA 和 pB 在 c1 相遇!
代码
def getIntersectionNode(headA: ListNode, headB: ListNode) -> ListNode:
if not headA or not headB:
return None
pA, pB = headA, headB
while pA != pB:
pA = pA.next if pA else headB
pB = pB.next if pB else headA
return pA # 相交节点或 None
复杂度:时间 O(m + n),空间 O(1)
例题 8:LC 234. 回文链表
题目:给你一个单链表的头节点 head,请你判断该链表是否为回文链表。
思路
- 快慢指针找中点
- 反转后半部分
- 比较前后两部分
- (可选)恢复链表
代码
def isPalindrome(head: ListNode) -> bool:
# 找中点
slow = fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
# 反转后半部分
prev = None
cur = slow
while cur:
next_temp = cur.next
cur.next = prev
prev = cur
cur = next_temp
# 比较
left, right = head, prev
while right: # 只需要比较后半部分
if left.val != right.val:
return False
left = left.next
right = right.next
return True
复杂度:时间 O(n),空间 O(1)
例题 9:LC 148. 排序链表
题目:给你链表的头结点 head,请将其按升序排列并返回排序后的链表。要求 O(n log n) 时间复杂度和常数级空间复杂度。
思路
归并排序:
- 找中点
- 递归排序两半
- 合并
代码
def sortList(head: ListNode) -> ListNode:
if not head or not head.next:
return head
# 找中点(用快慢指针,注意要断开)
slow = fast = head
prev = None
while fast and fast.next:
prev = slow
slow = slow.next
fast = fast.next.next
prev.next = None # 断开
# 递归排序
left = sortList(head)
right = sortList(slow)
# 合并
return merge(left, right)
def merge(l1: ListNode, l2: ListNode) -> ListNode:
dummy = ListNode(0)
cur = dummy
while l1 and l2:
if l1.val <= l2.val:
cur.next = l1
l1 = l1.next
else:
cur.next = l2
l2 = l2.next
cur = cur.next
cur.next = l1 if l1 else l2
return dummy.next
复杂度:时间 O(n log n),空间 O(log n)(递归栈)
四、常见错误与陷阱
错误 1:指针丢失
# 错误:直接修改,丢失了 next
cur.next = new_node
new_node = cur.next # new_node 是刚才赋值的,不是原来的 next!
# 正确:先保存
next_temp = cur.next
cur.next = new_node
new_node.next = next_temp
错误 2:忘记虚拟头
# 错误:删除头节点时需要特殊处理
if head.val == target:
head = head.next # 需要更新 head
# 正确:使用虚拟头,统一处理
dummy = ListNode(0, head)
# ... 操作 ...
return dummy.next
错误 3:快慢指针初始位置
# 找中点时,如果需要断开,要记住前一个节点
slow = fast = head
prev = None # 记住 slow 的前一个
while fast and fast.next:
prev = slow
slow = slow.next
fast = fast.next.next
prev.next = None # 断开
错误 4:环检测忘记处理边界
# 错误:空链表会出错
while fast.next: # fast 可能是 None
# 正确:先检查 fast
while fast and fast.next:
五、复盘清单
触发信号
- 链表数据结构
- 需要反转
- 需要找中点/倒数第 k 个
- 需要检测环
- 需要合并
边界条件
- 空链表
- 单节点链表
- 两节点链表
- 删除头节点
- 删除尾节点
常见错误
- 指针丢失
- 忘记虚拟头
- 快慢指针边界
- 没有断开链表
六、题目清单
基础题
| 题号 | 题目 | 难度 | 考点 |
|---|---|---|---|
| 206 | 反转链表 | Easy | 反转 |
| 21 | 合并两个有序链表 | Easy | 合并 |
| 141 | 环形链表 | Easy | 快慢指针 |
| 876 | 链表的中间结点 | Easy | 快慢指针 |
| 19 | 删除倒数第 N 个 | Medium | 快慢指针 |
进阶题
| 题号 | 题目 | 难度 | 考点 |
|---|---|---|---|
| 92 | 反转链表 II | Medium | 反转区间 |
| 142 | 环形链表 II | Medium | 环入口 |
| 160 | 相交链表 | Easy | 双指针 |
| 234 | 回文链表 | Easy | 中点+反转 |
| 148 | 排序链表 | Medium | 归并排序 |
| 23 | 合并 K 个升序链表 | Hard | 堆/分治 |