堆
最近在leetcode上遇到topK问题,但是js中没有堆这种数据结构,因此做这种题需要自己实现一个最大堆或最小堆。在此记录一下堆的相关知识点和js实现。
堆基本概念
- 堆是一种特殊的二叉树。它的底层是一颗完全二叉树。一般用堆实现求最值和topk问题。
- 最小堆:对于指定节点,它的值都小于等于其子节点,所以最小堆的根节点是最小值。
- 最大堆:对于指定节点,它的值都大于等于其子节点,所以最大堆的根节点是最大值。
堆的数组表示
一般基于数组实现堆,下图是一个最小堆映射至数组的示例:
二叉堆从上到下,从左到右的顺序映射至数组,可以总结以下规律, 对于给定位置index的节点:
- 它的左侧子节点的位置是2 * index + 1(如果位置可用);
- 它的右侧子节点的位置是2 * index + 2(如果位置可用);
- 它的父节点位置是Math.floor((index - 1) / 2)(如果位置可用)。
使用js基于数组实现最小堆
堆常用的3个方法:
- 添加元素:offer()
- 删除堆顶元素: poll()
- 查看堆顶元素(堆顶元素就是最大/最小值):peek() tips:如果要解决topK问题,添加元素时需要判断堆的size是否超过k,其次是堆中如果存储了其他类型的数据,如map或者对象,只需要改造一下类中比较函数的实现。
以下基于最小堆,理解添加和删除操作中的两个重要过程。
理解添加元素时的上移操作过程:
下图展示的是新增元素1插入堆,并从最底层上移到堆顶的过程。因为1是堆中最小元素,最后上移到了最小堆的堆顶。
- 添加元素时,应该从上到下,从左到右找出第一个空缺位置,将新节点添加进去,数组中就是末尾添加元素。
- 插入空缺位置后,找到其父节点,如果新节点比其父节点的值小,则交换二者位置。
- 重复步骤2,直到满足下列条件之一:
-
3.1 新节点的值大于等于其父节点的值
-
3.2 新节点到达堆顶位置
-
// 上移操作
siftUp(index) {
// 找到父节点位置
let parentNodeIndex = this.getParentNodeIndex(index);
// 比较插入节点与父节点值,如果父节点值大于插入节点,就交换位置。
while(index > 0 && this.compareFn(this.heap[parentNodeIndex], this.heap[index]) > 0) {
this.swap(this.heap, parentNodeIndex, index);
index = parentNodeIndex;
parentNodeIndex = this.getParentNodeIndex(index);
}
}
理解删除堆顶元素时的下移操作过程:
下图展示了从最小堆中删除堆顶节点1,堆最底层最右边的节点9被移至堆顶,然后逐步从堆顶下移到最底层的过程。
- 堆中删除元素一般指删除堆顶元素。删除堆顶元素(数组中第一个元素),并将堆最底层最右边的节点移到堆顶部。
- 比较堆顶元素与其左右子节点的值,如果堆顶值大于它的左右子节点的值,那么将堆顶元素与其左右子节点的较小值交换。
- 如果交换后节点值仍然大于其子节点的值,则继续交换。直到满足以下条件之一停止:
-
3.1 节点值小于等于其左右子节点
-
3.2 到达最底层位置
-
// 下移操作
siftDown(index) {
// 记录下移元素的位置
let curIndex = index;
// 下移元素目前的左右子节点
const leftNodeIndex = this.getLeftNodeIndex(index);
const rightNodeIndex = this.getRightNodeIndex(index);
// 最底层位置也就是数组的大小
const size = this.size();
// 以下两个判断可以得到左右子节点中的较小者
if (leftNodeIndex < size && this.compareFn(this.heap[curIndex], this.heap[leftNodeIndex]) > 0) {
curIndex = leftNodeIndex;
}
if (rightNodeIndex < size && this.compareFn(this.heap[curIndex], this.heap[rightNodeIndex]) > 0) {
curIndex = rightNodeIndex;
}
// 判断最终是否需要调整
if (index !== curIndex) {
this.swap(this.heap, index, curIndex);
// 相当于重复步骤2
this.siftDown(curIndex);
}
}
完整代码
class MinHeap {
constructor() {
this.heap = [];
this.compareFn = (a, b) => a - b;
}
// 向堆中插入新值
offer(value) {
if (value != null) {
this.heap.push(value);
this.siftUp(this.heap.length - 1);
return true;
}
return false;
}
// 获取最小值:堆顶元素
peek() {
return this.isEmpty() ? undefined : this.heap[0];
}
//堆中的删除:一般删除堆顶元素
poll() {
if (this.isEmpty()) {
return undefined;
}
if (this.size() === 1) {
return this.heap.shift();
}
// 获取堆顶元素
const removedVal = this.heap[0];
//将堆中最后一个元素移至堆顶
this.heap[0] = this.heap.pop();
// 堆顶元素下移
this.siftDown(0);
return removedVal;
}
size() {
return this.heap.length;
}
isEmpty() {
return this.size() === 0;
}
// 上移操作
siftUp(index) {
let parentNodeIndex = this.getParentNodeIndex(index);
while(index > 0 && this.compareFn(this.heap[parentNodeIndex], this.heap[index]) > 0) {
this.swap(this.heap, parentNodeIndex, index);
index = parentNodeIndex;
parentNodeIndex = this.getParentNodeIndex(index);
}
}
// 下移操作
siftDown(index) {
let curIndex = index;
const leftNodeIndex = this.getLeftNodeIndex(index);
const rightNodeIndex = this.getRightNodeIndex(index);
const size = this.size();
if (leftNodeIndex < size && this.compareFn(this.heap[curIndex], this.heap[leftNodeIndex]) > 0) {
curIndex = leftNodeIndex;
}
if (rightNodeIndex < size && this.compareFn(this.heap[curIndex], this.heap[rightNodeIndex]) > 0) {
curIndex = rightNodeIndex;
}
if (index !== curIndex) {
this.swap(this.heap, index, curIndex);
this.siftDown(curIndex);
}
}
getLeftNodeIndex(index) {
return 2 * index + 1;
}
getRightNodeIndex(index) {
return 2 * index + 2;
}
getParentNodeIndex(index) {
if (index === 0) return undefined;
return Math.floor((index - 1) / 2);
}
swap(arr, index1, index2) {
const temp = arr[index1];
arr[index1] = arr[index2];
arr[index2] = temp;
}
}
const heap = new MinHeap();
heap.offer(6)
heap.offer(7)
heap.offer(3)
heap.offer(4)
heap.offer(2)
heap.offer(5)
heap.offer(1)
console.log(heap);
console.log(heap.peek()); // 1
console.log(heap.poll()); // 1