[路飞]_查找和最小的 K 对数字

156 阅读1分钟

「这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战

leetcode-373 查找和最小的 K 对数字
b站视频

题目介绍

给定两个以 升序排列 的整数数组 nums1nums2 , 以及一个整数 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 <= 10^5
  • -10^9 <= nums1[i], nums2[i] <= 10^9
  • nums1nums2 均为升序排列
  • 1 <= k <= 1000

解题思路

此题是求最值的问题,那么就可以用堆来解决,关键是用什么堆?

因为要求和最小的 K 对数字,所以可以使用一个大小为 K 的大顶堆来保存和最小的 K 对数字,此时这 K 对数字中和最大的数字对在堆顶的位置,所以之后的数字对只需要和堆顶的数字对进行比较即可,如果比堆顶的数字对的和小,那么就可以替换掉堆顶的数字对进入堆中,否则就不给进入堆中

解题步骤

  1. 创建一个大小为 K 的大顶堆
  2. 用双重 for 循环将两个数组的所有数对 push 到大顶堆中
  3. 如果当前堆的大小小于 K,那么直接往堆尾插入数对,然后向上调整大顶堆的结构
  4. 如果当前堆的大小等于 K,那么比较当前堆顶数对的和是否比新的数对的和大,如果是,新的数对替换当前堆顶的数对,然后向下调整大顶堆的结构;否则新的数对无法进入大顶堆中
  5. 当所有数对全部插入完毕,返回此时堆中的所有数对即为和最小的 K 对数字

解题代码

构建大顶堆

大顶堆需要具有 push(插入数对)、top(返回堆顶的数对)、size(堆的大小)、sortBack(向上调整堆结构)、sortFront(向下调整堆结构)、getHeap(返回堆中的元素)这几个方法

class Heap {
    constructor(k) {
        this.heap = []
        this.k = k
    }

    getHeap() {
        return this.heap
    }

    size() {
        return this.heap.length
    }

    top() {
        return this.heap[0]
    }

    push(val) {
        if (this.size() < this.k) {
            this.heap.push(val)
            this.sortBack()
        } else if (compare(val, this.heap[0]) < 0) {
            this.heap[0] = val
            this.sortFront()
        }
    }

    sortBack() {
        let i = this.heap.length - 1
        while (i > 0 && compare(this.heap[i], this.heap[Math.floor((i - 1) / 2)]) > 0) {
            [this.heap[i], this.heap[Math.floor((i - 1) / 2)]] = [this.heap[Math.floor((i - 1) / 2)], this.heap[i]]
            i = Math.floor((i - 1) / 2)
        }
    }

    sortFront() {
        let i = 0
        while (i * 2 + 1 < this.heap.length) {
            let temp = i
            if (compare(this.heap[temp], this.heap[i * 2 + 1]) < 0) temp = i * 2 + 1
            if (i * 2 + 2 < this.heap.length && compare(this.heap[temp], this.heap[i * 2 + 2]) < 0) temp = i * 2 + 2
            if (temp === i) break
            [this.heap[temp], this.heap[i]] = [this.heap[i], this.heap[temp]]
            i = temp
        }
    }
}

比较方法

需要比较两个数对之间和的大小关系,因此还需要一个比较方法 compare

var compare = function(a, b) {
    return a[0] + a[1] - b[0] - b[1]
}

遍历 nums1 和 nums2 两个数组中的所有数对插入到堆中

var kSmallestPairs = function(nums1, nums2, k) {
    const heap = new Heap(k)
    for (let i = 0; i < nums1.length; i++) {
        for (let j = 0; j < nums2.length; j++) {
            heap.push([nums1[i], nums2[j]])
        }
    }
    return heap.getHeap()
};

按照这样的解题方法,如果数据量过大会出现超出时间限制的报错

1642929145(1).png

优化思路

超出时间限制是因为我们没有利用好题目的条件,题目已经说明了这两个数组是 升序排序 的数组,因此假设当 [nums1[i], nums2[j] 这个数组已经大于等于堆顶的数对之和,那么说明 nums1[i], nums2[j + 1] 以及其之后的所有数对之和都将大于堆顶数对之和,因此我们可以不需要遍历之后的数对了,直接结束当前循环即可

优化方法

在遍历数对并将数对插入到堆中的过程中,我们可以做以下的判断:

  • 如果当前堆的大小等于 K,判断当前的数对是否大于堆顶的数对之和,如果是,退出当前的循环,不需要再遍历 数组 nums2 之后的元素

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

优化代码

var kSmallestPairs = function(nums1, nums2, k) {
    const heap = new Heap(k)
    for (let i = 0; i < nums1.length; i++) {
        for (let j = 0; j < nums2.length; j++) {
            if (heap.size() < k) {
                heap.push([nums1[i], nums2[j]])
            } else {
                // 如果待插入数对的和大于堆顶数对之和,退出当前循环
                if (compare([nums1[i], nums2[j]], heap.top()) >= 0) break
                heap.push([nums1[i], nums2[j]])
            }
        }
    }
    return heap.getHeap()
};

如此优化之后,顺利解决该问题