开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 6 天,点击查看活动详情
背景
笔者今天刷了一道力扣题 两数之和 II - 输入有序数组,借着昨天输出 二分法 的印象,一下子写出了一个时间复杂度 O(NlogN) 的解法,这比 O(N2) 强,笔者以为这就是最优解了。
然而,点开官方题解一看,竟然有 O(N) 的解法!
本着吃透技术的理念,笔者详细研读了这个解法,消化以后,觉得其中的数学推导非常精妙,必须沉淀、分享出来,于是有了本文。
题目
这道题研究的是:给你一个有序数组 nums,再给你一个数字 target,nums 中有且只有一对下标 c1,c2,满足 nums[c1] + nums[c2] 等于 target,请你找出 c1,c2 并返回。
注意:题目的 nums 的下标是从 1 开始的。
二分查找
笔者首先想到的是二分查找:
从左往右遍历 nums,到达一个下标i时,得到候选数字nums[i],然后计算出它的目标 x = target - nums[i],只要通过下标找到x,就找到了c1,c2。
由于是从左往右遍历,所以 i 之前的数字已经找过了,没有返回就是答案不在其中,不用再考虑,那么只需要在[i,nums.length-1] 范围内查找x。
整个数组是有序的,那么 [i,nums.length-1] 范围也就是有序的,所以可以使用 二分法 查找目标,这个过程是时间复杂度是 O(logN)。
再考虑上遍历数组的时间复杂度 O(N),每到达一个数组就执行一次二分法,所以本解法的时间复杂度是 O(NlogN)。
最优解
我们的最终目的,就是找到两个下标c1,c2,并且这 2 个下标是 1 <= c1 <= c2 <= nums.length 的。
上面的解法已经是 O(NlogN) 了,还要更优,那就是 O(N),找2个下标这件事很自然能想到用 2 个指针来做,双指针的解法有快慢指针和首尾指针,一比较显然快慢指针没法一趟遍历完成,所以只能尝试首尾指针:
- 初始时
L在数组头,R在数组尾。 - 开始检查:
- 如果
nums[L] + nums[R]等于target,那么返回[L,R]。 - 如果
nums[L] + nums[R]大于target,R--。 - 如果
nums[L] + nums[R]小于target,L++。
- 如果
- 当
L和R撞上时,停,找不到c1,c2。
只有这么做才能做到 O(N),因为两个指针都不回退,等于一趟遍历就完成。
编写代码提交,通过了。但是为什么呢?
为什么呢?
数学推导
c1,c2 是有且只有一对的,L,R 在面对面推进的过程中,有 3 种情况:
L,R刚好就是c1,c2,直接返回;L,R都没碰到c1,c2,由于只有唯一的答案,所以L,R还要继续推进;L先到达c1,此时R还在c2右边:nums[L]+nums[R] === nums[c1]+nums[R] > nums[c1]+nums[c2] === target,两数之和还要减少,所以R必须--。R先到达c2,此时L还在c1左边:nums[L]+nums[R] === nums[L]+nums[c2] < nums[c1]+nums[c2] === target,两数之和还要增加,所以L必须++。
下面这张图可以帮助理解:
在这整个推进过程中,答案 (c1,c2) 一直保持在 L,R 之间,不会被错过,所以这个解法是对的✅。
文末
最后,针对最优解笔者输出了代码,希望对掘友们有所帮助🍺,大家也可以到力扣上查看 本题题解,获取更多内容。
javascript
function twoSum(numbers, target) {
let L = 0;
let R = numbers.length - 1;
while (L < R) {
const sum = numbers[L] + numbers[R];
if (sum === target) {
return [L+1, R+1];
}
else if (sum > target) {
R--;
}
else {
L++;
}
}
return [-1, -1];
};