【LeetCode选讲·第八期】「三数之和」「最接近的三数之和」「四数之和」

212 阅读6分钟

T15 三数之和

题目链接:leetcode-cn.com/problems/3s…

基本思路

为了便于入手,我们可以先不考虑题目中"答案中不可以包含重复的三元组"的要求,先只考虑如何在一个乱序的数组中找出3个和为0的数

首先,既然我们已经知道传入的数组nums有可能是乱序的,那么为了方便处理,我们不妨先对TA进行升序排序。这里我们使用Array.prototype.sort即可。

JavaScript中原生的数组排序方法的原理为先将数组元素转换为「字符串」,再按照字符串的「字典序」进行排序。因为本题中出现了负数,所以不能直接调用原生方法,必须传入判别函数,即nums.sort((a, b) => a - b).

在处理有序数组的问题中,我们常用的方法便是「双指针」,而本题中需要我们寻找3个数,因此我们不妨设定3个指针进行解答。

基本思路如下:

  • 设指针ijk分别代表三个需要寻找的数,设数组nums最后一个元素下标为n
  • 使用指针inums进行遍历以确定第一个数. 同时让指针jk分别从i + 1n开始向中间移动,以扫描整个数组,避免漏解.
  • sum = nums[i] + nums[j] + nums[k].
  • sum = 0,则成功找到一组答案. 并且让ij继续向中间移动,以尝试寻找其他答案;
  • sum < 0,则说明三数之和偏小,保持j不动,向右移动i
  • sum > 0,则说明三数之和偏大,保持i不动,向左移动j

代码如下:

function threeSum(nums) {
    const ans = [];
    const n = nums.length - 1;
    nums.sort((a, b) => a - b);
    for(let i = 0; i <= n - 2; i++) {
        let j = i + 1;
        let k = n;
        let x = nums[i];
        while(j < k) {
            let [y, z] = [nums[j], nums[k]];
            let sum = x + y + z;
            if (sum === 0) {
                ans.push([x, y, z]);
                j++;
                k--;
            } else if (sum < 0) {
                j++;
            } else if (sum > 0) {
                k--;
            }
        }
    }
    return ans;
}

处理重复

下面我们来实现答案中不可以包含重复的三元组的要求,这里提供两种方法。

朴素方法

由于我们事先已经对nums数组进行升序排序,因而不难推知我们获取到每组三元组一定是按照nums[i] < nums[j] < nums[k]的固定顺序排列的。

所以一种很容易想到的朴素方法便是,我们可以直接按顺序记录每一组为正确答案的三元组。当下一次出现sums = 0的情况时,检查新求得的有序三元组是否与我们已记录的有序三元组重复即可。

代码如下:

function threeSum(nums) {
    const ans = [];
    const n = nums.length - 1;
    const sets = new Set();
    nums.sort((a, b) => a - b);
    for(let i = 0; i <= n - 2; i++) {
        let j = i + 1;
        let k = n;
        let x = nums[i];
        while(j < k) {
            let [y, z] = [nums[j], nums[k]];
            let sum = x + y + z;
            if (sum === 0) {
                !sets.has('' + x + y + z) && (ans.push([x, y, z]), sets.add('' + x + y + z));
                j++;
                k--;
            } else if (sum < 0) {
                j++;
            } else if (sum > 0) {
                k--;
            }
        }
    }
    return ans;
}

这种方法虽然很简单,但其缺点也很明显。

首先,我们不得不创建一个额外的Set对象(具体实现中也有可能是其他数据结构)供我们记录已经出现过的三元组,造成了额外的空间开销。

其次我们必须在每一次sums = 0的时候才能对三元组进行记录和校验,难以在重复解出现的第一时间进行发现和纠正,造成了时间上的浪费。

有没有更加理想的方法?

调节指针

想要找到一个更加理想的方法,我们需要对这个问题的本质有更加深刻的理解:为什么会出现「重复解」?

为了更方便地理解这个问题,我们这里举一个具体的例子:nums = [-1, -1, 0, 1](假定数组已完成排序)。

i = 0j = 2k = 3时,出现了sum = 0,我们成功求出了第一个三元组[-1, 0, 1]

尔后i = 1,另外两个指针的初值分别为j = 2k = 3。很不巧的是,这时又出现了sum = 0,而因为nums[0] = nums[1] = 1,此时的三元组恰恰又是[-1, 0, 1]!重复解出现了!

现在我们势必已经清楚「重复解」出现的原因了,即排序后的数组nums中存在连续的重复数字,导致指针前后两次获取到的数字可能是一样的

我们可以直接对数组进行「去重」吗?答案是否定的。例如我们令nums = [-4, -1, -1, 0, 1, 2],那么由题意ans = [[-1,-1,2],[-1,0,1]]因为在同一个三元组中可以出现重复的数字,所以我们不能够企图通过去重来解决问题。

OK,现在请你再看一遍我们刚才分析的出现重复解的原因,相信你的心里已经有一个正确的解决方案了!

是的,只需要我们进行检测每一次指针移动后,它们新指向的值与前一次指向的值是否重复就可以了。如果发现重复,再次「调节」该指针的位置即可。

此外根据方程的数学原理,很容易证明:我们只需要通过调节指针ij,保证nums[i] ≠ nums[i - 1]nums[j] ≠ nums[j - 1],就能保证不出现重复解,而不必再去关心nums[k]nums[k + 1]是否重复。

代码如下:

function threeSum(nums) {
    const ans = [];
    const n = nums.length - 1;
    nums.sort((a, b) => a - b);
    for(let i = 0; i <= n - 2; i++) {
        //注意必须保证i避免越界
        if(i >= 1 && nums[i] === nums[i - 1]) continue;
        let j = i + 1;
        let k = n;
        let x = nums[i];
        while(j < k) {
            //注意必须保证j避免越界
            while (nums[j] === nums[j - 1] && j < n && j > i + 1) j++;
            //如果j和k错位,表面在当前的nums[i]下不可能再有解,故直接放弃当前循环
            if (j >= k) break;
            let [y, z] = [nums[j], nums[k]];
            let sum = x + y + z;
            if (sum === 0) {
                ans.push([x, y, z]);
                //别忘了继续让j和k向中间靠拢,不然就死循环了
                j++, k--;
            } else if (sum < 0) {
                j++;
            } else if (sum > 0) {
                k--;
            }
        }
    }
    return ans;
}

编码过程中的要点已通过代码注释进行说明,故本文不再赘述!

通过「指针调节」的方法,我们避免了额外的空间开销和不必要的时间浪费,实现了程序性能的优化。

T16 最接近的三数之和

题目链接:leetcode-cn.com/problems/3s…

这道题与上一道T15的解法非常类似,我们只需将其的代码稍作改造即可。

改造后的代码如下:

function threeSumClosest(nums, target) {
    const n = nums.length - 1;
    let delta = Infinity;
    let ans = null;
    nums.sort((a, b) => a - b);
    for(let i = 0; i <= n - 2; i++) {
        if(i >= 1 && nums[i] === nums[i - 1]) continue;
        let j = i + 1;
        let k = n;
        let x = nums[i];
        while(j < k) {
            while (nums[j] === nums[j - 1] && j < n && j > i + 1) j++;
            if (j >= k) break;
            let [y, z] = [nums[j], nums[k]];
            let sum = x + y + z;
            if (sum === target) {
                return sum;
            } else if (sum < target) {
                j++;
            } else if (sum > target) {
                k--;
            }
            if (Math.abs(sum - target) < delta) {
                delta = Math.abs(sum - target);
                ans = sum;
            }
        }
    }
    return ans;
}

T17 四数之和

题目链接:leetcode-cn.com/problems/4s…

这道题是「三数之和」的推广,解题的关键在于我们需要再增加一个变量来控制最外层的区间,之后外层区间内我们就可以移植「三数之和」的代码了。

代码如下:

function fourSum(nums, target) {
    const ans = [];
    const n = nums.length - 1;
    nums.sort((a, b) => a - b);
    for(let i = 0; i <= n - 3; i++) {
        if(i >= 1 && nums[i] === nums[i - 1]) continue;
        for(let j = i + 1; j <= n - 2; j++) {
            //由于外层区间的范围是在不断变化的,
            //所以j开始微调的条件不是j >= 2,而是j >= i + 2.
            if(j >= i + 2 && nums[j] === nums[j - 1]) continue;
            let k = j + 1;
            let l = n;
            while(k < l) {
                while (nums[k] === nums[k - 1] && k < n && k > j + 1) k++;
                if (k >= l) break;
                let [a, b, c, d] = [ nums[i], nums[j], nums[k], nums[l] ];
                let sum = a + b + c + d;
                if (sum === target) {
                    ans.push([a, b, c, d]);
                    k++, l--;
                } else if (sum < target) {
                    k++;
                } else if (sum > target) {
                    l--;
                }
            }
        }
    }
    return ans;
}

写在文末

我是来自在校学生编程兴趣小组江南游戏开发社的PAK向日葵,我们目前正在致力于开发自研的非营利性网页端同人游戏《植物大战僵尸:旅行》,以锻炼我们的前端应用开发能力。

我们诚挚邀请您体验我们的这款优秀作品,如果您喜欢TA的话,欢迎向您的同事和朋友推荐。如果您有技术方面的问题希望与我们探讨,欢迎直接与我联系。您的支持是我们最大的动力!

QQ图片20220701165008.png