看题解的时候觉得双指针算法特别简单:左指针往右挪,右指针往左挪,或者一个走快一个走慢,来回就那几行代码。可一旦脱离题解自己上手,很多人会陷入同一种窘境——盯着题目很久,也不敢确定这题到底能不能用双指针,指针该从哪头开始。我在这个阶段挣扎了很长时间,直到把刷过的双指针题目放在一起横向对比,才真正理解它的本质:双指针不是一种需要硬记的招式,而是利用序列本身的顺序特征,把暴力解法中大量无效的搜索空间系统性地剪掉。这篇内容想用几个最经典的题目,把双指针的几类典型模式拆开讲清楚,包含每类模式的适用条件、核心代码和我在实际刷题过程中踩过的细节坑。不管你现在是刚开始刷题的入门选手,还是准备面试需要快速过一遍算法框架的开发者,这篇文章都值得耐心读完。
1. 暴力解法到底浪费在哪:双指针效率的根源
讲双指针之前,我觉得有必要先把"暴力解法为什么慢"这个问题想透。只有理解了浪费在哪里,才能明白双指针的每一步移动到底在省什么。
1.1 一个最简单的例子:有序数组的两数之和
假设现在有一个有序数组 numbers = [1, 2, 3, 4, 6, 8, 9, 11],目标值是 10,要求找出两个数,使它们的和等于 target,返回这两个数的下标。
暴力解法非常直接:两层循环枚举所有的二元组合,第一次外循环固定 1,内循环依次跟 2、3、4、6、8、9、11 相加;第二次外循环固定 2,再依次跟后面的所有元素相加。这样一共要比较 C(8, 2) = 28 次,当数组长度是 n 时,比较次数是 n*(n-1)/2,时间复杂度是 O(n²)。
如果数组长度是 1000,大概是 50 万次比较;如果长度是 10 万,那就是 50 亿次。这个增长速度在真实场景下很难接受。
关键是,这 28 次比较里有多少是"注定没结果"的?非常多。固定 1 的时候,1 + 11 = 12 已经大于 10,后面继续加 9、加 11 都只会更大,这些比较完全没有意义。换句话说,暴力解法最大的问题不是"比较慢",而是"把大量一边已经越界的组合依然拿过来硬比"。
1.2 单调性:双指针能成立的底层合同
为什么双指针能把这些无效比较直接跳过去?
因为数组是有序的,这给了我们一个非常强的信息:numbers[left] 和 numbers[right] 都沿着各自的方向单调变化。如果 numbers[left] + numbers[right] < target,说明什么?说明在维持 right 不变的情况下,把 left 往右移动,得到的和会变大;反过来,如果把 left 往左移动(已经不能了)或者把 right 往右移动(也不能了),没有意义。而更大的和才有希望接近 target,所以唯一正确的方向是 left++。
同样的逻辑,如果 numbers[left] + numbers[right] > target,说明和太大了,继续增大 left 只会更大,唯一正确的方向是 right--。
这就是双指针效率的根源——每一步指针移动都是"被数据特征逼出来的唯一选择",不会像暴力那样做无意义的尝试。可以把这种性质理解成双指针能成立的"底层合同":你必须在序列上找到某种单调性,无论是数组有序、窗口内累积和单调增加,还是链表环结构的周期性,只要这个合同存在,双指针就能把你的搜索空间从平方米级别的穷举,压缩到一条线性的路径上。
我还想特意强调一下,这里说的单调性不一定是数组有序。滑动窗口里的累积和、快慢指针在环里的相对距离变化,本质上都是某种"可预测的单调关系"。我们在后面几章会反复看到这个思想的影子。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 左右对撞指针:有序搜索和三数之和的骨架
2.1 双指针移动的"唯一性"原理
基于上面的分析,左右对撞指针的实现就非常自然了。left 指向数组开头,right 指向数组末尾,每次根据两数之和与 target 的大小关系,二选一地移动一个指针,直到相遇。
python复制def twoSum(numbers, target):
left, right = 0, len(numbers) - 1
while left < right:
s = numbers[left] + numbers[right]
if s == target:
return [left + 1, right + 1] # 题目要求下标从1开始
elif s < target:
left += 1
else:
right -= 1
return [-1, -1]
这段代码在有序数组两数之和里是标准答案,但比代码更重要的是理解它为什么不会漏解。
有人会担心:你每次只移动一个指针,是不是可能把正确答案错过了?不会。想象搜索空间是一个二维矩阵
