精通编码面试:第2部分(双指针模式)

发布: (2026年3月31日 GMT+8 00:32)
9 分钟阅读
原文: Dev.to

Source: Dev.to

如果你已经阅读了本系列的第 1 部分,就会知道单一的模式如何把慢速、暴力的解法转变为优雅且高效的方案。今天我们将做同样的事——但换上一件新工具:双指针模式。

这种模式在编码面试中随处可见。一旦你认出它,就会在以前可能让你苦恼的问题中频频看到它。

什么是双指针模式?

思路很简单:与其使用一个索引遍历数据结构,不如使用 两个。这两个指针根据特定条件在数组(或字符串)中移动,协同帮助你在不需要嵌套循环的情况下找到答案。

变体描述
相反方向指针一个指针从开头开始,另一个从末尾开始。它们向彼此移动,直至相遇。
同方向指针(快慢指针)两个指针从同一端开始,但以不同速度或在不同条件下移动。

让我们直接进入一个问题,看看第一种变体的实际应用。

Source:

问题 1 — 两数之和 II:输入数组已排序

Two Sum II: Input Array Is Sorted

给定一个 从 1 开始索引 的整数数组 numbers,该数组已按非递减顺序排序,找出两个数使它们的和等于特定的 target 值。返回这两个数的索引,形式为长度为 2 的整数数组 [index1, index2]

暴力解法

class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        for i in range(len(numbers)):
            for j in range(i + 1, len(numbers)):
                if numbers[i] + numbers[j] == target:
                    return [i + 1, j + 1]   # 1‑indexed result
        return []

时间复杂度O(n²) – 对于 30 000 个元素的数组,这相当于 9 亿次比较。
结果:在大规模输入(LeetCode)上 超出时间限制。在实际生产环境中,对大数据集进行二次循环是性能瓶颈。

使用双指针模式的优化

我们在暴力方法中忽略的关键点是 数组已经排好序。这个提示让我们可以使用双指针模式:

  1. 将一个指针放在开头(left),另一个放在末尾(right)。
  2. 计算它们的和。
    • 若和 等于 目标 → 返回索引。
    • 若和 过大 → 将 right 向左移动,以获得更小的值。
    • 若和 过小 → 将 left 向右移动,以获得更大的值。

每一次迭代都会剔除一个候选,由于指针会相互逼近,我们能够在一次遍历中找到答案(或确认不存在)。

class Solution:
    def twoSum(self, numbers: List[int], target: int) -> List[int]:
        left = 0                     # start of the array
        right = len(numbers) - 1     # end of the array

        while left < right:
            current_sum = numbers[left] + numbers[right]

            if current_sum == target:
                # Return 1‑indexed result
                return [left + 1, right + 1]

            if current_sum > target:
                # sum too large → move right pointer left
                right -= 1
            else:
                # sum too small → move left pointer right
                left += 1

        return []   # problem guarantees a solution, but good practice

时间复杂度O(n)
空间复杂度O(1)

相比暴力解法,这是一项显著的改进,并且在 LeetCode 上能够顺利通过。

变体 2 — 同向指针

第二种变体并不是从相对两端收敛。相反,两个指针从同一侧开始,并 朝相同方向 移动,但速度不同或在不同条件下移动。这非常适合需要 原地重构 数组或在向前扫描时检测特定模式的问题。

让我们来看一个经典例子。


Source:

Problem 2 — Remove Duplicates from Sorted Array

Remove Duplicates from Sorted Array

给定一个按非递减顺序排序的整数数组 nums,请 原地 删除重复元素,使每个唯一元素只出现一次。返回唯一元素的数量 k

直观的做法可能是使用集合收集唯一值并重新构建数组——但题目明确要求 原地 解法,且只能使用 O(1) 额外空间。采用同向双指针技术(通常称为 慢指针快指针)可以高效地解决此问题。

(后续解法将在下一篇文章中继续。)

使用 O(1) 额外内存的原地解法

使用集合会导致 O(n) 的空间开销,违背题意。

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        seen = set()
        result = []

        for num in nums:
            if num not in seen:
                seen.add(num)
                result.append(num)

        # This doesn't satisfy the in‑place constraint
        for i in range(len(result)):
            nums[i] = result[i]

        return len(result)

除了违反空间限制,这种做法还创建了我们不需要的辅助数据结构。
双指针 模式可以提供更简洁且符合要求的解法。

优化:同向双指针

思路: 使用 slow 指针记录下一个唯一元素应写入的位置,fast 指针向前扫描数组寻找新的唯一值。每当 fast 发现的值与 slow 所指的值不同时,就把 slow 前移并将该新值写入 slow 所在位置。

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        # 'slow' marks the boundary of our unique‑elements section
        slow = 0

        # 'fast' scans ahead to discover new unique elements
        for fast in range(1, len(nums)):
            # When fast finds a value different from slow's position,
            # a new unique element has been discovered
            if nums[fast] != nums[slow]:
                slow += 1                  # Expand the unique section
                nums[slow] = nums[fast]    # Write the new unique value in place

        # slow is now the index of the last unique element
        # so the count of unique elements is slow + 1
        return slow + 1
  • 时间复杂度: O(n)fast 只遍历数组一次。
  • 空间复杂度: O(1) — 完全在原地完成,没有额外的数据结构。

这里的巧妙之处在于两指针协同工作:fast 负责探索,slow 负责写入。它们永不冲突,因为 slow 永远在 fast 的同位置或其后方。

何时使用此模式

  • 当处理 已排序 的数组或字符串时(这是使用相反方向指针的一个重要提示)。
  • 当问题要求你找到满足条件的 一对(或三元组)元素时。
  • 当你需要 就地 重构或去重数组,且空间复杂度为 O(1) 时。
  • 当暴力解法涉及对同一线性数据结构的 嵌套循环 时——这几乎总是表明可以使用双指针将其压缩到 O(n)。

接下来做什么?练习

前往 LeetCode 并尝试 Two Pointers problem list —— 该标签下有超过 200 道题目。先从简单题开始培养直觉,然后再挑战中等难度。

接下来可以尝试的几个好题

  • Container With Most Water (Medium) — opposite‑direction pointers.
  • 3Sum (Medium) — two pointers inside an outer loop.
  • Linked List Cycle (Easy) — fast & slow pointers on a linked list.

注意: 与滑动窗口模式一样,Two Pointers 并不总是唯一的有效方法。当满足特定条件时——如已排序的数据、原地约束、配对/子集问题——它是一个强大的工具——但在使用之前请始终思考该模式是否真正适合。

在下一篇文章中再见,届时会介绍另一个编码模式!

0 浏览
Back to Blog

相关文章

阅读更多 »