跳到主要内容

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

· 阅读需 8 分钟

1、题干

给定两个以 非递减顺序排列 的整数数组 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 <= 105
  • -109 <= nums1[i], nums2[i] <= 109
  • nums1nums2 均为 升序排列
  • 1 <= k <= 104
  • k <= nums1.length * nums2.length

最近发现,力扣的JS环境支持最小优先队列 MinPriorityQueue 和 最大优先队列 MaxPriorityQueue,直接使用即可,无需引入或特殊配置,其 API 和相关信息可以参考datastructures-js


2、解法1-优先队列

  • 使用最小优先队列 MinPriorityQueue 存储 nums1 中所有元素下标与 nums20 下标构成的数对
  • 然后取队头下标数对进行遍历,遍历时以 nums2 的下标步进
  • 若队头下标数对所对应元素之和比当前数对之和更小则将当前下标入队并中断遍历,反之将当前数对推入结果数组 res
  • 重复前两步遍历直至结束

3、代码

var kSmallestPairs = function (nums1, nums2, k) {
const res = [];
const pq = new MinPriorityQueue({ compare: (a, b) => nums1[a[0]] + nums2[a[1]] - (nums1[b[0]] + nums2[b[1]]) });

for (let i = 0; i < nums1.length; i++) pq.enqueue([i, 0]);

while (res.length < k && pq.size()) {
let [i, j] = pq.dequeue();

for (; res.length < k && j < nums2.length; j++) {
// 队头的数对和更小
const [i1, j1] = pq.front() || [];
if (pq.size() && nums1[i1] + nums2[j1] < nums1[i] + nums2[j]) {
pq.enqueue([i, j]);
break;
}

// 队头的数对和相等或更大
res.push([nums1[i], nums2[j]]);
}
}

return res;
};

4、执行结果

执行用时: 160 ms 内存消耗: 61.7 MB


5、解法1-优化

  • 问题1:因为是求 TopK,所以优先队列没必要存储超过 k 个元素,这是导致时间空间过高的主要原因
  • 问题2:既然是求 TopK,队列初始时已加入 k 个元素,后续只要在队列中排序和轮替所有数据即可;而上述逻辑是一边遍历一边与队头元素比较,虽然不影响时间和空间,但是代码和逻辑都更复杂
  • 优化1:优先队列添加初始数据时,只需要加入 Math.min(k, nums1.length) 个元素即可
  • 优化2:while 循环那段代码的逻辑改成:每次取队头下标对应的数对推入结果数组 res,并且将下一对未入队的下标 [i, j + 1] 加入队列中

6、复杂度

  • 时间复杂度:O(klogk)O(klogk)
  • 空间复杂度:O(k)O(k)

7、代码

var kSmallestPairs = function (nums1, nums2, k) {
const res = [];
const pq = new MinPriorityQueue({ compare: (a, b) => nums1[a[0]] + nums2[a[1]] - (nums1[b[0]] + nums2[b[1]]) });

for (let i = 0; i < Math.min(k, nums1.length); i++) pq.enqueue([i, 0]);

while (res.length < k && pq.size()) {
const [i, j] = pq.dequeue();
if (j + 1 < nums2.length) pq.enqueue([i, j + 1]);
res.push([nums1[i], nums2[j]]);
}

return res;
};

8、执行结果

1.png


9、解法2-单调栈

  • 思路跟优先队列的方式类似,使用递减栈 minStack 存储 nums1 中所有元素下标与 nums20 下标构成的数对,注意要倒序入栈以保证 minStack 必定是一个递减栈
  • 然后取栈顶下标数对进行遍历,遍历时以 nums2 的下标步进
  • 若栈顶下标数对所对应元素之和比当前数对之和更小则将当前下标入栈并中断遍历,反之将当前数对推入结果数组 res
  • 上一步的入栈不是压入栈顶,而是倒序遍历插入更大元素之后以保持 minStack 递减(此时二分查找再入栈无法降时间复杂度)
  • 重复前三步遍历直至结束

10、代码

var kSmallestPairs = function (nums1, nums2, k) {
const res = [], minStack = [];
for (let i = nums1.length - 1; i > -1; i--) minStack.push([i, 0]);

while (res.length < k && minStack.length) {
let [i, j] = minStack.pop();

for (; res.length < k && j < nums2.length; j++) {
// 栈顶的数对和相等或更大
const [i1, j1] = minStack[minStack.length - 1] || [];
if (!minStack.length || nums1[i1] + nums2[j1] >= nums1[i] + nums2[j]) {
res.push([nums1[i], nums2[j]]);
continue;
}

// 栈顶的数对和更小
if (nums1[minStack[0][0]] + nums2[minStack[0][1]] <= nums1[i] + nums2[j]) {
minStack.unshift([i, j]);
break;
}

for (let m = minStack.length - 1; m > -1; m--) {
const [i1, j1] = minStack[m];
if (nums1[i1] + nums2[j1] >= nums1[i] + nums2[j]) {
minStack.splice(m + 1, 0, [i, j]);
break;
}
}
break;
}
}

return res;
};

11、执行结果

执行用时: 152 ms 内存消耗: 60.4 MB


12、解法2-优化

问题和优化点都跟解法1的问题和优化点一样,单调栈 minStack 没必要存储超过 k 个元素,后续找最小的 k 对数字只要在单调栈中排序和轮替所有数据即可


13、复杂度

  • 时间复杂度:复杂度不稳定,介于 O(k)O(k)O(k2)O(k^2) 之间
  • 空间复杂度:O(k)O(k)

14、代码

var kSmallestPairs = function (nums1, nums2, k) {
const res = [], minStack = [];
for (let i = Math.min(k, nums1.length) - 1; i > -1; i--) minStack.push([i, 0]);

while (res.length < k && minStack.length) {
const [i, j] = minStack.pop();
res.push([nums1[i], nums2[j]]);

if (j + 1 >= nums2.length) continue;
const len = minStack.length;
for (let m = len - 1; m > -1; m--) {
const [i1, j1] = minStack[m];
if (nums1[i1] + nums2[j1] >= nums1[i] + nums2[j + 1]) {
minStack.splice(m + 1, 0, [i, j + 1]);
break;
}
}
if (len === minStack.length) minStack.unshift([i, j + 1]);
}

return res;
};

15、执行结果

执行用时: 120 m 内存消耗: 51.6 MB