「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战」
leetcode-703 数据流中的第 K 大元素
b站视频
题目介绍
设计一个找到数据流中第 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 <= 10^40 <= nums.length <= 10^4-10^4 <= nums[i] <= 10^4-10^4 <= val <= 10^4- 最多调用
add方法10^4次 题目数据保证,在查找第k大元素时,数组中至少有k个元素
解题思路
这道题可以转化为求最值的问题,数据流中的第 K 大元素,那么我们可以将其转化为最大的 K 个元素中的最小值,转化为最值问题之后就可以用堆的思想来解决
但是今天不想直接从堆这种解决方式开始将,我们先从最直接的暴力排序讲起,然后一步一步优化成堆的结构
思路一:暴力排序
暴力排序的方式就是每次调用 add 方法的时候,直接将 val 值 push 到数组中,然后对数组进行排序,并返回第 k 大的值
解题代码
var KthLargest = function(k, nums) {
this.arr = nums
this.k = k
};
KthLargest.prototype.add = function(val) {
this.arr.push(val)
this.arr.sort((a, b) => a - b)
return this.arr[this.arr.length - this.k]
};
这种方法的缺点是,每次调用 add 方法插入值之后,整个数组的长度会越来越长,按题目所说,最多调用 add 方法 10^4 次,对这么长的数组就行多次排序是极其耗费性能的,我们可以用第二种方法来进行优化
思路二:维护最大 K 个数
因为题目要求的是返回第 k 大元素,所以我们只需要关注前 k 大的元素就可以了,后面多的元素我们可以直接丢掉,这样不需要每次都对这些没用的数据进行排序,只需要对前 k 个值进行排序就可以了
解题步骤
- 将
nums从大到小进行排序 - 截取排序后
nums的前k位 - 往数组中插入数据前进行判断:
- 如果数组的长度小于
k值,直接向数组中插入该值,然后对数组从大到小进行排序,返回数组的最后一个元素; - 如果数组的长度等于
k值,判断要插入的值是否大于数组的最后一个元素,如果是,将数组最后一个元素替换为插入的值,然后对数组从大到小进行排序,并返回数组的最后一个元素;如果不是,则不进行任何操作,直接返回数组的最后一个元素
解题代码
var KthLargest = function(k, nums) {
nums.sort((a, b) => b - a)
this.arr = nums.slice(0, k)
this.k = k
};
KthLargest.prototype.add = function(val) {
if (this.arr.length < this.k) {
this.arr.push(val)
this.arr.sort((a, b) => b - a)
} else if (val > this.arr[this.k - 1]) {
this.arr[this.k - 1] = val
this.arr.sort((a, b) => b - a)
}
return this.arr[this.k - 1]
};
这种方法虽然不需要对所有插入的元素进行排序,当还是需要对这 k 个元素进行排序,为什么堆的效率可以比这种方法高呢?因为堆同样也是维护 k 个元素,但是堆不需要对所有元素进行排序,每次只是父节点与左节点或者右节点进行比较,因此至少有一半的元素是不需要进行比较的。
思路三:小顶堆
小顶堆同样是维护最大的 k 个元素,堆顶元素最小,也就是第 k 大的元素,因此我们可以直接返回堆顶元素即可
本题的堆需要有 push(插入元素)、size(堆的大小)、top(返回堆顶元素,第 k 大的元素)、sortBack(向上调整堆结构)、sortFront(向下调整堆结构)五个方法
解题步骤
- 创建一个大小为
k的小顶堆 - 依次向堆中插入元素,并调整堆结构
- 如果要插入时,堆中元素的数量小于
k,则插入到堆尾,然后向上调整堆的结构 - 如果插入时,堆中元素的数量为
k,则比较要插入元素和堆顶元素的大小,如果堆顶元素大,则不做操作,如果堆顶元素小,则将堆顶元素替换为插入的值,然后向下调整堆结构 - 返回堆顶元素的值
解题代码
var KthLargest = function(k, nums) {
this.heap = new Heap(k)
while (nums.length) {
this.heap.push(nums.pop())
}
};
KthLargest.prototype.add = function(val) {
this.heap.push(val)
return this.heap.top()
};
class Heap {
constructor(k) {
this.arr = []
this.k = k
}
// 返回当前堆的大小
size() {
return this.arr.length
}
// 返回堆顶元素
top() {
return this.arr[0]
}
// 向堆中插入元素
push(val) {
if (this.size() < this.k) {
this.arr.push(val)
this.sortBack()
} else if (val > this.arr[0]) {
this.arr[0] = val
this.sortFront()
}
}
// 向上调整
sortBack() {
let i = this.arr.length - 1
while (i > 0 && this.arr[i] < this.arr[Math.floor((i - 1) / 2)]) {
[this.arr[i], this.arr[Math.floor((i - 1) / 2)]] = [this.arr[Math.floor((i - 1) / 2)], this.arr[i]]
i = Math.floor((i - 1) / 2)
}
}
// 向下调整
sortFront() {
let i = 0
while (i * 2 + 1 < this.size()) {
let temp = i
if (this.arr[i * 2 + 1] < this.arr[temp]) temp = i * 2 + 1
if (this.arr[i * 2 + 2] !== undefined && this.arr[i * 2 + 2] < this.arr[temp]) temp = i * 2 + 2
if (i === temp) break
[this.arr[i], this.arr[temp]] = [this.arr[temp], this.arr[i]]
i = temp
}
}
}