力扣解题-167. 两数之和 II - 输入有序数组
给你一个下标从 1 开始的整数数组 numbers ,该数组已按 非递减顺序排列 ,请你从数组中找出满足相加之和等于目标数 target 的两个数。如果设这两个数分别是 numbers[index1] 和 numbers[index2] ,则 1 <= index1 < index2 <= numbers.length 。
以长度为 2 的整数数组 [index1, index2] 的形式返回这两个整数的下标 index1 和 index2。
你可以假设每个输入 只对应唯一的答案 ,而且你 不可以 重复使用相同的元素。
你所设计的解决方案必须只使用常量级的额外空间。
示例 1:
输入:numbers = [2,7,11,15], target = 9
输出:[1,2]
解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。
示例 2:
输入:numbers = [2,3,4], target = 6
输出:[1,3]
解释:2 与 4 之和等于目标数 6 。因此 index1 = 1, index2 = 3 。返回 [1, 3] 。
示例 3:
输入:numbers = [-1,0], target = -1
输出:[1,2]
解释:-1 与 0 之和等于目标数 -1 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。
提示:
2 <= numbers.length <= 3 * 104
-1000 <= numbers[i] <= 1000
numbers 按 非递减顺序 排列
-1000 <= target <= 1000
仅存在一个有效答案
Related Topics
数组、双指针、二分查找
第一次解答(超时)
解题思路
核心方法:暴力双指针遍历(低效版),采用“左指针固定,右指针从左指针下一位开始逐个右移”的暴力遍历方式,未利用数组“非递减排列”的核心特性,导致时间复杂度极高,最终超时。
核心逻辑拆解
该解法的思路是“穷举所有可能的两数组合”:
- 左指针
left从0开始,右指针right初始为left+1; - 计算两数之和
sum:- 若
sum == target,返回[left+1, right+1](下标从1开始); - 若
sum > target,左指针右移,右指针重置为left+1; - 若
sum < target,右指针右移,若右指针越界则左指针右移、右指针重置;
- 若
- 遍历所有组合直到找到答案。
超时原因分析
- 时间复杂度:O(n²)(最坏情况需遍历n(n-1)/2个组合,n≤3×10⁴时,总操作数约4.5×10⁸,远超时间限制);
- 核心问题:未利用数组“有序”的特性,暴力遍历所有组合,完全浪费了题目给出的关键条件;
- 逻辑缺陷:当
sum > target时仅移动左指针,忽略了“有序数组中右指针左移也能减小和”的关键逻辑,导致遍历路径冗余。
public int[] twoSum(int[] numbers, int target) {
int left=0;int right=1;
while(left<right && left<numbers.length){
int sum=numbers[left]+numbers[right];
if(sum==target){
return new int[]{left+1,right+1};
}
if(sum>target){
left++;
right=left+1;
}
if(sum<target){
right++;
if(right>=numbers.length){
left++;
right=left+1;
}
}
}
return new int[]{};
}
第二次解答
解题思路
核心方法:首尾双指针法(最优解),利用数组“非递减排列”的特性,从首尾两端向中间逼近目标和,时间复杂度O(n)、空间复杂度O(1),完全满足题目“常量级额外空间”的要求,是本题的最优解法。
核心逻辑拆解(通俗版)
有序数组的核心特性是“左边小、右边大”,因此可以通过调整首尾指针的位置,精准控制两数之和的大小:
- 左指针
left初始为0(数组最左端,最小值),右指针right初始为numbers.length-1(数组最右端,最大值); - 计算两数之和
sum:- 若
sum == target:找到唯一答案,返回[left+1, right+1](下标从1开始); - 若
sum < target:当前和偏小,需要增大和 → 左指针右移(取更大的数); - 若
sum > target:当前和偏大,需要减小和 → 右指针左移(取更小的数);
- 若
- 循环直到
left < right(保证两个指针不重合、不重复使用元素),题目保证有唯一解,因此循环内必能找到答案。
具体步骤(以示例1 numbers=[2,7,11,15],target=9为例)
- 初始:left=0(2),right=3(15)→ sum=17 > 9 → right左移至2(11);
- sum=2+11=13 >9 → right左移至1(7);
- sum=2+7=9 ==9 → 返回[0+1,1+1]=[1,2]。
性能优势
- 时间复杂度:O(n)(最多遍历数组一次,每个元素仅被访问一次),耗时1ms击败99.89%用户;
- 空间复杂度:O(1)(仅使用两个指针变量,无额外数组/哈希表开销);
- 逻辑优势:
- 利用有序特性避免暴力遍历,效率呈指数级提升;
- 严格满足题目“常量级额外空间”的要求;
- 题目保证唯一解,无需处理多解或无解的边界情况。
public int[] twoSum(int[] numbers, int target) {
int left=0;
int right=numbers.length-1;
while (left<right){
int sum=numbers[left]+numbers[right];
if(sum==target){
return new int[]{left+1,right+1};
}else if(sum<target){
left++;
}else {
right--;
}
}
return new int[]{};
}
示例解答
解题思路
解法1:二分查找法(备选高效解)
核心方法:固定单值 + 二分查找补值,遍历数组中的每个元素numbers[i],计算需要匹配的补值complement = target - numbers[i],然后在i+1到数组末尾的范围内二分查找该补值,时间复杂度O(n×logn)、空间复杂度O(1),效率略低于首尾双指针,但也是符合题目要求的高效解法。
核心逻辑拆解
- 遍历数组中的每个下标
i(0 ≤ i < numbers.length-1); - 计算补值
complement = target - numbers[i]; - 在
[i+1, numbers.length-1]范围内二分查找complement:- 若找到,返回
[i+1, mid+1](mid为补值的下标); - 若未找到,继续遍历下一个
i;
- 若找到,返回
- 题目保证有唯一解,因此必能找到。
代码实现
public int[] twoSum(int[] numbers, int target) {
int n = numbers.length;
for (int i = 0; i < n; i++) {
int complement = target - numbers[i];
// 二分查找范围:i+1 到 n-1
int left = i + 1, right = n - 1;
while (left <= right) {
int mid = left + (right - left) / 2; // 避免溢出
if (numbers[mid] == complement) {
return new int[]{i + 1, mid + 1};
} else if (numbers[mid] < complement) {
left = mid + 1;
} else {
right = mid - 1;
}
}
}
return new int[]{};
}
优势与适用场景
- 时间复杂度:O(n×logn)(遍历O(n) + 每次二分O(logn)),效率低于双指针但远高于暴力法;
- 适用场景:若题目未明确数组有序,但允许使用O(1)空间,可先排序再用双指针,或直接用此方法;
- 逻辑价值:二分查找是有序数组的经典操作,该解法可拓展到“多值匹配”“范围查找”等场景。
总结
- 暴力双指针法(第一次解答):未利用有序特性,O(n²)时间复杂度导致超时,仅适合理解基础思路;
- 首尾双指针法(第二次解答):最优解,O(n)时间+O(1)空间,完全贴合题目要求,是面试/工程首选;
- 二分查找法(备选解):O(n×logn)时间+O(1)空间,效率略低但逻辑通用,适合拓展学习;
- 关键技巧:
- 有序数组的两数之和问题,优先使用首尾双指针法,利用“左小右大”特性精准调整和的大小;
- 避免暴力遍历,充分利用题目给出的“有序”“唯一解”等关键条件;
- 下标从1开始的要求:返回结果时需将指针下标+1,是本题的易出错点。