- 堆是一种特殊的完全二叉树
- 所有的节点都大于等于(最大堆),或者小于等于(最小堆)它的子节点
- JS 中通常用数组表示堆
- 左侧子节点的位置是 2 * ndex + 1
- 右侧子节点的位置是 2 * index + 2
- 父节点位置是 (index -1) / 2
堆的应用
-
堆能高效、快速的找出最大值和最小值,因为时间复杂度是 O(1)
-
找出第 K 个最大(小)元素
JS实现最小堆
class MinHeap {
constructor() {
this.heap = [];
}
// 交换节点的值
swap(i1, i2) {
[this.heap[i1], this.heap[i2]] = [this.heap[i2], this.heap[i1]];
}
// 获取父节点
getParentIndex(i) {
return Math.floor((i - 1) / 2);
// 等价于
// return (index - 1) >> 1;
}
// 获取左侧节点索引
getLeftIndex(i) {
return i * 2 + 1;
}
// 获取右侧节点索引
getRightIndex(i) {
return i * 2 + 2;
}
// 上移
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);
}
}
// 下移
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);
}
}
// 插入节点
insert(value) {
this.heap.push(value);
// 上移: 将这个值和它的父节点进行交换,直到父节点小于等于这个插入的值
this.shiftUp(this.heap.length - 1);
}
// 删除堆项
pop() {
// 把数组最后一位 转移到数组头部
this.heap[0] = this.heap.pop();
// 进行下移操作
this.shiftDown(0);
}
// 获取堆顶元素
peek() {
return this.heap[0];
}
// 获取堆大小
size() {
return this.heap.length;
}
}
LeetCode:215.数组中的第 K 个最大元素
解题思路
- 看到“第 K 个最大元素”
- 考虑选择使用最小堆
解题步骤
- 构建一个最小堆,并依次把数组的值插入堆中
- 当堆的容量超过K,就删除堆顶
- 插入结束后,堆顶就是第 K 个最大元素
// 时间复杂度 O(n * logK)
// 空间复杂度 O(K) K就是堆的大小
const findKthLargest = function (nums, k) {
// 使用上面js实现的最小堆类,来构建一个最小堆
const h = new MinHeap();
nums.forEach(item => {
h.insert(item);
// 当堆的大小超过k,进行优胜劣汰
if (h.size() > k) {
h.pop();
}
})
// 返回堆顶
return h.peek();
};
LeetCode:347.前 K 个高频元素
// 时间复杂度 O(n * logn)
// 空间复杂度 O(K)
const topKFrequent = (nums, k) => {
const map = new Map();
// 记录元素出现的次数
nums.forEach(n => {
map.set(n, map.has(n) ? map.get(n) + 1 : 1);
})
// 依据出现次数降序排序
const list = Array.from(map).sort((a, b) => b[1] - a[1]);
// 截取前 k 个元素
return list.slice(0, k).map(n => n[0])
}
使用堆解:
// 时间复杂度 O(n * logK)
// 空间复杂度 O(K)
const 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((value, key) => {
// 由于插入的元素结构发生了变化,所以需要对最小堆的类 进行改造一下,改造上移和下移操作即可,详见后面注释内容
h.insert({ key, value });
if(h.size() > k) {
h.pop();
}
})
return h.heap.map(item => item.key);
}
// shiftUp(index) {
// if (index == 0) return;
// const parentIndex = this.getParentIndex(index);
// if (this.heap[parentIndex] && this.heap[parentIndex].value > this.heap[index].value) {
// this.swap(parentIndex, index);
// this.shiftUp(parentIndex);
// }
// }
// shiftDown(index) {
// const leftIndex = this.getLeftIndex(index);
// const rightIndex = this.getRightIndex(index);
// if (this.heap[leftIndex] && this.heap[leftIndex].value < this.heap[index].value) {
// this.swap(leftIndex, index);
// this.shiftDown(leftIndex)
// }
// if (this.heap[rightIndex] && this.heap[rightIndex].value < this.heap[index].value) {
// this.swap(rightIndex, index);
// this.shiftDown(rightIndex)
// }
// }
LeetCode:23.合并K个排序链表
解题思路:
- 新链表的下一个节点一定是 k 个链表头中的最小节点
- 考虑使用最小堆
解题步骤:
- 构建一个最小堆,并依次把链表头插入堆中
- 弹出堆顶接到输出链表,并将堆顶所在链表的新链表头插入堆中
- 等堆元素全部弹出,合并工作就完成了
// 时间复杂度 O(n * logK) K是链表,n为所有链表的节点数之和
// 空间复杂度 O(K) K是堆的大小
const mergeKLists = function(lists) {
// 创建新链表及指针和最小堆
const res = new ListNode(0);
let p = res;
const h = new MinHeap();
// 将链表头部节点插入堆中
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;
};
// 改造下最小堆的上移下移和删除的操作
// shiftUp(index) {
// if(index === 0) return;
// const parentIndex = this.getParentIndex(index);
// if(this.heap[parentIndex] && this.heap[parentIndex].val > this.heap[index].val) {
// this.swap(parentIndex, index);
// this.shiftUp(parentIndex);
// }
// }
// shiftDown(index) {
// const leftIndex = this.getLeftIndex(index);
// const rightIndex = this.getRightIndex(index);
// if(this.heap[leftIndex] && this.heap[leftIndex].val < this.heap[index].val) {
// this.swap(leftIndex, index);
// this.shiftDown(leftIndex);
// }
// if(this.heap[rightIndex] && this.heap[rightIndex].val < this.heap[index].val) {
// this.swap(rightIndex, index);
// this.shiftDown(rightIndex);
// }
// }
// pop() {
// if(this.size() === 1) return this.heap.shift();
// const top = this.heap[0];
// this.heap[0] = this.heap.pop();
// this.shiftDown(0);
// return top;
// }