一、理论
1. 堆简介
- 堆是一种特殊的 完全二叉树
- 所有的节点都大于等于(最大堆)或小于等于(最小堆)它的子节点
1.1 js中的堆
- js中通常用数组表示堆
- 左侧子节点的位置是 2*index+1
- 右侧子节点的位置是 2*index+2
- 父节点位置是(index-1)/2
1.2 应用
- 堆能高效、快速找出最大值和最小值,时间复杂度O(1)
- 找出第k个最大(小)问题
2. js实现最小堆类
2.1 实现步骤
- 在类里声明一个数组,用来装元素
- 主要方法:插入、删除堆顶、获取堆顶、获取堆大小
class MinHeap {
constructor() {
this.heap = [];
}
}
插入
- 将值插入堆的底部,即数组尾部
- 然后上移:将该值与父节点交换,知道父节点小于等于该值
- 大小为k的堆中插入元素的时间复杂度为O(logk)
class MinHeap {
constructor() {
this.heap = [];
}
swap(i1, i2) {
const temp = this.heap[i1]
this.heap[i1] = this.heap[i2]
this.heap[i2] = temp
}
getParentIndex(i) {
// return Math.floor((i-1)/2);
return (i - 1) >> 1;
}
shiftUp(index) {
if(index === 0) return;
const parentIndex = this.getParentIndex(index);
if(this.heap[parentIndex] > this.heap[index]) {
this.swap(parentIndex, index);
this.shiftUp(parentIndex);
}
}
insert(value) {
this.heap.push(value);
this.shiftUp(this.heap.length-1);
}
}
删除堆顶
- 用数组尾部元素替换堆顶(直接删除堆顶会破坏堆结构)
- 然后下移:将新堆顶和它的子节点进行交换,直到子节点大于等于新堆顶
- 大小为k的堆中删除堆顶的时间复杂度为O(logk)
class MinHeap {
constructor() {
this.heap = [];
}
swap(i1, i2) {
const temp = this.heap[i1]
this.heap[i1] = this.heap[i2]
this.heap[i2] = temp
}
getLeftIndex(i) {
return i * 2 + 1
}
getRightIndex(i) {
return i * 2 + 2
}
shiftDown(index) {
const leftIndex = this.getLeftIndex(index)
const rightIndex = this.getRightIndex(index)
if(this.heap[leftIndex] < this.heap[index]) {
this.swap(leftIndex, index)
this.shiftDown(leftIndex)
}
if(this.heap[rightIndex] < this.heap[index]) {
this.swap(rightIndex, index)
this.shiftDown(rightIndex)
}
}
pop() {
this.heap[0] = this.heap.pop()
this.shiftDown(0)
}
}
获取堆顶和堆大小
- 获取堆顶:返回数组的头部
- 获取堆的大小:返回数组的长度
class MinHeap {
constructor() {
this.heap = [];
}
peek() {
return this.heap[0]
}
size() {
return this.heap.length
}
}
二、刷题
1. 数组中的第k个最大元素(215)
1.1 题目描述
- 给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素
- 请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素
1.2 解题思路
- 看到 第k个最大元素 --> 考虑选择使用最小堆
1.3 解题步骤
- 构建一个最小堆,并依次把数组的值插入堆中
- 当堆的容量超过k,删除堆顶
- 插入结束后,堆顶就是第k个最大元素
function findKthLargest(nums, k) {
const h = new MinHeap()
nums.forEach(n => {
h.insert(n)
if(h.size() > k) {
h.pop()
}
})
return h.peek()
}
1.4 时间复杂度 && 空间复杂度
- 时间复杂度:O(nlogk)
- 空间复杂度:O(k)
2. 前k个高频元素(347)
2.1 题目描述
- 给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素
- 可以按 任意顺序 返回答案
直觉解法
function topKFrequent(nums, k) {
const map = new Map()
nums.forEach(n => {
map.set(n, map.has(n) ? map.get(n)+1 : 1)
})
const list = [...map].sort((a, b) => b[1] - a[1])
return list.slice(0, k).map(n => n[0])
}
- 时间复杂度:O(n)
- 空间复杂度:O(nlogn)
2.2 解题思路
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
2.3 解题步骤
function topKFrequent(nums, k) {
const map = new Map()
nums.forEach(n => {
map.set(n, map.has(n) ? map.get(n)+1 : 1)
})
const h = new MinHeap()
map.forEach((val, key) => {
h.insert({ val, key })
if(h.size() > k) {
h.pop()
}
})
return h.heap.map(a => a.key)
}
2.4 时间复杂度 && 空间复杂度
- 时间复杂度:O(nlogk)
- 空间复杂度:O(n)
3. 合并k个排序链表(23)
3.1 题目描述
- 给你一个链表数组,每个链表都已经按升序排列
- 请你将所有链表合并到一个升序链表中,返回合并后的链表
3.2 解题思路
输入: lists = [ 1 -> 4 -> 5, 1 -> 3 -> 4, 2 -> 6 ]
输出:[1,1,2,3,4,4,5,6]
- 新链表的下一个节点一定是k个链表头中的最小节点 -> 考虑选择使用最小堆
3.3 解题步骤
- 构建最小堆,并依次把链表头插入堆中
- 弹出堆顶接到输出链表,并将堆顶所在链表的新链表头插入堆中
- 等堆元素全部弹出,合并工作就完成了
function mergeKLists(lists) {
const res = new ListNode(0)
const h = new MinHeap()
let p = res
lists.forEach(l => {
if(l) h.insert(l)
})
while(h.size()) {
const n = h.pop()
p.next = n
p = p.next
if(n.next) h.insert(n.next)
}
return res.next
}
3.4 时间复杂度 && 空间复杂度
- 时间复杂度:O(nlogk)
- 空间复杂度:O(k)
三、总结 -- 技术要点
-
堆是一种特殊的 完全二叉树
-
所有的节点都大于等于(最大堆)或小于等于(最小堆)它的子节点
-
js通常用数组表示堆
-
堆能高效、快速找出最大值和最小值,时间复杂度O(1)
-
找出第k个最大(小)问题