学习顺序主线第 07 / 22 课 · 阶段 3

为什么现在学:把二分中的排除思想推广为两个边界一起缩小候选范围。

前置知识 二分查找
基础课程

双指针:为什么能移动这一边Two Pointers

指针只是位置。真正的算法,是利用有序性证明一整组候选可以排除。

35 分钟 · 含手算与练习

学完这一课,你应该能做到

  • 维护 left<right,保证使用两个不同位置
  • 用不等式解释每次移动
  • 区分有序双指针与无序哈希查找

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 / 51 + 1516太大,排除右端 15
0 / 41 + 1112太小,排除左端 1
1 / 42 + 1113太小,排除左端 2
2 / 44 + 1115返回 [2,4]

没有枚举 15 对候选,而是每一步用一次比较排除一整行或一整列配对。最后返回下标,数值则是 4 和 11。

03把正确性的理由说清楚

排除规则的完整理由

  1. 初始化时所有 i<j 的候选都在 [left,right] 内。
  2. 若 a[left]+a[right]<target,对任意 k≤right,a[left]+a[k] 都不够大,因此可以排除 left;和大于目标时,对称地排除 right。
  3. 每步保留全部仍可能的答案并缩小区间。命中即得到合法配对;区间不足两项时,不再存在合法候选。

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 时,检查固定左端配更小的右端能否改善。

查看完整推导与答案
  1. −4+8=4,太小,left 从 0 到 1。
  2. −1+8=7,返回 [1,4]。
  3. 负数不影响有序性的排除逻辑;结果由 −1 与 8 构成。

01当前和太小时,为什么能排除 left?

02这段算法最重要的输入前提是?

03先排序再双指针的总时间通常是?

继续阅读

本课例子与推导为本站编写。需要另一种表述或进一步学习时,可对照这些公开课程与文档:

MIT 6.006 · 算法讲义Python 官方教程 · 数据结构

能独立完成本课练习,再标记掌握。也可以直接用上面的链接继续学习。