01先读懂这个概念
从两数之和继续,但增加一个前提
现在数组已经升序:a=[1,2,4,7,11,15],target=15。把 left 放在 0,right 放在 5,当前和为 16。因为和太大,我们希望它变小;但『希望变小』还不是证明,必须说明丢弃 right 不会漏解。
对当前 right=5,其他左端点的值都不小于 a[left]=1。因此任何 a[k]+15 都至少为 16,不可能等于 15。可以安全排除以 right 为右端的所有配对,令 right 减一。反过来,和太小时,固定 left 搭配其他更小的右端只会更小,因此应增加 left。
移动的不是答案,而是候选范围
算法不保证每移动一步就更接近某个唯一答案,它保证被丢弃的候选都不可能是答案。left 只增、right 只减,剩余区间不断缩小。当 left=right 时已经凑不出两个不同位置,循环应停止。
有序性是排除证明的根基。输入无序时,要么用上一课的哈希表,要么连同原下标一起排序,并计入 O(n log n) 排序成本。单纯把代码里的两个变量叫作 left、right,不会自动产生正确算法。
02从具体数据开始手算
拿一个小输入,逐步手算
在 [1,2,4,7,11,15] 中找到和为 15 的两个数,返回从 0 开始的下标。
| left / right | 两个值 | 和 | 决定 |
|---|---|---|---|
| 0 / 5 | 1 + 15 | 16 | 太大,排除右端 15 |
| 0 / 4 | 1 + 11 | 12 | 太小,排除左端 1 |
| 1 / 4 | 2 + 11 | 13 | 太小,排除左端 2 |
| 2 / 4 | 4 + 11 | 15 | 返回 [2,4] |
没有枚举 15 对候选,而是每一步用一次比较排除一整行或一整列配对。最后返回下标,数值则是 4 和 11。
03把正确性的理由说清楚
排除规则的完整理由
- 初始化时所有 i<j 的候选都在 [left,right] 内。
- 若 a[left]+a[right]<target,对任意 k≤right,a[left]+a[k] 都不够大,因此可以排除 left;和大于目标时,对称地排除 right。
- 每步保留全部仍可能的答案并缩小区间。命中即得到合法配对;区间不足两项时,不再存在合法候选。
04把刚才的思路写成代码
基础课用 Python 表达,先对照变量含义与执行顺序。后续算法课提供 Python、C++ 与 JavaScript 三种实现。
def sorted_two_sum(a, target):
left, right = 0, len(a) - 1
while left < right:
total = a[left] + a[right]
if total == target:
return [left, right]
if total < target:
left += 1
else:
right -= 1
return []代码里的每个关键决定
while left < right:- 不能写 ≤,否则可能把同一个下标用两次。空数组和单元素数组都自然跳过循环。
if total < target:- 在升序前提下,固定左值已经无法凑够目标,因此丢弃整个左端候选,而不是随便试一个方向。
return [left, right]- 本课返回 0 基下标。如果外部题目要求从 1 开始编号,需要在这里加 1,不能混用约定。
05这些操作要付出多少代价
把工作量具体数出来
每轮至少移动一个指针。二者之间的距离初始为 n−1,最多做 n−1 轮,所以时间 O(n)。
只保存两个下标与当前和,辅助空间 O(1)。若输入需要先排序,总时间要加上排序成本;若复制输入保留原数组,空间也会增加。
存在负数不会破坏证明,只要整个数组仍然升序。真正依赖的是单调次序,而不是数值必须为正。
06用一个反例检查理解
无序输入会错误排除答案
a=[3,2,4],target=6。先算 3+4=7,右移到 2 后算 3+2=5,最终相遇并报告无解,漏掉了 2+4。
怎样修正:检查前提:这个双指针版本只用于有序数组。无序时使用哈希表,或排序并处理原下标。
07自己推一次,再做自测
有负数还能用吗
对 [-4,-1,2,5,8] 和 target=7 手算双指针,写出每次和与被排除的位置。
需要一点提示
当前和小于 7 时,检查固定左端配更小的右端能否改善。
查看完整推导与答案
- −4+8=4,太小,left 从 0 到 1。
- −1+8=7,返回 [1,4]。
- 负数不影响有序性的排除逻辑;结果由 −1 与 8 构成。