[前端]_一起刷leetcode 373. 查找和最小的K对数字

183 阅读5分钟

大家好,我是挨打的阿木木,爱好算法的前端摸鱼老。最近会频繁给大家分享我刷算法题过程中的思路和心得。如果你也是想提高逼格的摸鱼老,欢迎关注我,一起学习。

题目

373. 查找和最小的K对数字

给定两个以升序排列的整数数组 nums1 和 ****nums2 ****, 以及一个整数 k ****。

定义一对值 (u,v),其中第一个元素来自 nums1,第二个元素来自 nums2 ****。

请找到和最小的 k 个数对 (u1,v1) (u2,v2)  ...  (uk,vk) 。

 

示例 1:

输入: nums1 = [1,7,11], nums2 = [2,4,6], k = 3
输出: [1,2],[1,4],[1,6]
解释: 返回序列中的前 3 对数:
     [1,2],[1,4],[1,6],[7,2],[7,4],[11,2],[7,6],[11,4],[11,6]

示例 2:

输入: nums1 = [1,1,2], nums2 = [1,2,3], k = 2
输出: [1,1],[1,1]
解释: 返回序列中的前 2 对数:
     [1,1],[1,1],[1,2],[2,1],[1,2],[2,2],[1,3],[1,3],[2,3]

示例 3:

输入: nums1 = [1,2], nums2 = [3], k = 3 
输出: [1,3],[2,3]
解释: 也可能序列中所有的数对都被返回:[1,3],[2,3]

 

提示:

  • 1 <= nums1.length, nums2.length <= 104
  • -109 <= nums1[i], nums2[i] <= 109
  • nums1nums2 均为升序排列
  • 1 <= k <= 1000

每日一皮

为了看官老爷们尽可能的能够容易理解,我时不时皮一下用暴力解法结合一些奇技淫巧把最简单的思路分享一遍,这个过程可能会经常翻车,请按需阅读。(PS: 适合没基础初学者)

思路

  1. 很简单的题目嘛,首先我们把所有的组合枚举一遍,可以用两个for循环, 我比较懒一行reduce搞定;
  2. 对所有枚举值做一个排序;
  3. 返回数组前k个元素;
  4. 说实话要不是为了格式容易读,一行链式编程代码就搞定了,洒洒水啦~

实现

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @param {number} k
 * @return {number[][]}
 */
var kSmallestPairs = function(nums1, nums2, k) {
    return  nums1.reduce((total, cur) => total.concat(nums2.map(v => [cur, v])), [])
                 .sort((a, b) => a[0] + a[1] - b[0] - b[1])
                 .slice(0, k);
};

翻车现场

image.png

可能很多初学的朋友没看懂,这是什么错误? 这在算法题里面比较常见,直白点翻译就是内存空间溢出了,就是说我们直接暴力枚举是不可取的。最后执行的输入可能折叠了看起来不直观,给各位看官老爷看个全貌。

image.png

可能有朋友又会疑惑了,那我刷题的时候怎么能够预见待会儿是否会内存溢出呢?这个答案要在提示中去寻找。

image.png

题目中已经说了, 每个数组最多有1万条数据, 正常1万条数据的话我们是不会溢出的,但是我们枚举是相当于1万条*1万条, 也就是一亿条数据。。。 咳咳,所以说翻车不是没有原因的。刚刚发生了一点小小的意外,不过不影响我们继续刷题,接下来给大家介绍一下正儿八经的解题思路。

正经做法

思路

  1. 我们可以创建一个长度为nums1.length的数组,来判断当前每个位置的值在另外数组中对应的指针位置;
  2. 由于nums1.length的最大取值范围在1万,有可能超过k的1千,为了勤俭持家我们可以创建数组的时候取两者中的最小值去创建,节省空间;
  3. 创建一个result数组,去存储我们的结果,当result.length < k时,我们每次从遍历所有位置找到其和对应指针的最小和;
  4. 每一轮我们把和最小的两个元素push进数组中,同时该元素的指针往右移动一个位置;
  5. 如果指针的位置已经到达最右边了,找下一个元素;
  6. 如果没有下一个元素了,结束循环。

实现

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @param {number} k
 * @return {number[][]}
 */
var kSmallestPairs = function(nums1, nums2, k) {
    let m = nums1.length, n = nums2.length;
    // 记录每个位置的指针,索引从0开始
    let dp = new Array(Math.min(m, k)).fill(0);
    let result = [];

    while (result.length < k) {
        let minIndex = -1, minValue = Number.MAX_VALUE;
        for (let i = 0; i < dp.length; i++) {
            // 兼容一下数量不够的情况
            if (dp[dp.length - 1] === n) return result;

            // 走到尽头了,此路不通
            if (dp[i] === n) continue;

            let cur = nums1[i] + nums2[dp[i]];
            if (cur < minValue) {
                minIndex = i;
                minValue = cur;
            }
        }

        // 记录当前最小值,指针右移
        result.push([ nums1[minIndex], nums2[dp[minIndex]] ]);
        dp[minIndex]++;
    }

    return result;
};

结果

image.png

追求极致

这道题目我们还可以做一个小细节优化,就是当我们走到第一个指针为0的元素时候即可结束循环,因为当前索引的指针位置为0, 说明再往后的值也不可能有比当前值小的了。

极致代码

/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @param {number} k
 * @return {number[][]}
 */
var kSmallestPairs = function(nums1, nums2, k) {
    let m = nums1.length, n = nums2.length;
    // 记录每个位置的指针,索引从0开始
    let dp = new Array(Math.min(m, k)).fill(0);
    let result = [];

    while (result.length < k) {
    let minIndex = -1, minValue = Number.MAX_VALUE;
    for (let i = 0; i < dp.length; i++) {
        // 兼容一下数量不够的情况
        if (dp[dp.length - 1] === n) return result;
        
        // 走到尽头了,此路不通
        if (dp[i] === n) continue;

        let cur = nums1[i] + nums2[dp[i]];
        if (cur < minValue) {
            minIndex = i;
            minValue = cur;
        }

        // 判断为空的时候不往后走
        if (dp[i] === 0) {
            break;
        }
    }

        // 记录当前最小值,指针右移
        result.push([ nums1[minIndex], nums2[dp[minIndex]] ]);
        dp[minIndex]++;
    }

    return result;
};

圆满

image.png

看懂了的小伙伴可以点个关注、咱们下道题目见。如无意外以后文章都会以这种形式,有好的建议欢迎评论区留言。