跳到主要内容

19 篇博文 含有标签「堆(优先队列)」

查看所有标签

· 阅读需 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

· 阅读需 4 分钟

1、题干

设计一个找到数据流中第 k 大元素的类(class)。注意是排序后的第 k 大元素,不是第 k 个不同的元素。

请实现 KthLargest 类:

  • KthLargest(int k, int[] nums) 使用整数 k 和整数流 nums 初始化对象。
  • int add(int val)val 插入数据流 nums 后,返回当前数据流中第 k 大的元素。

 

示例:

输入:
["KthLargest", "add", "add", "add", "add", "add"]
[[3, [4, 5, 8, 2]], [3], [5], [10], [9], [4]]
输出:
[null, 4, 5, 5, 8, 8]

解释:
KthLargest kthLargest = new KthLargest(3, [4, 5, 8, 2]);
kthLargest.add(3); // return 4
kthLargest.add(5); // return 5
kthLargest.add(10); // return 5
kthLargest.add(9); // return 8
kthLargest.add(4); // return 8

 

提示:

  • 1 <= k <= 104
  • 0 <= nums.length <= 104
  • -104 <= nums[i] <= 104
  • -104 <= val <= 104
  • 最多调用 add 方法 104
  • 题目数据保证,在查找第 k 大元素时,数组中至少有 k 个元素

 

注意:本题与主站 703 题相同: https://leetcode-cn.com/problems/kth-largest-element-in-a-stream/

2、解法1-优先队列

使用优先队列解题,初始化时把所有元素加入到最小优先队列pq中,添加元素时直接把元素加入pq,若pq元素个数小于k则直接返回-1,若pq元素个数超过k则逐一移除最后返回队头元素。


3、代码

var KthLargest = function (k, nums) {
this.k = k;
this.pq = new MinPriorityQueue({ compare: (a, b) => a - b });
for (const n of nums) this.pq.enqueue(n);
};

KthLargest.prototype.add = function (val) {
this.pq.enqueue(val);
if (this.pq.size() < this.k) return -1;
while (this.pq.size() > this.k) this.pq.dequeue();
return this.pq.front();
};

4、复杂度

  • 时间复杂度:O(nlogn)O(nlogn)
  • 空间复杂度:O(n)O(n)

5、执行结果

执行用时: 132 ms 内存消耗: 46.6 MB


6、解法2-计数排序

对数据流使用计数排序,首次确定第 k 大元素 kIdx 需要遍历所有元素;此后添加数据需要再确定第 k 大元素时,如果添加的数据比 kIdx 小则直接返回 kIdx ;如果添加的数据比 kIdx 小则从下标 kIdx 开始在容器 list 找到更大且元素值不为0的下标返回即可。


这个题目中使用该算法坑略多,只作参考,不建议使用。


7、复杂度

  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

8、代码

var KthLargest = function (k, nums) {
this.k = k;
this.size = nums.length;
this.kIdx = -1;
this.list = new Array(2e4 + 1).fill(0);
for (const n of nums) this.list[n + 1e4]++;
};

KthLargest.prototype.add = function (val) {
(val += 1e4), this.list[val]++, this.size++;
if (this.size < this.k) return -1;

if (this.kIdx < 0) {
for (let i = this.list.length - 1, k = this.k; i >= 0; i--) {
if (k < this.list[i]) this.list[i] = k;
if ((k -= this.list[i]) <= 0) {
this.kIdx = i;
break;
}
}
return this.kIdx - 1e4;
}

if (this.size > this.k && val >= this.kIdx) {
this.list[this.kIdx]--;
for (let i = this.kIdx; i < this.list.length; i++) {
if (this.list[i]) {
this.kIdx = i;
break;
}
}
}

return this.kIdx - 1e4;
};

9、执行结果

1.png

· 阅读需 4 分钟

1、题干

设计一个找到数据流中第 k 大元素的类(class)。注意是排序后的第 k 大元素,不是第 k 个不同的元素。

请实现 KthLargest 类:

  • KthLargest(int k, int[] nums) 使用整数 k 和整数流 nums 初始化对象。
  • int add(int val)val 插入数据流 nums 后,返回当前数据流中第 k 大的元素。

 

示例:

输入:
["KthLargest", "add", "add", "add", "add", "add"]
[[3, [4, 5, 8, 2]], [3], [5], [10], [9], [4]]
输出:
[null, 4, 5, 5, 8, 8]

解释:
KthLargest kthLargest = new KthLargest(3, [4, 5, 8, 2]);
kthLargest.add(3); // return 4
kthLargest.add(5); // return 5
kthLargest.add(10); // return 5
kthLargest.add(9); // return 8
kthLargest.add(4); // return 8

 

提示:

  • 1 <= k <= 104
  • 0 <= nums.length <= 104
  • -104 <= nums[i] <= 104
  • -104 <= val <= 104
  • 最多调用 add 方法 104
  • 题目数据保证,在查找第 k 大元素时,数组中至少有 k 个元素

 

注意:本题与主站 703 题相同: https://leetcode-cn.com/problems/kth-largest-element-in-a-stream/

2、解法1-优先队列

使用优先队列解题,初始化时把所有元素加入到最小优先队列pq中,添加元素时直接把元素加入pq,若pq元素个数小于k则直接返回-1,若pq元素个数超过k则逐一移除最后返回队头元素。


3、代码

var KthLargest = function (k, nums) {
this.k = k;
this.pq = new MinPriorityQueue({ compare: (a, b) => a - b });
for (const n of nums) this.pq.enqueue(n);
};

KthLargest.prototype.add = function (val) {
this.pq.enqueue(val);
if (this.pq.size() < this.k) return -1;
while (this.pq.size() > this.k) this.pq.dequeue();
return this.pq.front();
};

4、复杂度

  • 时间复杂度:O(nlogn)O(nlogn)
  • 空间复杂度:O(n)O(n)

5、执行结果

执行用时: 132 ms 内存消耗: 46.6 MB


6、解法2-计数排序

对数据流使用计数排序,首次确定第 k 大元素 kIdx 需要遍历所有元素;此后添加数据需要再确定第 k 大元素时,如果添加的数据比 kIdx 小则直接返回 kIdx ;如果添加的数据比 kIdx 小则从下标 kIdx 开始在容器 list 找到更大且元素值不为0的下标返回即可。


这个题目中使用该算法坑略多,只作参考,不建议使用。


7、复杂度

  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

8、代码

var KthLargest = function (k, nums) {
this.k = k;
this.size = nums.length;
this.kIdx = -1;
this.list = new Array(2e4 + 1).fill(0);
for (const n of nums) this.list[n + 1e4]++;
};

KthLargest.prototype.add = function (val) {
(val += 1e4), this.list[val]++, this.size++;
if (this.size < this.k) return -1;

if (this.kIdx < 0) {
for (let i = this.list.length - 1, k = this.k; i >= 0; i--) {
if (k < this.list[i]) this.list[i] = k;
if ((k -= this.list[i]) <= 0) {
this.kIdx = i;
break;
}
}
return this.kIdx - 1e4;
}

if (this.size > this.k && val >= this.kIdx) {
this.list[this.kIdx]--;
for (let i = this.kIdx; i < this.list.length; i++) {
if (this.list[i]) {
this.kIdx = i;
break;
}
}
}

return this.kIdx - 1e4;
};

9、执行结果

1.png

· 阅读需 6 分钟

1、题干

有一棵特殊的苹果树,一连 n 天,每天都可以长出若干个苹果。在第 i 天,树上会长出 apples[i] 个苹果,这些苹果将会在 days[i] 天后(也就是说,第 i + days[i] 天时)腐烂,变得无法食用。也可能有那么几天,树上不会长出新的苹果,此时用 apples[i] == 0days[i] == 0 表示。

你打算每天 最多 吃一个苹果来保证营养均衡。注意,你可以在这 n 天之后继续吃苹果。

给你两个长度为 n 的整数数组 daysapples ,返回你可以吃掉的苹果的最大数目

 

示例 1:

输入:apples = [1,2,3,5,2], days = [3,2,1,4,2]
输出:7
解释:你可以吃掉 7 个苹果:
- 第一天,你吃掉第一天长出来的苹果。
- 第二天,你吃掉一个第二天长出来的苹果。
- 第三天,你吃掉一个第二天长出来的苹果。过了这一天,第三天长出来的苹果就已经腐烂了。
- 第四天到第七天,你吃的都是第四天长出来的苹果。

示例 2:

输入:apples = [3,0,0,0,0,2], days = [3,0,0,0,0,2]
输出:5
解释:你可以吃掉 5 个苹果:
- 第一天到第三天,你吃的都是第一天长出来的苹果。
- 第四天和第五天不吃苹果。
- 第六天和第七天,你吃的都是第六天长出来的苹果。

 

提示:

  • apples.length == n
  • days.length == n
  • 1 <= n <= 2 * 104
  • 0 <= apples[i], days[i] <= 2 * 104
  • 只有在 apples[i] = 0 时,days[i] = 0 才成立

又是优先队列,JS没有优先队列,又是被折磨的一天。。。 这道题跟课程表 III类似,可以看看我之前的题解,也是计数排序

2、执行结果

1.png

3、解题思路

  • 总体思路:使用贪心策略,在尚未腐烂的苹果中优先选择保质期最小的苹果。
  • 关键思路:基于计数排序求保质期最小的苹果,用数组存储苹果数量和保质期,数组的下标表示保质期、值表示该日期对应的苹果数量;找保质期最小的苹果只需要以上一个最小保质期为起点,遍历数组找到下标最小且值不为0的元素。
  • 时间复杂度:由于遍历的起点是上一个最小的保质期,因此总体实现出来的时间复杂度是 O(n)O(n),其中 nn 是最大保质期。

下面说明代码实现细节:

  • 计数排序容器记为freshArr,其下标表示保质期、值表示该保质期之前的苹果数量
  • 最小保质期记为minDay,用于记录有苹果剩余的最小保质期
  • 可以吃掉苹果的最大数量记为res,即最终结果
  • 接下来以最大保质期为限制逐天遍历,天数记为i
  • 在第i天时,如果i没有超过n且过期天数大于0(i < days.length && days[i] > 0),进行以下操作
    • 在计数排序容器freshArr中按保质期i + days[i] - 1累加苹果数量
    • 更新最小保质期minDay = Math.min(minDay, i + days[i] - 1)
  • 接下来只要找到有苹果剩余的最小保质期即可
    • 先确保最小保质期在今天之后,即minDay = Math.max(minDay, i)
    • 然后在计数排序容器freshArr中遍历,找到有苹果剩余的最小保质期
  • 接下来更新最终结果和计数排序容器freshArr中的苹果数量
    • 注意更新之前确定该保质期有苹果剩余,即if (freshArr[minDay]) res++, freshArr[minDay]--
  • 最后就得到可以吃掉苹果的最大数量res

4、代码

var eatenApples = function (apples, days) {
let res = 0, minDay = days[0] - 1;
const freshArr = new Array(days.length).fill(0);
for (let i = 0; i < freshArr.length; i++) {
if (i < days.length && days[i] > 0) {
freshArr[i + days[i] - 1] = (freshArr[i + days[i] - 1] || 0) + apples[i];
minDay = Math.min(minDay, i + days[i] - 1);
}
minDay = Math.max(minDay, i);
while (minDay < freshArr.length && !freshArr[minDay]) minDay++;
if (freshArr[minDay]) res++, freshArr[minDay]--;
}
return res;
};

· 阅读需 5 分钟

1、题干

这里有 n 门不同的在线课程,按从 1n 编号。给你一个数组 courses ,其中 courses[i] = [durationi, lastDayi] 表示第 i 门课将会 持续durationi 天课,并且必须在不晚于 lastDayi 的时候完成。

你的学期从第 1 天开始。且不能同时修读两门及两门以上的课程。

返回你最多可以修读的课程数目。

 

示例 1:

输入:courses = [[100, 200], [200, 1300], [1000, 1250], [2000, 3200]]
输出:3
解释:
这里一共有 4 门课程,但是你最多可以修 3 门:
首先,修第 1 门课,耗费 100 天,在第 100 天完成,在第 101 天开始下门课。
第二,修第 3 门课,耗费 1000 天,在第 1100 天完成,在第 1101 天开始下门课程。
第三,修第 2 门课,耗时 200 天,在第 1300 天完成。
第 4 门课现在不能修,因为将会在第 3300 天完成它,这已经超出了关闭日期。

示例 2:

输入:courses = [[1,2]]
输出:1

示例 3:

输入:courses = [[3,2],[4,3]]
输出:0

 

提示:

  • 1 <= courses.length <= 104
  • 1 <= durationi, lastDayi <= 104

2、提交结果

逻辑和代码比优先队列更简洁,执行效率更高。 1.png

3、解题思路

破题思路还是看官解。这个题解主要说明如何在保证效率的前提下用更少代码替代优先队列。

每次答优先队列的题都巨难受,因为JS没有优先队列这种数据结构。由于数据范围特征明显1 <= duration[i] <= 1e4,可以用计数排序解这道题,代码比优先队列更简洁。凑巧的是测试用例也比较给力,执行效率比优先队列还好看些。接下来说下如何用计数排序替代优先队列。

设计思路

优先队列解决的关键问题是快速插入、删除和查找最值。因此我们设计一个名为FPQ的数据结构解决这几个问题,FPQ主要包括的函数和变量是:listmaxconstructor()add()del()

  • list:元素容器,下标代表待排序的数据,元素值代表下标数字的数量
  • max:所有元素中的最大值;可直接读取,时间复杂度为O(1)O(1)
  • constructor():构造函数,入参为整数n;时间复杂度为O(n)O(n);函数逻辑如下:
    • 创建一个大小为n+1的数组list,初始化max为0
  • add():往list中添加元素,入参为整数n;时间复杂度为O(1)O(1);函数逻辑如下:
    • list[n]自加1
    • 如果 n > max,则将max赋值为n
  • del():删除list中的元素,入参为整数 n;最坏时间复杂度为O(n)O(n);函数逻辑如下:
    • list[n]自减1
    • 如果 n === maxlist[n]===0,则需要重新计算最大值
    • 计算最大值:从下标 n 开始倒序遍历 list,如果list[n]>0,则将max赋值为list[n]并退出遍历

FPQ的插入方法比优先队列更高效,但删除方法不稳定,其效率可能比优先队列更好也可能更差。另外由于使用计数排序,因此使用场景也很有限。

4、代码

var scheduleCourse = function (courses) {
courses.sort((a, b) => a[1] - b[1]);

const max = courses.reduce((acc, cur) => (cur[0] > acc ? cur[0] : acc), courses[0][0]);
const pq = new FPQ(max);

let res = 0;
let time = 0;
for (let i = 0; i < courses.length; i++) {
if (time + courses[i][0] <= courses[i][1]) {
res++;
time += courses[i][0];
pq.add(courses[i][0]);
} else if (courses[i][0] < pq.max && time + courses[i][0] - pq.max < courses[i][1]) {
time += courses[i][0] - pq.max;
pq.add(courses[i][0]);
pq.del(pq.max);
}
}

return res;
};

class FPQ {
constructor(n) {
this.list = new Array(n + 1).fill(0);
this.max = 0;
}

add(n) {
this.list[n] += 1;
if (n > this.max) this.max = n;
}

del(n) {
this.list[n] -= 1;
if (n !== this.max || this.list[n]) return;
while (n > -1) {
if (this.list[n] || !n) return (this.max = n);
n--;
}
}
}

· 阅读需 9 分钟

1、题干

给你一个按递增顺序排序的数组 arr 和一个整数 k 。数组 arr1 和若干 素数  组成,且其中所有整数互不相同。

对于每对满足 0 <= i < j < arr.lengthij ,可以得到分数 arr[i] / arr[j]

那么第 k 个最小的分数是多少呢?  以长度为 2 的整数数组返回你的答案, 这里 answer[0] == arr[i] 且 answer[1] == arr[j]

 

示例 1:

输入:arr = [1,2,3,5], k = 3
输出:[2,5]
解释:已构造好的分数,排序后如下所示:
1/5, 1/3, 2/5, 1/2, 3/5, 2/3
很明显第三个最小的分数是 2/5

示例 2:

输入:arr = [1,7], k = 1
输出:[1,7]

 

提示:

  • 2 <= arr.length <= 1000
  • 1 <= arr[i] <= 3 * 104
  • arr[0] == 1
  • arr[i] 是一个 素数i > 0
  • arr 中的所有数字 互不相同 ,且按 严格递增 排序
  • 1 <= k <= arr.length * (arr.length - 1) / 2

 

进阶:你可以设计并实现时间复杂度小于 O(n2) 的算法解决此问题吗?

2、解法1-暴力解法

创建数组matrix,两层循环遍历素数数组,把所有素数对存入matrix中,然后对matrix进行排序。最先尝试了暴力解法,居然过了。。。


3、代码

var kthSmallestPrimeFraction = function (arr, k) {
const p = [];
for (let i = 1; i < arr.length; i++) {
for (let j = 0; j < i; j++) {
p.push([arr[j], arr[i]]);
}
}

p.sort((a, b) => a[0] / a[1] - b[0] / b[1]);

return p[k - 1];
};
  • 时间复杂度: O(n2log(n2))O(n^2*log(n^2))
  • 执行用时: 1908 ms,内存消耗: 94 MB

4、解法2-暴力解法的二分优化

之后尝试使用二分法优化这段代码,这里二分的目标是matrix而不是原始数组arr,因为时间复杂度最高的地方在matrix的排序。优化后时间复杂度量级没有改变,但是matrix长度减半,执行速度会有所提升

思路是:

  • 创建两个数组m1和m2,两层循环遍历素数数组
  • 把所有商小于0.5的素数对存入m1中,其余存入m2中
  • 如果k <= m1.length就对m1排序并返回m1[k-1]
  • 如果k > m1.length就对m2排序并返回m2[k - m1.length - 1]

5、代码

var kthSmallestPrimeFraction = function (arr, k) {
const p1 = [];
const p2 = [];
for (let i = 1; i < arr.length; i++) {
for (let j = 0; j < i; j++) {
if (arr[j] / arr[i] < 0.5) {
p1.push([arr[j], arr[i]]);
} else {
p2.push([arr[j], arr[i]]);
}
}
}

if (k <= p1.length) {
p1.sort((a, b) => a[0] / a[1] - b[0] / b[1]);
return p1[k - 1];
} else {
p2.sort((a, b) => a[0] / a[1] - b[0] / b[1]);
return p2[k - p1.length - 1];
}
};
  • 时间复杂度: O(n2log(n2))O(n^2*log(n^2))
  • 执行用时: 1276 ms 内存消耗: 104.1 MB

6、解法3-桶排序

二分方案中,把matrix二分后两个子数组的元素个数的数量级几乎相当,其实推广到N(2<N<=n(n1)/2,n=1000)N(2<N<=n*(n-1)/2,n=1000)也适用。也就是说matrix分成N个子组后每个子组的长度几乎相等,这其实就是桶排序。

所以如果这样理解题意就简单很多:数组中最多包含约500000(n(n1)/2,n=1000n*(n-1)/2,n=1000)个无序排列的小数,这些小数从0到1均匀分布,返回其中第k小的小数

桶排序的思路:

  • 创建大小为N的二维数组matrix,matrix中的每个元素(数组)就是一个桶
  • 根据分组规则把所有小数均匀放入桶中
  • 累计每个桶内元素数量,找到第k个小数所在的桶matrix[k'],对该桶内小数进行排序
  • 在matrix[k']这个桶中找到第k个小数即可

小数的分组规则:

  • 分组的关键问题是分组规则,也就是任意小数a应该放入桶的序号i是多少(即a与i之间的映射关系)
  • 实际上可以用桶的数量NN乘以小数aa再取整作为桶的序号,即i=Math.trunc(aN)i=Math.trunc(a*N),这样就建立起小数a与桶序号i的映射,消耗的时间复杂度是O(1)O(1)

对于这个分组规则可以举个例子:假设NN10001000,对于任意小数0.00120.0012,把数据代入公式i=Math.trunc(aN)i=Math.trunc(a*N)中算得i=1i=1,也就是说0.00120.0012应该放入matrix[1]matrix[1]这个桶中。

还是在暴力解法的基础上优化,只需要把matrix分为N个子数组,然后只对包含第k个小数的子数组进行排序,最后返回对应的素数对即可。

如果N的取值合适的话,时间复杂度可以降到 O(n2)O(n^2)。这里使用的取值计算公式是:N = 10 ** Math.trunc(Math.log10(arr[arr.length - 1]))


7、代码

var kthSmallestPrimeFraction = function (arr, k) {
const BASE = 10 ** Math.trunc(Math.log10(arr[arr.length - 1]));
const matrix = new Array(BASE);
for (let i = 1; i < arr.length; i++) {
for (let j = 0; j < i; j++) {
const index = Math.trunc(arr[j] / arr[i] * BASE);
if (!matrix[index]) matrix[index] = [];
matrix[index].push([arr[j], arr[i]]);
}
}

for (let arr of matrix) {
arr = arr || [];
if (k - arr.length <= 0) {
arr.sort((a, b) => a[0] / a[1] - b[0] / b[1]);
return arr[k - 1];
}
k = k - arr.length;
}
};
  • 时间复杂度: O(n2)O(n^2)
  • 执行用时: 876 ms 内存消耗: 97.8 MB

8、解法4-桶排序优化

每个桶都使用Array.push放入数据,最终只使用了其中一个桶进行排序,这在时间和空间上都存在大量浪费。可以进一步优化为,只在包含第k个小数的桶中存入素数对,其他的桶只记录元素个数即可。

程序最终逻辑:

  • 计算分组数量N
  • 初始化matrix并写入N个0
  • 双层遍历arr,计算每个桶包含小数的数量
  • 按桶累计小数数量,找到第k个小数对应的桶序号index,将桶matrix[index]赋值为空数组
  • 双层遍历arr,当小数对应的序号与index相等时,将素数对放入桶matrix[index]
  • 对桶matrix[index]进行排序,返回第k个素数对

9、代码

var kthSmallestPrimeFraction = function(arr, k) {
// 计算分组数量N
const N = 10 ** Math.trunc(Math.log10(arr[arr.length - 1]));
// 初始化matrix并写入N个0
const matrix = new Array(N).fill(0);

// 双层遍历arr,计算每个桶包含小数的数量
for (let i = 1; i < arr.length; i++) {
for (let j = 0; j < i; j++) {
const m = Math.trunc((arr[j] / arr[i]) * N);
matrix[m] += 1;
}
}

// 按桶累计小数数量,找到第k个小数对应的桶序号index,将桶`matrix[index]`赋值为空数组
let index = 0;
for (let i = 0; i < matrix.length; i++) {
if (k - matrix[i] <= 0) {
index = i;
matrix[index] = [];
break;
}
k = k - matrix[i];
}

// 双层遍历arr,当小数对应的序号与index相等时,将素数对放入桶`matrix[index]`中
for (let i = 1; i < arr.length; i++) {
for (let j = 0; j < i; j++) {
const m = Math.trunc((arr[j] / arr[i]) * N);
if (index === m) matrix[m].push([arr[j], arr[i]]);
}
}

// 对桶`matrix[index]`进行排序,返回第k个素数对
matrix[index].sort((a, b) => a[0] / a[1] - b[0] / b[1]);
return matrix[index][k - 1];
};
  • 时间复杂度: O(n2)O(n^2)
  • 执行用时: 92 ms 内存消耗: 41.1 MB

20211129.png

· 阅读需 2 分钟

1、题干

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

 

示例 1:

输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]

示例 2:

输入: nums = [1], k = 1
输出: [1]

 

提示:

  • 1 <= nums.length <= 105
  • k 的取值范围是 [1, 数组中不相同的元素的个数]
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的

 

进阶:你所设计算法的时间复杂度 必须 优于 O(n log n) ,其中 n 是数组大小。

解题思路

  • 创建哈希映射map,用于统计数字出现次数,key为数字,value为出现次数
  • 创建数组arr,用于统计每个出现次数对应的数字集合,数组下标代表数字出现次数,值为出现次数与之相等的数字的集合
  • 倒序遍历数组arr,提取前出现频率前 k 高的数字

代码

/**
* @param {number[]} nums
* @param {number} k
* @return {number[]}
*/
var topKFrequent = function(nums, k) {
const map = {};
for (let i = 0; i < nums.length; i++) {
map[nums[i]] = (map[nums[i]] || 0) + 1;
}

const matrix = Object.entries(map);
const arr = [];
for (let i = 0; i < matrix.length; i++) {
const [n, c] = matrix[i];
if (!arr[c]) arr[c] = [];
arr[c].push(+n);
}

const res = [];
for (let i = arr.length - 1; i > -1; i--) {
if (arr[i] && res.length < k) {
res.push(...arr[i].slice(0, k - res.length));
}
}

return res;
};

· 阅读需 2 分钟

1、题干

给你一个 n x n 矩阵 matrix ,其中每行和每列元素均按升序排序,找到矩阵中第 k 小的元素。
请注意,它是 排序后 的第 k 小元素,而不是第 k不同 的元素。

你必须找到一个内存复杂度优于 O(n2) 的解决方案。

 

示例 1:

输入:matrix = [[1,5,9],[10,11,13],[12,13,15]], k = 8
输出:13
解释:矩阵中的元素为 [1,5,9,10,11,12,13,13,15],第 8 小元素是 13

示例 2:

输入:matrix = [[-5]], k = 1
输出:-5

 

提示:

  • n == matrix.length
  • n == matrix[i].length
  • 1 <= n <= 300
  • -109 <= matrix[i][j] <= 109
  • 题目数据 保证 matrix 中的所有行和列都按 非递减顺序 排列
  • 1 <= k <= n2

 

进阶:

  • 你能否用一个恒定的内存(即 O(1) 内存复杂度)来解决这个问题?
  • 你能在 O(n) 的时间复杂度下解决这个问题吗?这个方法对于面试来说可能太超前了,但是你会发现阅读这篇文章( this paper )很有趣。

代码

/**
* @param {number[][]} matrix
* @param {number} k
* @return {number}
*/
var kthSmallest = function(matrix, k) {
while (matrix.length > 1) {
const nums = [];
const nums1 = matrix.shift();
const nums2 = matrix.shift();

let [k1, k2] = [0, 0];
while (k1 < nums1.length || k2 < nums2.length) {
if (k2 >= nums2.length || (k1 < nums1.length && nums1[k1] <= nums2[k2])) {
nums.push(nums1[k1]);
k1++;
continue;
}

if (k1 >= nums1.length || (k2 < nums2.length && nums1[k1] > nums2[k2])) {
nums.push(nums2[k2]);
k2++;
}
}

matrix.push(nums);
}

return matrix[0][k - 1];
};

· 阅读需 4 分钟

1、题干

有些数的素因子只有 3,5,7,请设计一个算法找出第 k 个数。注意,不是必须有这些素因子,而是必须不包含其他的素因子。例如,前几个数按顺序应该是 1,3,5,7,9,15,21。

示例 1:

输入: k = 5

输出: 9

1、解题思路

一开始没想到动态规划、三指针、步进法这些方案,看了题解还是觉得这些规律或方案有点难想到。我的第一反应是:这个问题好像可以用BFS+Set去重来解决。

1.1、转换思路

根据这个方向,实际上可以把问题转换成:已知一棵树的根是1,第一层节点是根节点与 3、5、7相乘的结果,第二层节点是第一层节点与 3、5、7相乘的结果,以此类推; 求这棵树中按广度遍历的第k个子节点的值。于是写下了这段代码:

var getKthMagicNumber = function(k) {
let nums = [1];

while (nums.length) {
const set = new Set();

for (let i = 0; i < nums.length; i++) {
k--;
if (!k) return nums[i];
set.add(3 * nums[i]);
set.add(5 * nums[i]);
set.add(7 * nums[i]);
}

nums = [...set];
}
};

1.2、测试出错

写完之后测试出错,是因为忽略了一个很重要的条件:按顺序排列。在上述代码中用于遍历树的 nums 代表的是每一层节点,它始终是单调递增的。虽然每一层节点都单调递增,但是随着层数增加,下一层的节点却有可能小于上一层节点。

1.3、解决问题

要解决这个问题也不难,只要把所有节点都放入 nums 中,并且保证它的单调性就OK了。

1.4、整体方案

因此,使用BFS + Set去重 + 单调栈的方式就能解出这道题,问题的关键点有以下3个:

  • 使用非递归的BFS方式进行遍历
  • 使用Set去除重复的数字
  • 使用单调栈保证数字升序排列

2、代码

var getKthMagicNumber = function (k) {
const nums = [1];
const set = new Set();

for (let i = 0; i < k; i++) {
if (i + 1 === k) return nums[i];
const arr = [3 * nums[i], 5 * nums[i], 7 * nums[i]];

arr.forEach(n => {
if (set.has(n)) return;
set.add(n);

if (n >= nums[nums.length - 1]) return nums.push(n);

// 以上代码是常规逻辑:BFS + Set去重
// 以下代码是为了保证 nums 的单调性
const arr = [];
while (n < nums[nums.length - 1]) {
arr.unshift(nums.pop());
}
nums.push(n, ...arr);
});
}
};