「二分查找/双指针/数学推导」---- 对「两数之和 II」的思考

301 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 6 天,点击查看活动详情

背景

笔者今天刷了一道力扣题 两数之和 II - 输入有序数组,借着昨天输出 二分法 的印象,一下子写出了一个时间复杂度 O(NlogN) 的解法,这比 O(N2) 强,笔者以为这就是最优解了。

然而,点开官方题解一看,竟然有 O(N) 的解法!

本着吃透技术的理念,笔者详细研读了这个解法,消化以后,觉得其中的数学推导非常精妙,必须沉淀、分享出来,于是有了本文。

题目

这道题研究的是:给你一个有序数组 nums,再给你一个数字 targetnums 中有且只有一对下标 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 个指针来做,双指针的解法有快慢指针首尾指针,一比较显然快慢指针没法一趟遍历完成,所以只能尝试首尾指针:

  1. 初始时 L 在数组头,R 在数组尾。
  2. 开始检查:
    • 如果 nums[L] + nums[R] 等于 target,那么返回 [L,R]
    • 如果 nums[L] + nums[R] 大于 targetR--
    • 如果 nums[L] + nums[R] 小于 targetL++
  3. LR 撞上时,停,找不到 c1,c2

只有这么做才能做到 O(N),因为两个指针都不回退,等于一趟遍历就完成。

编写代码提交,通过了。但是为什么呢?

为什么呢?

数学推导

c1,c2 是有且只有一对的,L,R面对面推进的过程中,有 3 种情况:

  1. L,R 刚好就是 c1,c2,直接返回;
  2. L,R 都没碰到 c1,c2,由于只有唯一的答案,所以 L,R 还要继续推进;
  3. L 先到达 c1,此时 R 还在 c2 右边:nums[L]+nums[R] === nums[c1]+nums[R] > nums[c1]+nums[c2] === target,两数之和还要减少,所以 R 必须 --
  4. R 先到达 c2,此时 L 还在 c1 左边:nums[L]+nums[R] === nums[L]+nums[c2] < nums[c1]+nums[c2] === target,两数之和还要增加,所以 L 必须 ++

下面这张图可以帮助理解:

do1.jpeg

在这整个推进过程中,答案 (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];
};