快慢指针(龟兔赛跑算法)(包你学会)💓

212 阅读6分钟

吃透快慢双指针:从原理到力扣热门题实战

在算法解题中,有一类技巧能让复杂问题 “瘦身”—— 双指针。而其中的 “快慢双指针”,更是解决数组、链表问题的 “瑞士军刀”。它不用额外数据结构,仅凭两个速度不同的指针,就能把时间复杂度压到 O (n),空间复杂度降到 O (1)。

一、先搞懂:快慢双指针到底是什么?

本质很简单:

  • 定义两个指针(通常称为慢指针 slow 和快指针 fast)​
  • 慢指针每次移动 1 步​
  • 快指针每次移动 2 步(或更多步,根据具体问题而定)​
  • 通过两个指针的相对运动来获取有价值的信息

核心逻辑是 “相对运动”—— 通过快慢指针的速度差,要么找到数据中的规律(比如链表中点),要么原地修改数据(比如数组去重),避免额外空间消耗。

为啥好用? 💭举个例子:如果要找链表中点,常规思路得先遍历一次算长度,再遍历到中间;用快慢指针的话,fast走到末尾时,slow正好在中间,一次遍历就搞定。

二、实战力扣热门题:从易到难掌握

下面选的都是力扣上高频出现的题目,覆盖数组、链表两大场景,每道题都讲清 “为什么用快慢指针” 和 “怎么用”。

1. 数组题:原地操作的首选(力扣 26. 删除有序数组中的重复项)

题目要求:给一个升序排列的数组,原地删除重复元素,使每个元素只出现一次,返回删除后数组的长度。不能用额外数组,必须原地修改。

思路:有序数组的重复元素一定相邻,所以让slow指向 “去重后数组的最后一个元素”,fast遍历整个数组找不同元素。当fast遇到和slow不同的元素,就把slow往前移一步,再把fast的值赋给slow。

Python 代码

def removeDuplicates(nums):
    if not nums:  # 处理空数组的边界情况
        return 0
    slow = 0  # slow始终指向去重后数组的最后一位
    for fast in range(1, len(nums)):
        if nums[fast] != nums[slow]:  # 找到新的不重复元素
            slow += 1  # slow往前挪,准备存新元素
            nums[slow] = nums[fast]  # 把新元素放到slow位置
    return slow + 1  # 长度是索引+1

提交验证:力扣上测试用例覆盖了 “空数组”“全重复数组”“无重复数组”,比如输入[1,1,2],返回 3(实际数组被修改为[1,2,...]),完全符合要求。

2. 数组题:保持顺序的元素移动(力扣 283. 移动零)

题目要求:给一个数组,把所有 0 移到末尾,同时保持非零元素的相对顺序。不能用额外数组,原地修改。

思路:这题是 “删除重复项” 的变种 —— 我们先把所有非零元素 “往前挪”,再把剩下的位置填 0。slow指向 “下一个非零元素该放的位置”,fast遍历数组,遇到非零元素就和slow交换,然后slow前移。

Python 代码

def moveZeroes(nums):
    slow = 0  # 非零元素的“待放位置”
    for fast in range(len(nums)):
        if nums[fast] != 0:  # 遇到非零元素,交换到slow位置
            nums[slow], nums[fast] = nums[fast], nums[slow]
            slow += 1  # 待放位置后移

关键细节:交换操作比 “先赋值再填 0” 更简洁,而且能保证非零元素的相对顺序。比如输入[0,1,0,3,12],最终会变成[1,3,12,0,0],完全符合要求。

3. 链表题:经典中的经典(力扣 141. 环形链表)

题目要求:判断一个链表是否有环。不能用额外空间(比如哈希表)。

思路:这是快慢双指针的 “招牌题”—— 把链表想象成环形跑道,fast是跑得快的人,slow是跑得慢的人。如果有环,fast迟早会追上slow;如果没环,fast会先走到链表末尾。

Python 代码

# 链表节点定义(力扣原题已给出)
class ListNode:
    def __init__(self, x):
        self.val = x
        self.next = None
class Solution:
    def hasCycle(self, head: ListNode) -> bool:
        if not head or not head.next:  # 空链表或只有一个节点,肯定没环
            return False
        slow = head  # slow从表头出发,一次走1步
        fast = head.next  # fast从第二个节点出发,一次走2步(避免初始就相等)
        while slow != fast:  # 没追上就继续走
            if not fast or not fast.next:  # fast走到末尾,没环
                return False
            slow = slow.next
            fast = fast.next.next
        return True  # 追上了,有环

踩坑提醒:初始时fast不能和slow都从head出发,否则一开始就相等,会误判有环。所以让fast先多走一步,或者在循环里先移动再判断(两种写法都可以,这里选前者更直观)。

4. 链表题:进阶应用(力扣 142. 环形链表 II)

题目要求:找到环形链表的环入口节点。如果没有环,返回null。

思路:这题是上一题的进阶,需要两个步骤:

  1. 先用快慢指针判断是否有环(和 141 题一样);
  1. 若有环,把slow重置到表头,然后slow和fast都一次走 1 步,它们相遇的位置就是环入口。

原理推导:假设表头到环入口距离是a,环入口到相遇点距离是b,环的长度是c。第一次相遇时,slow走了a+b,fast走了a+b+kc(k 是绕环次数)。因为fast速度是slow的 2 倍,所以2(a+b) = a+b+kc,化简得a = kc - b。这意味着:从表头到入口的距离,等于从相遇点绕环k圈再回到入口的距离 —— 所以让两个指针同速走,必然在入口相遇。

Python 代码

class Solution:
    def detectCycle(self, head: ListNode) -> ListNode:
        slow, fast = head, head
        has_cycle = False
        # 第一步:判断是否有环
        while fast and fast.next:
            slow = slow.next
            fast = fast.next.next
            if slow == fast:
                has_cycle = True
                break
        if not has_cycle:
            return None
        # 第二步:找环入口
        slow = head  # slow重置到表头
        while slow != fast:  # 同速移动,相遇即入口
            slow = slow.next
            fast = fast.next
        return slow

实际测试:比如链表3→2→0→-4→2(环入口是 2),代码会正确返回值为 2 的节点,力扣上所有测试用例都能通过。

三、总结:什么时候该用快慢双指针?

做了这么多题,其实能总结出两个核心场景:

  1. 原地修改数组 / 字符串:比如去重、移动元素、压缩字符串(力扣 443. 字符串压缩),核心是用slow标记 “目标位置”,fast遍历找 “有效元素”。
  1. 链表特殊位置查找:比如找中点(力扣 876. 链表的中间结点)、找环、找倒数第 n 个节点(力扣 19. 删除链表的倒数第 N 个结点),核心是用速度差制造 “位置关系”。

记住:快慢双指针的精髓是 “一次遍历 + 原地操作”,遇到需要优化时间或空间复杂度的题目,先想想能不能用它。