哈喽,今天在这里总结一下力扣上遇到的返回第K个最大或者最小元素的问题
首先先看看力扣上的: 215. 数组中的第K个最大元素
返回数组中的第K个最大的元素,那通过按大小排序,返回K-1的下标即可拿到
//暴力解
var findKthLargest = function(nums, k) {
nums.sort((a,b)=>b-a)
return nums[k-1]
};
可以看到这里的暴力解是可以通过力扣的测试用例的
那我们再看看另一道题: 703. 数据流中的第 K 大元素
题意大致上就是说每次对数组进行一次添加元素的操作,返回该操作后的第K大的元素,相较于上面一题,多了一步添加的操作
那这里我就可以这样进行暴力解,对每一次添加的时候都进行一次排序,那每次通过访问下标k-1即可得到第K大的元素了
var KthLargest = function(k, nums) {
this.arr = [...nums]
this.k = k
};
/** * @param {number}
val * @return {number} */
KthLargest.prototype.add = function(val) {
this.arr.push(val)
let arr = this.arr.sort((a,b)=>b-a)
return arr[this.k-1]
};\
很显然当add操作很多时,这种解法的耗时就非常高了
这类题,通过了解都可以用到一种叫大(小)顶堆的解决方式
大(小)顶堆是一种经过排序的完全二叉树的形式,那什么是完全二叉树呢
因为完全二叉树的叶子节点的位置比较规律,这里问对二叉树的理解还没有很多,暂时不展开说明,即可理解为如成下图的二叉树,这里是最小堆堆模拟
如果我们进行插入节点(比如插入的是值为5的节点),为了不破坏原先的结构,则进行尾部插入,然后不断向上进行判断是否需要交换节点
理论上是这样进行的,但是这里会遇到一个困难,就是如果获取节点的父节点呢
根据表格中数据可观察出以下规律:
左子节点:leftIndex
右子节点:rightIndex
父节点:parentIndex
leftIndex = (parentIndex + 1) * 2 - 1
rightIndex = leftIndex + 1
parentIndex = leftIndex + 1 >>> 1 或者 ( (leftIndex + 1) /2 需要做小数点都向下 取整)
这里解决了各个节点的获取,那节点的删除与插入是差不多的,插入是向上迭代,那删除就是向下调整,可以取队尾的元素替换到堆顶上进行向下的调整
有了这写前提就可以对703 进行编码
// 定义类
class MinHeap {
// 初始化data 是数组
constructor(data){
this.data = data || [];
}
// 获取data 长度
size(){
return this.data.length;
}
// 获取最小值,因为是升序长度为k数组所以第一个的元素肯定是第K大的
peek(){
return this.size() === 0 ? null : this.data[0];
}
//比较元素大小
compare(a,b){
return a-b;
}
//添加操作
push(node){
this.data.push(node);
//传入最新添加的元素与该元素在数组的下标 进行向上调整
this.changeUp(node,this.size() - 1);
}
//向上调整的方法
changeUp(node,i){
let index = i;
while( index > 0){
//寻找父节点的下标
let paretIndex = (index - 1) >>> 1;
//获取父节点的值
let parent = this.data[paretIndex];
//当前节点与父节点比较
if(this.compare(node,parent) < 0){
//当前节点小与父节点,则进行节点的交换
this.swap(index,paretIndex);
//交换结束后,由于该节点已经移动到了父节点的位置,所以将父节点的位置继续往上比较调整
index = paretIndex;
}else{
break;
}
}
}
// 删除元素
pop(){
// 如果数组还存在元素
if(this.size() !== 0){
//弹出数组最后一个元素
let last = this.data.pop();
//将堆顶替换成最后一个元素
this.data[0] = last;
//进行向下调整
this.changeDown(last,0)
}
return null
}
//向下调整的方法
changeDown(node,i){
let index = i;
let length = this.size();
//因为比较的永远是一半的节点,所以只需要需取一半的长度即可
let halfLen = length >>> 1;
while(index < halfLen){
// 获取左子节点,与右子节点的值
let leftIndex = (index+1) * 2 - 1,left = this.data[leftIndex];
let rightIndex = leftIndex + 1,right = this.data[rightIndex];
if(this.compare(left,node) < 0){
// 如果左子节点的值小于父节点的值
if(rightIndex < length && this.compare(right,left) < 0){
//左子节点的值大于右节点的值 交换右节点
this.swap(rightIndex,index)
index = rightIndex
}else{
//左子节点的值小于右节点的值 交换左节点
this.swap(leftIndex,index)
index = leftIndex
}
}else if(rightIndex < length && this.compare(right,node) < 0){
// 因为上面已经做了左子节点跟父节点的判断,所以右子节点小于父节点的时候 左节点肯定是大于右节点的 可以直接进行交换
this.swap(rightIndex,index)
index = rightIndex
}else{
// 父节点最小跳出循环
break;
}
}
}
//交换元素
swap(i,j){
[this.data[i],this.data[j]] = [this.data[j],this.data[i]];
}
}
var KthLargest = function(k, nums) {
this.k = k;
this.heap = new MinHeap()
//依次添加保证堆的长度与有序性
for(let node of nums){
this.add(node)
}
};
/**
* @param {number} val
* @return {number}
*/
KthLargest.prototype.add = function(val) {
this.heap.push(val);
//维护堆的大小一直为k个 超出k个就进行删除操作
if(this.heap.size() > this.k){
this.heap.pop()
}
return this.heap.peek()
};
这种解法对代码量与复杂度相较于暴力解来说是增加了很多的,但在得到的回馈也是正向增长的