堆
-
堆是一种特殊的完全二叉树
- 完全二叉树:每层节点都完全填满;最后一层,如果不是满的,则缺少右边的若干节点
-
堆:所有节点都大于等于(最大堆)或小于等于(最小堆)它的子节点
- 可见堆是一种有顺序的数据结构
- 如下图所示
-
最大堆示例
6 / \ 5 3 / \ / 4 2 1
-
最小堆示例
1 / \ 3 2 / \ / 4 6 5
-
堆的规律
- 一个堆,如果按广度优先遍历就是 0 1 2 3 4 5,我们把0 ~ 5作为数组的下标
- 并把节点中的值填到数组中,如此一来就可以把数组来表示一个堆了,不仅如此有公式可以获取子节点和父节点的位置关系
- 左侧子节点的位置是 2 * index + 1
- 右侧子节点的位置是 2 * index + 2
- 父节点的位置是 (index - 1) / 2 的商
-
在前端js中表示堆的示例: 这是一个最小堆的结构
1 / \ 3 6 / \ / 5 9 8
-
给这个最小堆标记下索引(下标),如下
1(0) / \ 3(1) 6(2) / \ / 5(3) 9(4) 8(5)
-
上图括号中的就是在堆中的下标
-
这个最小堆在js中的表示形式是:[1,3,6,5,9,8], 数组中对应的各个元素的下标:0,1,2,3,4,5
- 这个规律就是按照广度优先遍历来走的
- 也就是从上到下,从左到右
-
堆的应用
- 快速高效找出最大值和最小值
- 时间复杂度O(1)
- 找出第k个最大(小)元素
-
解决第k个最大元素
- 构建一个最小堆,并将元素依次插入堆中
- 当堆的容量超过K,就删除堆顶
- 当插入结束后,堆顶就是第k个最大元素
-
如果要找到第k个最小元素,反过来就行,道理一致
实现一个最小堆类
- 在类里,声明一个数组,用来装元素
- 主要方法:插入,删除堆顶、获取堆顶、获取堆大小
- 插入
- 将值插入堆的底部,即数组的尾部
- 然后上移:将这个值和它的父节点进行交换换,直到父节点小于等于这个插入的值
- 大小为k的堆中插入元素的时间复杂度为O(logk)
- 复杂度主要体现在上移操作中,最多循环的次数是堆的高度,因为它不断上移
- 这个堆的高度也就是二叉树的高度,二叉树的高度和树节点数量的关系是logk
- 所以时间复杂度是O(logk)
- 删除堆顶
- 用数组尾部元素替换堆顶(直接删除堆顶会破坏堆结构)
- 然后下移,将新堆顶和它的子节点进行交换,直到子节点大于等于这个新堆顶
- 大小为k的堆中删除堆顶的事件复杂度为O(logk)
- 和插入一样的道理,下移次数最大是堆的高度,所以时间复杂度也是logk
- 获取堆顶:返回数组的头部
- 获取堆的大小:返回数组的长度
class MinHeap {
heap: number[] = []
// 获取父节点
getParentIndex(i) {
// return Math.floor(i - 1) / 2; // 这样做代码比较多
return (i - 1) >> 1; // 比较极客的写法:把一个二进制的数字右移一位;往右移一位,就是除以2,就是取得商
}
// 获取左子节点
getLeftIndex(i) {
// return i * 2 + 1; // 老的写法
return (i << 1) + 1; // 极客写法
}
// 获取右子节点
getRightIndex(i) {
// return i * 2 + 1;
return (i << 1) + 2;
}
// 两数交换
swap(i, j) {
// 写法1, 比较繁琐
// let temp = this.heap[i];
// this.heap[i] = this.heap[j];
// this.heap[j] = temp;
// 写法3 对象结构,别名的方式
// let a = 1,b = 2;
// {a:b,b:a} = {a,b}
// 写法2 通过数学运算加减关系,利用两个数字在坐标轴上的距离固定来交换
// let a = 1,b = 2;
// b = b - a; // b(原) - a(原)
// a = a + b; // a(原) + b(原) - a(原)
// b = a - b; // b(原) - (b(原) - a(原))
// console.log(a); //2
// console.log(b); //1
// 使用es6解构数组赋值来交换两数
[this.heap[j], this.heap[i]] = [this.heap[i], this.heap[j]];
}
// 上移操作封装 是个递归
shiftUp(i) {
// 如果到了堆顶元素,index是0,则不要再上移了
if(!i) return;
// 获得父节点下标: pi
const pi = this.getParentIndex(i);
// 开始做比较
if(this.heap[pi] > this.heap[i]) {
// 实现交换
this.swap(pi, i);
// 继续尝试上移操作
this.shiftUp(pi);
}
}
// 下移操作
shiftDown(i) {
// 如果到了堆尾元素,则不要再下移了
if(i >= this.heap.length - 1) return;
const size = this.size(); // 获取当前堆长度
const li = this.getLeftIndex(i); // 左孩子索引
const ri = this.getRightIndex(i); // 右孩子索引
const cur = this.heap[i]; // 堆的当前元素
// 左孩子节点的值 < 当前节点的值
if(li < size && this.heap[li] < cur) {
this.swap(li, i);
this.shiftDown(li);
}
// 同样,对右孩子节点的值 < 当前节点的值
if(ri < size && this.heap[ri] < cur) {
this.swap(ri, i);
this.shiftDown(ri);
}
}
// 添加
insert(value) {
this.heap.push(value);
// 保证堆的结构是正确的,要进行上移的操作,把该值移动到合适的位置
this.shiftUp(this.heap.length - 1); // 这个方法封装上移操作,参数是接受一个上移值的下标
}
// 删除
pop() {
// 变相删除堆顶, 堆尾元素移动到堆顶
this.heap[0] = this.heap.pop(); // 删除数组的最后一个元素并返回,返回值赋值给堆顶元素
// 执行堆顶下移操作,维持堆的有序性
this.shiftDown(0);
}
// 获取堆顶
peak() {
return this.heap[0];
}
// 获取堆大小
size() {
return this.heap.length;
}
// 输出最后的堆结构, 有参,输出参数;无参,输出堆结构
print(o) {
console.log(o ? o : this.heap);
}
}
// 使用
const h = new MinHeap();
h.print();
h.insert(3);
h.insert(2);
h.insert(1);
h.print(); // [1,3,2]
h.pop();
h.print(); // [2,3]
h.print(h.peak()) // 2
h.print(h.size()) // 2