🚩传送门:牛客题目
力扣题目

题目

给定一个 升序排列 的整数数组 numbers,请你从数组中找出两个数满足相加之和等于目标数 target。函数应该以长度为 2 的整数数组的形式返回这两个数的下标值。numbers 的下标 从 1 开始计数 ,所以答案数组应当满足 1 <= answer[0] < answer[1] <= numbers.length。

你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。

示例 1:

输入:numbers = [2,7,11,15], target = 9 输出:[1,2] 解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。

示例 2:

输入:numbers = [2,3,4], target = 6 输出:[1,3]

示例 3:

输入:numbers = [-1,0], target = -1 输出:[1,2]

📖 文字题解

这道题可以使用「1. 两数之和」的解法,使用 [NC]275. 两数之和 II/和为S的两个数字 - 图1 的时间复杂度和 [NC]275. 两数之和 II/和为S的两个数字 - 图2 的空间复杂度暴力求解,或者借助哈希表使用 [NC]275. 两数之和 II/和为S的两个数字 - 图3 的时间复杂度和 [NC]275. 两数之和 II/和为S的两个数字 - 图4 的空间复杂度求解。但是这两种解法都是针对 无序数组 的,没有利用到 输入数组有序 的性质。利用输入数组有序的性质,可以得到时间复杂度和空间复杂度更优的解法。

解题思路:二分查找

在数组中找到两个数,使得它们的和等于目标值,可以首先固定第一个数,然后寻找第二个数,第二个数等于目标值减去第一个数的差。

  1. 利用数组的有序性质,可以通过 二分查找 的方法寻找第二个数。
  2. 为了避免重复寻找,在寻找第二个数时,只在第一个数的 右侧 寻找。

复杂度分析

时间复杂度:[NC]275. 两数之和 II/和为S的两个数字 - 图5,其中 [NC]275. 两数之和 II/和为S的两个数字 - 图6 为数组长度。

  1. - 需要遍历数组一次确定第一个数,时间复杂度是 ![](https://cdn.nlark.com/yuque/__latex/bf7c2e3ac858e1c3496fd2f47a300139.svg#card=math&code=%5Csmall%20O%28n%29&id=BnkUv),寻找第二个数使用二分查找,时间复杂度是 ![](https://cdn.nlark.com/yuque/__latex/c2e2b55e8835427a2520eb3765ff8d28.svg#card=math&code=%5Csmall%20O%28%5Clog%20n%29&id=TvKW8),因此总时间复杂度是 ![](https://cdn.nlark.com/yuque/__latex/751398f04b5c304d746078df6396f301.svg#card=math&code=%20%5Csmall%20O%28n%20%5Clog%20n%29&id=Jequ0) 。

空间复杂度:[NC]275. 两数之和 II/和为S的两个数字 - 图7

官方代码

class Solution {
    public int[] twoSum(int[] numbers, int target) {
        for (int i = 0; i < numbers.length; ++i) {
            //二分查找
            int low = i + 1, high = numbers.length - 1;
            while (low <= high) {
                int mid = (high - low) / 2 + low;
                if (numbers[mid] == target - numbers[i]) {
                    return new int[]{i + 1, mid + 1};
                } else if (numbers[mid] > target - numbers[i]) {
                    high = mid - 1;
                } else {
                    low = mid + 1;
                }
            }
        }
        return new int[]{-1, -1};
    }
}

解题思路:滑动窗口 [ 双指针 ]

初始时两个指针分别指向第一个元素位置和最后一个元素的位置。每次计算两个指针指向的两个元素之和,并和目标值比较。如果两个元素之和等于目标值,则发现了唯一解。

  1. 如果两个元素之和小于目标值,则将左侧指针右移一位。
  2. 如果两个元素之和大于目标值,则将右侧指针左移一位。
  3. 移动指针之后,重复上述操作,直到找到答案。

使用双指针的实质是缩小查找范围。那么会不会把可能的解过滤掉?答案是不会

解释1:

我们知道一开始的时候,[NC]275. 两数之和 II/和为S的两个数字 - 图8[NC]275. 两数之和 II/和为S的两个数字 - 图9[NC]275. 两数之和 II/和为S的两个数字 - 图10区域最小值[NC]275. 两数之和 II/和为S的两个数字 - 图11区域最大值

  • 如果 [NC]275. 两数之和 II/和为S的两个数字 - 图12,一定是[NC]275. 两数之和 II/和为S的两个数字 - 图13左移,因为[NC]275. 两数之和 II/和为S的两个数字 - 图14加上 区域最小值 还比 [NC]275. 两数之和 II/和为S的两个数字 - 图15大,只能减小 [NC]275. 两数之和 II/和为S的两个数字 - 图16
  • 如果 [NC]275. 两数之和 II/和为S的两个数字 - 图17,一定是[NC]275. 两数之和 II/和为S的两个数字 - 图18右移, 因为[NC]275. 两数之和 II/和为S的两个数字 - 图19加上 区域最大值 还比 [NC]275. 两数之和 II/和为S的两个数字 - 图20小,只能增加 [NC]275. 两数之和 II/和为S的两个数字 - 图21

那么整体的区间会由[NC]275. 两数之和 II/和为S的两个数字 - 图22开始慢慢缩小,直至找到最终答案

解释2:

假设 [NC]275. 两数之和 II/和为S的两个数字 - 图23 是唯一解,其中 [NC]275. 两数之和 II/和为S的两个数字 - 图24。初始时两个指针分别指向下标 [NC]275. 两数之和 II/和为S的两个数字 - 图25 和下标 [NC]275. 两数之和 II/和为S的两个数字 - 图26,左指针指向的下标小于或等于 [NC]275. 两数之和 II/和为S的两个数字 - 图27,右指针指向的下标大于或等于 [NC]275. 两数之和 II/和为S的两个数字 - 图28。除非初始时左指针和右指针已经位于下标 [NC]275. 两数之和 II/和为S的两个数字 - 图29[NC]275. 两数之和 II/和为S的两个数字 - 图30,否则一定是左指针先到达下标 [NC]275. 两数之和 II/和为S的两个数字 - 图31 的位置或者右指针先到达下标 [NC]275. 两数之和 II/和为S的两个数字 - 图32的位置。

  1. 如果左指针先到达下标 [NC]275. 两数之和 II/和为S的两个数字 - 图33的位置,此时右指针还在下标 [NC]275. 两数之和 II/和为S的两个数字 - 图34的右侧,sum>target,一定是右指针左移,左指针不可能移到 [NC]275. 两数之和 II/和为S的两个数字 - 图35的右侧。
  2. 如果右指针先到达下标 [NC]275. 两数之和 II/和为S的两个数字 - 图36的位置,此时左指针还在下标 [NC]275. 两数之和 II/和为S的两个数字 - 图37的左侧,sum<target,一定是左指针右移,右指针不可能移到 [NC]275. 两数之和 II/和为S的两个数字 - 图38的左侧。

由此可见,在整个移动过程中,左指针不可能移到 [NC]275. 两数之和 II/和为S的两个数字 - 图39的右侧,右指针不可能移到 [NC]275. 两数之和 II/和为S的两个数字 - 图40的左侧,因此不会把可能的解过滤掉。由于题目确保有唯一的答案,因此使用双指针一定可以找到答案。

复杂度分析

时间复杂度:[NC]275. 两数之和 II/和为S的两个数字 - 图41,其中 [NC]275. 两数之和 II/和为S的两个数字 - 图42 为数组长度,两个指针移动的总次数最多为 [NC]275. 两数之和 II/和为S的两个数字 - 图43

空间复杂度:[NC]275. 两数之和 II/和为S的两个数字 - 图44

官方代码

class Solution {
    public int[] twoSum(int[] numbers, int target) {
        int low = 0, high = numbers.length - 1;
        while (low < high) {
            int sum = numbers[low] + numbers[high];
            if (sum == target) {
                return new int[]{low + 1, high + 1};
            } else if (sum < target) {
                ++low;
            } else {
                --high;
            }
        }
        return new int[]{-1, -1};
    }
}