打爆链表双指针的狗头:速度差与初始距离差

链表里的快慢指针题很多,比如:

  • 找倒数第 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 开始。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇