又到周五了,不过今天的中等题稍微有些动脑哦。
1、读题
言简意赅。
2、审题
题目给的信息很少,但足够表述清楚题意。
而你也能很轻松地发现这题的考点和难点。
但我们先说重点:
- 提供的两个数组是保证升序的
- 数组内可能存在相同的数字
- 两个数组组成的所有数对可能都不够K个(见示例3)
3、思路
最简单无脑暴力的思路,当然是嵌套循环,将两个数组的每个元素组成笛卡尔积,塞到一个优先级队列里,然后依次取出前K个。
很明显,在1 <= nums1.length, nums2.length <= 105 的条件下,这种思路多半是超时的。
即使不超时,耗时排名也会很难堪。
有这种想法绝不是坏事,它虽然朴素,但却能成为好的思路的跳板。
由于题目给出的K的范围最大是1000,所以一股脑把所有元素的笛卡尔积全部塞进去是很浪费的,于是我们可以一个个塞进去。
由于题目保证了nums1
和nums2
是严格升序的,所以很明显,(nums1[0],nums2[0])
是最小的数对。
而在此基础上,可能第二小的数对将存在于(nums1[1],nums2[0])
或(nums1[0],nums2[1])
之中。
于是,我们可以实现一个优先级队列,用于存放最小数对的下标,每次取出当前最小的数对,假设为(i,j)
,则在(i,j)
之后,比他稍大一点的数对可能为(i,j+1)
或(i+1,j)
,将它们全部添加到优先级队列中,并重复此过程,直到出队的的数量达到K,或队列已空。
4、动手吧!
class Solution {
public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
List<List<Integer>> result = new ArrayList<>();
Set<Integer> vis = new HashSet<>();
Queue<int[]> queue = new PriorityQueue<>((o1, o2) -> nums1[o1[0]] + nums2[o1[1]] - nums1[o2[0]] - nums2[o2[1]]);
queue.add(new int[]{0, 0});
while (result.size() < k && !queue.isEmpty()) {
int[] pair = queue.poll();
int i = pair[0], j = pair[1];
int p = i * 10000 + j;
//已访问则跳过
if (vis.contains(p)) {
continue;
}
//添加当前对到结果中
result.add(Arrays.asList(nums1[i], nums2[j]));
//也标记为已访问
vis.add(p);
//当nums1未抵达边界时,提交nums2的当前数和nums1的下一个数
if (i + 1 < nums1.length) {
queue.add(new int[]{i + 1, j});
}
//当nums2未抵达边界时,提交nums1的当前数和nums2的下一个数
if (j + 1 < nums2.length) {
queue.add(new int[]{i, j + 1});
}
}
return result;
}
}
5、解读
正如我之前所说,我们可以通过优先级队列来实时获取当前最小的数对。
于是,我实现了一个元素为int[]
的优先级队列,每一个int[]
包含了两个数,即为当前nums1[]
和nums2[]
的下标。
毕竟我们不仅要获取当前最小数对,还得获取可能的下一个最小数对,即(i,j+1)
或(i+1,j)
。
Queue<int[]> queue = new PriorityQueue<>((o1, o2) -> nums1[o1[0]] + nums2[o1[1]] - nums1[o2[0]] - nums2[o2[1]]);
queue.add(new int[]{0, 0});
然后,就是不停地从队列中取出当前最小数对,并往队列里放入下一个个可能的最小数对
while (result.size() < k && !queue.isEmpty()) {
int[] pair = queue.poll();
int i = pair[0], j = pair[1];
int p = i * 10000 + j;
//已访问则跳过
if (vis.contains(p)) {
continue;
}
//添加当前对到结果中
result.add(Arrays.asList(nums1[i], nums2[j]));
//也标记为已访问
vis.add(p);
//当nums1未抵达边界时,提交nums2的当前数和nums1的下一个数
if (i + 1 < nums1.length) {
queue.add(new int[]{i + 1, j});
}
//当nums2未抵达边界时,提交nums1的当前数和nums2的下一个数
if (j + 1 < nums2.length) {
queue.add(new int[]{i, j + 1});
}
}
循环的跳出条件则是达到K个数量,或是队列已空,即数组组成的数对不够K个时。
由于在添加次小数对时,是可能存在重复的,所以我还额外使用了一个Set
来去重。里面存放了已经访问过了的下标对。
由于题目限制了nums1
和nums2
的长度最大只有105,所以我将数对中nums1
的下标i
放大了10000倍,并和nums2
的下标j
相加,化二维为一维。
Set<Integer> vis = new HashSet<>();
……
……
int p = i * 10000 + j;
//已访问则跳过
if (vis.contains(p)) {
continue;
}
//添加当前对到结果中
result.add(Arrays.asList(nums1[i], nums2[j]));
//也标记为已访问
vis.add(p);
6、提交
7、咀嚼
如果是暴力的情况的话,我们会嵌套遍历nums1[]
和nums2[]
,时间复杂度将来到O(MN),M为nums1[]
的长度N为nums2[]
的长度。
而由于要存放所有产生的数对,所以空间复杂度同理也是O(MN)。
而经过优化呢,我们此时的时间复杂度将为O(NlogN),而N是K的大小。
当然,这里并不会辣么精准,你会发现优先级队列里存放的数据可能会超过K。
而由于使用了一个Set
和一个优先级队列,空间复杂度会来到O(2N),也是**O(N)**就是了。
8、学习大牛
嗯……
大牛到底是大牛,同是优先级队列,但使用思路上却是完全不同。
而且,除了堆的实现外,大牛还提供了了二分查找的实现方式,这里一并放出来供大家学习。
膜拜。
9、总结
周五了,今天也顺利地抢到了回老家的票。
但是上海昨天却新增了5例本地病例,这让我们沪漂的社畜们很是担忧。
不知道这个年能否团聚呢……