目录

题目描述

LCR 006. 两数之和 II - 输入有序数组

题意分析

给一个非递减排序的整数数组 numbers 和一个目标值 target,找出两个数使它们相加等于 target,返回这两个数的下标(从 $0$ 开始)。题目保证有且仅有一组答案,且同一个元素不能重复使用。

「有且仅有一组答案」这句保证很关键:它意味着搜索过程一旦命中就可以立刻返回,不需要继续找、也不需要处理「找不到」的分支;同时也保证了外层循环一定会在越界前终止。

「已排序」是全部算法信号的来源。数组无序时(对应 $1$ 题)只能靠哈希表把查找降到 $O(1)$;而一旦有序,就可以用「有序 + 目标值」的两大经典手段:一是二分查找定位配对值,二是相向双指针靠单调性收缩。本题的进阶要求还常常追加「只使用常量级额外空间」,这会把哈希表排除掉。

边界方面:元素可以是负数、可以有重复值,所以不能靠「值唯一」做任何假设;target 可能是任意整数,两数之和在极端数据下可能超出 32 位(本题范围内不会,但用减法 target - numbers[i] 而不是加法比较,本身就规避了这个风险)。另外「同一元素不可复用」要求配对的第二个下标必须严格大于第一个。

解法:二分查找判定答案

核心思路

暴力做法是双层循环枚举所有下标对,$O(n^2)$。瓶颈在内层:对固定的 i,我们其实是在问「数组里有没有值等于 target - numbers[i]」,而这个问题被用线性扫描回答了——完全浪费了数组已经有序这个条件。

观察:有序数组里的「查找某个值是否存在」正是二分查找的标准场景,可以从 $O(n)$ 降到 $O(\log n)$。于是固定第一个下标 i,令 $x = target - numbers[i]$,在区间 $[i+1, n-1]$ 内二分查找 $x$。搜索区间从 i + 1 而不是 $0$ 开始,一举解决了两件事:不会把同一个元素用两次,也不会把 (i, j)(j, i) 重复枚举。

二分部分用的是「找第一个大于等于 $x$ 的位置」这个下界写法,维持的不变量是:答案若存在,必定落在闭区间 $[l, r]$ 内;r 始终是一个「值大于等于 $x$」的候选或右端哨兵,l 左侧的元素全部严格小于 $x$。循环条件写 l < r,每轮把区间至少砍掉一半,退出时 l == r,这个位置就是唯一的候选;再单独比一次 numbers[l] == x 才能确认 $x$ 真的存在,因为下界位置上的值可能大于 $x$。

外层循环写成无终止条件的 for (int i = 0; ; ++i),靠题目「必定有解」的保证退出。这在竞赛/面试里是可以接受的写法,但要能说清依据;如果题目不保证有解,就必须补上 i < n - 1 的边界。

解题步骤

  • 外层枚举第一个下标 i,从 $0$ 递增。因为答案唯一,第一次命中即为答案,可以直接 return
  • 算出配对值 x = target - numbers[i]。用减法而不是「枚举 j 再判断和是否等于 target」,是为了把问题变成一次纯粹的「查值」,从而能接上二分。
  • 二分区间取 [i + 1, n - 1]。左端从 i + 1 开始保证不复用同一元素;同时也让整体枚举不重不漏。
  • 循环条件 l < r,中点 mid = (l + r) >> 1。这里 lr 都是合法下标,和不超过 $2n$,不会溢出;用右移而非除法只是习惯,语义相同。
  • numbers[mid] >= xr = mid,否则 l = mid + 1。收缩方式必须和「区间闭合」匹配:mid 满足条件时它自己仍可能是答案,所以 r 收到 mid(不能是 mid - 1);不满足时 mid 一定不是答案,l 才能跳过它到 mid + 1。这一对写法保证区间每轮严格变小,不会死循环。
  • 退出后必须再判一次 numbers[l] == x。下界查找只保证「l 是第一个大于等于 $x$ 的位置」,并不保证等于;漏掉这一步会把「最接近但不相等」的元素当成答案。
  • 命中就返回 {i, l};否则外层 i 前进一格,继续下一轮。

numbers = [1, 2, 4, 6, 10]target = 8 走一遍,期望答案 [1, 3]i = 0x = 8 - 1 = 7,在 $[1, 4]$ 上二分——mid = 2numbers[2] = 4 < 7l = 3mid = 3numbers[3] = 6 < 7l = 4;此时 l == r == 4 退出,numbers[4] = 10 ≠ 7,未命中。i = 1x = 8 - 2 = 6,在 $[2, 4]$ 上二分——mid = 3numbers[3] = 6 >= 6r = 3mid = 2numbers[2] = 4 < 6l = 3l == r == 3 退出,numbers[3] = 6 == 6,命中,返回 [1, 3]。注意 i = 0 那一轮里 l 最终停在了值为 $10$ 的位置——它确实是第一个「大于等于 $7$」的元素,但并不等于 $7$,正是那句额外的相等判断把它挡了下来。

代码实现

class Solution {
    public int[] twoSum(int[] numbers, int target) {
        // 题目保证必有一组解,所以外层不需要终止条件。
        for (int i = 0, n = numbers.length;; ++i) {
            int x = target - numbers[i];
            // 在 [i + 1, n - 1] 上找第一个大于等于 x 的位置,区间左端保证不复用同一元素。
            int l = i + 1, r = n - 1;
            while (l < r) {
                int mid = (l + r) >> 1;
                if (numbers[mid] >= x) {
                    r = mid;
                } else {
                    l = mid + 1;
                }
            }
            // 下界只保证「大于等于」,必须再确认一次相等。
            if (numbers[l] == x) {
                return new int[] {i, l};
            }
        }
    }
}
func twoSum(numbers []int, target int) []int {
    for i, n := 0, len(numbers); ; i++ {
        x := target - numbers[i]
        l, r := i+1, n-1
        for l < r {
            mid := (l + r) >> 1
            if numbers[mid] >= x {
                r = mid
            } else {
                l = mid + 1
            }
        }
        if numbers[l] == x {
            return []int{i, l}
        }
    }
}

复杂度分析

  • 时间复杂度:$O(n \log n)$。外层最多枚举 $n$ 个起点,每次在长度不超过 $n$ 的有序区间上二分,代价 $O(\log n)$。凭的是有序性让「查找配对值」从线性扫描降到了折半。
  • 空间复杂度:$O(1)$。只用了 ixlrmid 几个下标变量,没有哈希表也没有额外数组,满足常量级空间的进阶要求。

关键点总结

  • 「有序数组 + 找定值」是二分查找的标准触发条件;把「找一对数之和等于 target」改写成「对每个 numbers[i]target - numbers[i]」,是把配对问题降维成查找问题的通用手法。
  • 二分的收缩方式必须和区间语义严格配套:判定成立时 r = mid(保留候选),不成立时 l = mid + 1(跳过),配 l < r 的循环条件,退出时区间恰好收成一个点。改动其中任何一处都要同步改其余两处。
  • 下界二分返回的是位置而不是答案,用完必须再验一次相等,这是 lower_bound 类写法最容易漏的收尾。
  • 搜索区间从 i + 1 而非 $0$ 起步,同时解决了「元素不可复用」和「配对不重复枚举」,比事后加 if (j != i) 判断更干净。
  • 面试视角:这道题面试官期待的最优解是相向双指针——l 从头、r 从尾,和偏小就 l++、偏大就 r--,$O(n)$ 时间 $O(1)$ 空间,且能一句话说清正确性(当前和偏小时,l 与任何更靠左的 r 配对只会更小,所以 l 位置的所有配对都可以整体排除)。二分版本是很好的过渡答案,它证明你能利用有序性,但被追问「还能更快吗」时必须立刻给出双指针,并解释为什么每次能安全地排除一整行/一整列。

易错点总结

  • 错误写法:二分区间取 [0, n - 1]。输入 numbers = [3, 3], target = 6i = 0 会找到 l = 0,返回 [0, 0],同一个元素被用了两次。
  • 错误写法:判定成立时写 r = mid - 1。输入 numbers = [1, 2, 4, 6, 10], target = 8i = 1 的二分会把 numbers[3] = 6 这个正确候选跳过,最终漏解并让外层一路 i++ 越界。
  • 错误写法:判定不成立时写 l = mid。当 lr 相邻时 mid 恒等于 l,区间不再变小,直接死循环。
  • 错误写法:循环条件写 l <= r。退出时 l 会变成 r + 1,可能等于 n,紧接着的 numbers[l] 直接越界。
  • 错误写法:省掉退出后的 numbers[l] == x 判断,直接返回 {i, l}。输入 numbers = [1, 2, 4, 6, 10], target = 8i = 0 轮会返回 [0, 4]($1 + 10 = 11$),完全错误。
  • 错误写法:把配对值算成 numbers[i] - target。输入 numbers = [1, 2, 4, 6, 10], target = 8i = 0 要找 $-7$,永远找不到,外层递增到越界后抛异常。
  • 错误写法:外层不写终止条件却又用在「不保证有解」的变体题上。此题的无条件 for 依赖题目保证;一旦题面改成「可能无解」,i 会一路加到 n 并在 numbers[i] 处越界,必须补 i < n - 1 并在结尾返回空数组。
  • 错误写法:返回 {i + 1, l + 1}。这是 167 题的 1-based 约定,本题要求 0-based,直接答案错位。

相似题目

题目 难度 考察点
167. 两数之和 II - 输入有序数组 中等 与本题同题,但返回值是 1-based 下标,套用时要整体加一
剑指 Offer 57. 和为s的两个数字 简单 返回的是两个数值而非下标,双指针可直接输出元素
1099. 小于 K 的两数之和 简单 目标从「等于」放宽成「小于」,需在收缩过程中持续记录最优和
15. 三数之和 中等 多一层枚举 + 排序,还要在三个位置上分别跳过重复值以去重
LCR 007. 三数之和 中等 与 15 同题,是本题「固定一个数 + 有序配对」思路的直接升级
16. 最接近的三数之和 中等 找不到精确解,改为在移动指针时维护与目标的最小差值
611. 有效三角形的个数 中等 判定条件变成不等式,命中时可一次性统计一整段区间的贡献
面试题 16.24. 数对和 中等 输入无序,需先排序再双指针,且要收集全部答案而非第一组