链表里的快慢指针题很多,比如:
- 找倒数第
N个节点 - 删除倒数第
N个节点 - 找链表中点
- 找中点前驱
- 链表切分
- 回文链表
这些题表面上模板很多,但本质上都可以从两个角度理解:
1. 速度差
2. 初始距离差
一、速度差决定大方向
如果两个指针同速移动:
slow = slow.next
fast = fast.next
那么它们之间的距离始终不变。
这种题通常用来解决:
倒数第 N 个节点
删除倒数第 N 个节点
核心是:
先让
fast领先slow若干步,然后一起走。
当fast到达终点时,slow的位置就由这个领先距离决定。
如果两个指针不同速移动:
slow = slow.next
fast = fast.next.next
那么 fast 每轮比 slow 多走一步。
当 fast 到达链表末尾时,slow 大约走到中间。
这种题通常用来解决:
找中点
找中点前驱
链表切分
回文链表
所以:
速度差决定 slow 会落到中间附近。
二、初始距离差决定具体落点
同样是快慢指针,初始化不同,最终结果也不同。
例如:
slow = head
fast = head
fast 初始没有领先 slow。
slow = head
fast = head.next
fast 初始领先 slow 一步。
dummy = ListNode(0)
dummy.next = head
slow = dummy
fast = head
fast 初始也领先 slow 一步。
这个初始距离差会决定:
slow 最后是落在目标节点本身,
还是目标节点前驱;
是偏左中点,
还是偏右中点。
一句话:
速度差决定大方向,初始距离差决定精确落点。
三、什么时候从 head 开始?什么时候从 dummy 开始?
这个规则非常实用:
1. 只是查找节点:一般从 head 开始
比如:
- 找倒数第
N个节点 - 找链表中点
- 判断某个节点位置
这类题只是返回某个节点,不需要修改链表连接关系。
通常写:
slow = head
fast = head
或者:
slow = head
fast = head.next
2. 要修改链接:优先从 dummy 开始
如果题目需要:
- 删除节点
- 断开链表
- 拼接链表
- 找某个节点的前驱
那么最好引入 dummy。
例如:
dummy = ListNode(0)
dummy.next = head
slow = dummy
这样做的好处是:
即使要操作的是头节点,也有统一的前驱节点 dummy。
例如删除头节点:
1 -> 2 -> 3
如果要删除 1,它本身没有前驱。
但是加上 dummy 后:
dummy -> 1 -> 2 -> 3
就可以统一写:
prev.next = prev.next.next
不用单独判断删除的是不是头节点。
四、例题一:找倒数第 N 个节点
题目要求返回倒数第 N 个节点。
这是查找节点,不需要修改链接,所以可以从 head 开始。
def findNthFromEnd(head, n):
slow = head
fast = head
for _ in range(n):
fast = fast.next
while fast:
slow = slow.next
fast = fast.next
return slow
分析:
速度差:0
初始距离差:fast 先走 N 步
结果:fast 到 None 时,slow 在倒数第 N 个节点
这里的核心是:
同速移动时,距离差保持不变。
五、例题二:删除倒数第 N 个节点
删除节点需要修改链接,所以推荐从 dummy 开始。
def removeNthFromEnd(head, n):
dummy = ListNode(0)
dummy.next = head
slow = dummy
fast = head
for _ in range(n):
fast = fast.next
while fast:
slow = slow.next
fast = fast.next
slow.next = slow.next.next
return dummy.next
分析:
slow = dummy
fast = head
此时 fast 已经领先 slow 一步。
然后:
for _ in range(n):
fast = fast.next
fast 又领先了 n 步。
所以总距离差是:
n + 1
当 fast 到达 None 时,slow 停在:
倒数第 N 个节点的前驱
然后删除:
slow.next = slow.next.next
六、例题三:找链表中点
找中点只是查找节点,所以从 head 开始。
如果题目要求:
偶数长度时返回第二个中点
也就是找偏右中点,可以写:
def middleNode(head):
slow = head
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow
分析:
速度差:fast 每次走 2 步,slow 每次走 1 步
初始距离差:0
结果:奇数长度时返回唯一中点,偶数长度时返回偏右中点
例如:
1 -> 2 -> 3 -> 4
返回:
3
七、例题四:找偏左中点
如果偶数长度时想返回第一个中点,也就是偏左中点,可以让 fast 一开始领先一步:
def leftMiddleNode(head):
slow = head
fast = head.next
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow
分析:
速度差:fast 每次走 2 步,slow 每次走 1 步
初始距离差:1
结果:偶数长度时 slow 少走一步,停在偏左中点
例如:
1 -> 2 -> 3 -> 4
返回:
2
八、例题五:链表切分,找中点前驱
链表切分需要修改链接,比如归并排序中要把链表断成两半。
例如:
1 -> 2 -> 3 -> 4
希望切成:
1 -> 2
3 -> 4
需要找到 3 的前驱,也就是 2。
这时要修改链接:
mid_prev.next = None
所以推荐从 dummy 开始。
def getMidPrev(head):
dummy = ListNode(0)
dummy.next = head
slow = dummy
fast = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
return slow
分析:
速度差:fast 每次走 2 步,slow 每次走 1 步
初始距离差:fast 比 slow 领先 1 步
结果:slow 停在偏右中点的前驱
然后切分:
mid_prev = getMidPrev(head)
right = mid_prev.next
mid_prev.next = None
九、核心总结
链表双指针不用死记模板,可以从两个角度看:
1. 速度差
2. 初始距离差
同速双指针
slow = slow.next
fast = fast.next
特点:
速度差为 0,距离差保持不变。
适合:
找倒数第 N 个节点
删除倒数第 N 个节点
结论:
fast 领先 slow K 步,
fast 到 None 时,
slow 就在倒数第 K 个节点。
快慢双指针
slow = slow.next
fast = fast.next.next
特点:
fast 每轮比 slow 多走一步,
slow 最终会落在中间附近。
适合:
找中点
找中点前驱
链表切分
回文链表
常见结论:
slow = head, fast = head
=> 偏右中点
slow = head, fast = head.next
=> 偏左中点
slow = dummy, fast = head
=> 偏右中点前驱
slow = dummy, fast = head.next
=> 偏左中点前驱
十、head 和 dummy 的使用原则
最后再记一个实战原则:
只是查找节点:
通常从 head 开始。
需要修改链接:
优先从 dummy 开始。
原因是:
修改链接通常需要找到目标节点的前驱。
而头节点没有天然前驱,所以引入dummy可以统一处理边界情况。
所以:
找中点:head
找倒数第 N 个节点:head
删除倒数第 N 个节点:dummy
切分链表:dummy
找中点前驱:dummy
最终一句话:
速度差决定 slow 会走到哪一带,初始距离差决定 slow 精确停在哪个节点;如果要修改链表连接关系,就优先让 slow 从 dummy 开始。