前端算法面试必刷题系列[56]

260 阅读3分钟

这个系列没啥花头,就是纯 leetcode 题目拆解分析,不求用骚气的一行或者小众取巧解法,而是用清晰的代码和足够简单的思路帮你理清题意。让你在面试中再也不怕算法笔试。

108. 数据流中的第 K 大元素 (kth-largest-element-in-a-stream)

标签

  • 优先级队列
  • 简单(针对有优先队列 api 的语言)

题目

leetcode 传送门

这里不贴题了,leetcode打开就行,题目大意:

设计一个找到数据流中第 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

基本思路

这个问题用其他语言实现比较简单,js 的话要绕一点,用优先级队列思想,其中一种实现方式就是

因为数据流是变化的,会有数加进来,但我们始终关心前 k 大的数,那么建立一个小顶堆,堆顶就是第 k 个大的数,我们只需要维护这个堆(长度为 k)即可。

add 方法的关键:

  • 当 heap 数组长度小于 k 时,新数从数组末尾推入上浮跟父元素比较,交换到它合适的位置。
  • 当 heap 数组长度大于等于 k 时,如果新数字大于栈顶元素,用它替换堆顶下沉跟左右孩子比较,最终交换到合适的位置。

最后这个堆存的是前 k 大的数字,堆顶是第 k 大的数字,因为是小顶堆,也就是这个堆的最小值。

写法实现

class MinHeap {

  constructor(k, nums) {
    // 优先级队列数组,heap 是个 二叉小顶堆 为了计算方便
    this.heap = [];
    this.k = k;
  }

  // 堆的插入
  add(val) {
    // 最小堆没满,先入堆
    if (this.heap.length < this.k) {
    	// 入堆后在数组末尾 index = this.heap.length - 1
    	this.heap.push(val)
    	// 上浮操作,让小顶堆成立
    	this.up(this.heap.length - 1)
    } else if (val > this.heap[0]) {
    	// 如果这个元素比堆顶大,就把顶换掉,因为顶原来是第 k 个大的,
    	// 现在比他大的来了一个,他就排到第 k + 1个大的了,被挤出了这个小顶堆
    	this.heap[0] = val
    	this.down(0)
    }
    // 否则直接丢弃这个元素,因为它比最小堆顶还小,说明入不了前 k 个大的元素了
  }

  // 由底比较 上浮操作
  up(idx) {
    // 因为是二叉堆,计算出堆的父节点的 index = parent
    let parent = Math.floor((idx - 1) / 2);
    // 当发现 父节点大于 idx 节点 上浮
    if (parent >= 0 && this.heap[parent] > this.heap[idx]) {
    // 交换 parent 和 idx 位置
      [this.heap[parent], this.heap[idx]] = [this.heap[idx], this.heap[parent]];
      // 继续递归上浮
      this.up(parent);
    }
  }

  // 由顶比较 下沉操作
  down(idx) {
    let tempIdx = idx;
    // 左孩子 idx = idx * 2 + 1
    let left = idx * 2 + 1;
    // 比较左孩子,如果发现比左孩子大,换位置
    if (left < this.heap.length && this.heap[left] < this.heap[tempIdx]) {
      tempIdx = left;
    }
    let right = idx * 2 + 2;
    // 右边同理
    if (right < this.heap.length && this.heap[right] < this.heap[tempIdx]) {
      tempIdx = right;
    }
    // tempIdx 现在是左右孩子中小的那个,或者都比根大(tempIdx 仍然为 idx) 如果有改动,则交换节点位置
    if (tempIdx !== idx) {
      [this.heap[tempIdx], this.heap[idx]] = [this.heap[idx], this.heap[tempIdx]];
      // 继续递归
      this.down(tempIdx);
    }
  }
}

// 声明全局变量
let minHeap = []

var KthLargest = function (k, nums) {
  // 建立一个小顶堆 赋给全局变量
  minHeap = new MinHeap(k, nums);
  // 所有元素都添加到最小堆
  for (let i = 0; i < nums.length; i++) {
    minHeap.add(nums[i]);
  }
};

KthLargest.prototype.add = function (val) {
  minHeap.add(val);
  // 返回堆顶元素,就是第 k 大的元素
  return minHeap.heap[0];
};

let kthLargest = new KthLargest(3, [8, 4, 5, 2]);
console.log(kthLargest.add(3)) // return 4
console.log(kthLargest.add(5)) // return 5
console.log(kthLargest.add(10)) // return 5
console.log(kthLargest.add(9)) // return 8
console.log(kthLargest.add(4)) // return 8

另外向大家着重推荐下这个系列的文章,非常深入浅出,对前端进阶的同学非常有作用,墙裂推荐!!!核心概念和算法拆解系列

今天就到这儿,想跟我一起刷题的小伙伴可以加我微信哦 点击此处交个朋友 Or 搜索我的微信号infinity_9368,可以聊天说地 加我暗号 "天王盖地虎" 下一句的英文,验证消息请发给我 presious tower shock the rever monster,我看到就通过,加了之后我会尽我所能帮你,但是注意提问方式,建议先看这篇文章:提问的智慧

参考