当前:LC167 · 两数之和 II · 输入有序数组 · 首次出现于 Day 4 · 路径:顶栏「56天打卡」→ 点击 LC 题号 → 逐题动画

LC167 · Two Sum II · 双指针

两数之和 II:有序数组从两端夹

最小加最大。和太大,右边那个数对左边所有剩余数都偏大,右指针左移;和太小则左指针右移。

左指针停在当前最小,右指针停在当前最大。和等于 target 就返回 1 起始下标;大于 target 则右指针左移,小于则左指针右移。有序保证每步淘汰一整行或一整列,相遇前必能命中(题目保证有解)。时间 O(n),空间 O(1)。

时间 O(n)空间 O(1)结论先行 · 全文约 6 节
导读

这是 LeetCode 167. Two Sum II - Input Array Is Sorted。大白话:给你已经按非递减排好的整数数组 numbers,再给一个 target,找出两个不同下标使得两数之和等于 target,返回这两个下标。下标从 1 开始。题目保证恰好有一组解,同一元素不能用两次。

主例 numbers = [2, 7, 11, 15],target = 9。2 + 7 = 9,返回 [1, 2]。若 target 改成 18,则 7 + 11 = 18,返回 [2, 3]。

无序的两数之和用哈希记补数,空间 O(n)。这里数组已经有序,缺的不是「补数在不在」,而是「下一步该丢掉哪一端」。最大加最小,和的大小直接告诉你该收右还是扩左,空间回到 O(1)。

两端夹逼:和的大小决定动哪边

第一反应仍是哈希:扫到 x 就查 target−x 见过没有。能做对,但浪费了「已经排好序」这件事,还多占一张表。另一条错路是对每个左端二分右端,时间 O(n log n),正确但慢。有序真正给的是:任意时刻,左端是剩下数里最小的,右端是最大的,它们的和与 target 一比,淘汰方向唯一。

记 i 在最左、j 在最右,s = numbers[i] + numbers[j]。s 等于 target,返回 [i+1, j+1],下标要加一。s 大于 target:j 已经是当前最大,它和 i 以及 i 左边那些更小的数相加只会更大,这一列全废,j 减一。s 小于 target:i 已经是当前最小,它和 j 以及 j 右边那些更大的数相加只会更小,这一行全废,i 加一。每步丢掉一个下标,指针相向,最多 n−1 次比较。

用手走主例。i=0、j=3,2+15=17,大于 9。15 配 2、7、11 都会超过 9,丢掉 15,j 移到 2。演示第一帧就是 17。下一拍 2+11=13,仍大于 9,11 配 2 或 7 也偏大,丢掉 11,j 移到 1。演示第二帧是 13。再一拍 2+7=9,命中,返回 [1, 2]。演示第三帧 hit。主例三次比较都走「太大收右」,没有走到「太小扩左」;规则对称,和偏小时当前左端对右边所有剩余数都不够,整行淘汰。

单调有序让「下一步动哪边」只剩一个答案。无序时 15 偏大并不能推出「15 和谁都不行」,因为左边可能还有负数。本题非递减,这个推出成立。
两端夹逼:和太大收右边,和太小扩左边
2
1
7
2
11
3
15
4
nums[1] + nums[4] = 17 > 9 → 收右边

为什么丢掉一端不会漏掉那组解

把每个有序对 (i, j)、i < j 看成矩阵上的一格,行随 i 增大而增大,列随 j 增大而增大。目标格子一定在这条对角线上的某处。每次只看当前左上角那一格 numbers[i]+numbers[j]。

主例第一格是 2+15=17>9。第 15 这一列上,2、7、11 配 15 都 ≥ 17 > 9,整列没有解,j 左移不会漏。第二格 2+11=13>9,第 11 这一列同样全超,再收。第三格正好 9。若某一步和小于 target,当前行上右边那些格子只会更大但仍可能不够,更准确的说法是:当前 i 配上所有 ≥ i 且 ≤ j 的右端都偏小,整行没有解,i 右移才安全。

每步删一行或一列,搜索区域是 shrinking 的右上角矩形,面积每次至少减 1,O(n) 内矩形空掉。题目保证有解,指针相遇之前必踩到那一格。返回值记得加一:主例命中的是下标 0 与 1,题目要 [1, 2]。不要用哈希表的 0 起始下标去对这道题的样例。

七行 Go:两端夹逼

solution.goGo
func twoSum(numbers []int, target int) []int {
i, j := 0, len(numbers)-1
for i < j {
s := numbers[i] + numbers[j]
if s == target { return []int{i + 1, j + 1} }
if s > target { j-- } else { i++ }
}
return nil
}

1i、j 钉在两端。主例从 2 与 15 开始,不是从 2 与 7 开始。

2s 只算当前两端。相等立刻返回,下标加一。主例第三拍 2+7=9,返回 [1,2]。

3s > target 只动 j。主例 17、13 两拍都走这一支,15 和 11 被整列扔掉。

4否则动 i。循环条件 i < j,同一元素不会用两次。题目有解,nil 只是防编译器。

总结

有序则两端夹:太大整列丢掉右边,太小整行丢掉左边。主例 2+15→2+11→2+7。

  • 哈希能做,但忽略了有序,还多占 O(n) 空间。缺口是「和的大小直接指定淘汰方向」。
  • 主例三次比较都收右,答案下标是 [1,2] 不是 [0,1]。
  • 每步淘汰一行或一列,不会漏掉那组解;无序时这个推出不成立。
同族题目
LC1两数之和(无序用哈希)LC15三数之和LC11盛最多水的容器