👆👇 双指针 — 对撞与追击的艺术

用两个指针在数组上跳舞,将O(n²)暴力法变为O(n)优雅解

📖 双指针的核心概念

双指针(Two Pointers)是一种通过两个指针在数据结构上协同移动来解决问题的技巧。根据指针移动方向的不同,可分为对撞指针和快慢指针两大类。双指针能将许多 O(n²) 的问题优化为 O(n),是面试中出现频率极高的技巧。

双指针的两种经典模式: ┌─ 对撞指针 (相向而行) ─────────────────┐ │ │ │ [1, 2, 3, 4, 5, 6, 7, 8] │ │ ↑ ↑ │ │ left right │ │ → ← ← ← ← ← ← ← ← ← ← ← → │ │ 两端向中间靠拢 │ │ 适用: 有序数组找配对、回文判断 │ └─────────────────────────────────────────┘ ┌─ 快慢指针 (同向而行) ─────────────────┐ │ │ │ [1, 2, 3, 4, 5, 6, 7, 8] │ │ ↑ ↑ │ │ slow fast │ │ → → → → → → → → → → → │ │ fast追超slow │ │ 适用: 原地修改、链表环检测 │ └─────────────────────────────────────────┘

1. 对撞指针详解

核心思想:两个指针分别从数组两端出发,向中间移动。
适用条件:数组有序(或可以排序)
移动规则:根据当前状态决定移动哪一侧指针
经典场景:两数之和(有序版)、盛水容器、三数之和

2. 快慢指针详解

核心思想:两个指针同向移动,但速度不同(快指针领先)。
适用条件:不需要排序,适用于任何数组/链表
移动规则:快指针每步都动,慢指针按条件动
经典场景:删除元素、去重、链表找环

3. 何时使用双指针?

信号推荐指针类型示例题目
有序数组找配对对撞指针LC 167, LC 11
原地删除/去重快慢指针LC 26, LC 27
连续子数组问题滑动窗口LC 3, LC 76
链表环检测快慢指针LC 141, LC 142
n数之和对撞+固定LC 15, LC 18

🎯 题目一:盛最多水的容器 (LC 11)

题目描述:给定一个长度为 n 的整数数组 height,有 n 条垂线,第 i 条线的两个端点是 (i, 0)(i, height[i])。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。返回容器可以储存的最大水量。

示例: height = [1, 8, 6, 2, 5, 4, 8, 3, 7] │ █ █ │ █ █ █ █ │ █ █ █ █ █ │ █ █ █ █ █ █ │ █ █ █ █ █ █ █ └───────────────────────────────────── 0 1 2 3 4 5 6 7 8 left=1, right=8: 面积 = min(8,7) × (8-1) = 49 这就是最大面积!

思路分析

暴力法:枚举所有线对,计算面积取最大。
时间复杂度:O(n²),面试需要优化。
对撞指针法(最优):
初始化 left=0, right=n-1。
面积 = min(height[left], height[right]) × (right - left)
关键贪心:每次移动较矮的那一侧指针!
为什么?移动较矮的指针,宽度减少1,但可能找到更高的线,面积可能增大。
移动较高的指针,宽度减少1,高度不可能超过较矮的线,面积一定减少。
时间复杂度:O(n),空间复杂度:O(1)

代码实现

class Solution:
    def maxArea(self, height: List[int]) -> int:
        left, right = 0, len(height) - 1
        max_area = 0
        while left < right:
            width = right - left
            h = min(height[left], height[right])
            max_area = max(max_area, width * h)
            if height[left] < height[right]:
                left += 1
            else:
                right -= 1
        return max_area

复杂度分析

时间复杂度:O(n) — 两个指针各最多移动 n 次

空间复杂度:O(1) — 只用常数变量

变体练习

🎯 题目二:三数之和 (LC 15)

题目描述:给你一个整数数组 nums,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ≠ j ≠ knums[i] + nums[j] + nums[k] = 0。请你返回所有不重复的三元组。

示例: nums = [-1, 0, 1, 2, -1, -4] 排序后: [-4, -1, -1, 0, 1, 2] 固定 -1 → 双指针找 [-1, 0, 1, 2] 中的两数之和 = 1 left=0(0), right=3(2) → 0+2=2 > 1 → right-- left=0(0), right=2(1) → 0+1=1 = 1 ✓ → [-1, 0, 1] 固定 -1(第二个) → 跳过,避免重复 结果: [[-1, -1, 2], [-1, 0, 1]]

思路分析

暴力法:三重循环枚举所有三元组,去重。
时间复杂度:O(n³),绝对不可接受。
排序 + 双指针(最优):
1. 先排序 O(n log n)
2. 固定第一个数 nums[i],在剩余区间用对撞指针找两个数
3. 去重:固定数和指针移动时跳过重复值
时间复杂度:O(n²),空间复杂度:O(1)(不含输出)

代码实现

class Solution:
    def threeSum(self, nums: List[int]) -> List[List[int]]:
        nums.sort()
        n = len(nums)
        result = []
        for i in range(n - 2):
            if i > 0 and nums[i] == nums[i - 1]:
                continue
            if nums[i] > 0:
                break
            left, right = i + 1, n - 1
            while left < right:
                s = nums[i] + nums[left] + nums[right]
                if s == 0:
                    result.append([nums[i], nums[left], nums[right]])
                    while left < right and nums[left] == nums[left + 1]:
                        left += 1
                    while left < right and nums[right] == nums[right - 1]:
                        right -= 1
                    left += 1
                    right -= 1
                elif s < 0:
                    left += 1
                else:
                    right -= 1
        return result

复杂度分析

时间复杂度:O(n²) — 外层 O(n),内层双指针 O(n)

空间复杂度:O(1) — 排序是原地的

去重是本题的难点!三种去重位置:(1) 固定数跳过重复;(2) 找到答案后 left/right 跳过重复;(3) 剪枝:nums[i] > 0 直接 break。

变体练习

🎯 题目三:删除有序数组中的重复项 (LC 26)

题目描述:给你一个非严格递增排列的数组 nums,请你原地删除重复出现的元素,使每个元素只出现一次,返回删除后数组的新长度。元素的相对顺序应该保持一致。

示例: nums = [0, 0, 1, 1, 1, 2, 2, 3, 3, 4] 快慢指针过程: slow=0, fast=1: nums[0]=0 ≠ nums[1]=0 → fast++ slow=0, fast=2: nums[0]=0 ≠ nums[2]=1 → slow=1, nums[1]=1, fast++ slow=1, fast=3: nums[1]=1 ≠ nums[3]=1 → fast++ slow=1, fast=4: nums[1]=1 ≠ nums[4]=1 → fast++ slow=1, fast=5: nums[1]=1 ≠ nums[5]=2 → slow=2, nums[2]=2, fast++ ... 结果: [0, 1, 2, 3, 4], 长度=5

思路分析

快慢指针法:慢指针 slow 记录已去重部分的末尾位置,快指针 fast 遍历数组。
当 nums[fast] ≠ nums[slow] 时,发现新元素:slow 前进一步,将 nums[fast] 复制到 nums[slow]。
当 nums[fast] = nums[slow] 时,是重复元素,fast 继续前进。
时间复杂度:O(n),空间复杂度:O(1)

代码实现

class Solution:
    def removeDuplicates(self, nums: List[int]) -> int:
        if not nums:
            return 0
        slow = 0
        for fast in range(1, len(nums)):
            if nums[fast] != nums[slow]:
                slow += 1
                nums[slow] = nums[fast]
        return slow + 1

复杂度分析

时间复杂度:O(n) — fast 指针遍历一次

空间复杂度:O(1) — 原地修改

变体练习

成就解锁:双指针大师 — 掌握对撞指针与快慢指针,能在有序数组上优雅降维
LeetCode AC验证:LC 11 Container With Most Water ✅ | LC 15 3Sum ✅ | LC 26 Remove Duplicates from Sorted Array ✅

📝 课后练习

  1. LC 167 两数之和 II(有序数组的对撞指针解法)
  2. LC 344 反转字符串(对撞指针)
  3. LC 680 验证回文串 II(对撞指针+贪心)
  4. LC 88 合并两个有序数组(逆向双指针)
  5. LC 125 验证回文串(对撞指针+字符过滤)

🔑 本课要点回顾

📚 扩展阅读

思考题:四数之和如何用双指针解决?时间复杂度是多少?如果数组没有排序,还能用双指针吗?如果要求四数之和等于 target 且不重复呢?