# 堆(Heap)
有时候需要按照优先级来管理一些数据的特殊队列,而不是按照队列的元素进入的先后顺序访问数据。
这种特殊队列描述为: 优先队列(Priority Queue)
例如:CPU 任务进程按照优先级进行判断那些进程优先执行。网络 QOS 按照预定义的一些优先级控制流量输入输出等等。。
# 不同实现优先队列的数据结构对比
数据结构 | 插入 | 删除 | 删除后调整位置 |
---|---|---|---|
数组 | O(1) | O(n) | O(n) |
链表 | O(1) | O(n) | O(1) |
有序数组 | O(n)或者 O(log2 n) | O(n) | O(1) |
有序链表 | O(n) | O(1) | O(1) |
二叉搜索树 | 取决树的高度 | 取决树的高度 | - |
数组
- 插入:总是在数组尾部插入。效率约等于 O(1)
- 删除:查找最大或早小关键字,效率约等于 O(n);
- 删除元素,需要进行元素移动,效率约等于 O(n);
链表
- 插入:总是在链表头部插入,效率约等于 O(1)
- 删除:查找最大或早小关键字,效率约等于 O(n);
- 删除节点,元素不需要移动,改变指针即可: O(1)
有序数组:
- 插入: 找到合适位置,效率约等于 O(n)或者 O(log2 n)
- 插入操作需要将元素移动并插入,效率约等于 O(n)
- 删除: 删除最后一个元素,效率约等于 O(1)
有序链表:
- 插入:找到合适位置,效率约等于 O(n)
- 插入找到合适位置后需要进行元素操作,效率约等于 O(1)
- 删除: 第一个元素或者最后一个元素,效率约等于 O(1)
二叉搜索树:
- 搜索树插入跟树的高度有关,效率约等于 O(log2 n)
- 删除: 最大值在最右边,最小值在最左边,跟树的高度有关。效率约等于 O(log2 n)
- 问题: 如果每次都删除最大值,或者最小值。二叉搜索树会发生倾斜。这样效率不再是一样。
# Heap 实现
思路
可以安排一种二叉树结构,最大值(最大堆:MaxHeap)或者最小值(最小堆:MinHeap)总是在树根上。
需要删除时候,只要删除树根即可!
但是需要将二叉树数据分配均匀, 可以用完全二叉树来构造堆。
任何节点值都比左右子树值大!
操作概念
shiftUP 上推操作:一个正常的堆,任何父元素优先级都大于左右子树,如果插入了一个新的元素,会导致这种平衡打破。新元素位于最后一位,这是需要一次跟父元素对比优先级,进行位置交换,直到满足堆的条件为止。这种情况新元素一直向上对比操作就称为 shiftUP
shiftDOWN: 与 shiftUP 同理,新元素位于第一个元素,一次向下进行匹配对比,直到满足堆的条件为止。
# 最大堆实现
通过完全二叉树构建,数据容器为数组。
# 最大堆操作集
- 创建空的最大堆
- 判断是否已满
- 插入元素到最大堆
- 判断最大堆是否为空
- 返回最大堆中最大元素
# 最大堆基本结构及创建
class Heap {
constructor(MaxSize = 10) {
this.data = [];
this.size = 0;
this.capacity = MAXSIZE;
// 哨兵用于判断
this.data[0] = Number.MAX_VALUE;
}
}
# 最大堆的插入
完全二叉树插入最后一位,同时需要进行调整以满足最大堆的要求。将最新插入值与父树值对比,最大值作为父树的值。依次向上判断,直到第一位为止。
- shiftDOWN 操作:
isFull() {
return this.size === this.capacity;
}
insert(data) {
if (this.isFull()) {
console.log("heap is full!");
return;
}
let i = ++this.size;
// 依次判断根节点是否小于插入值,如果小,则替换位置。
for (; this.data[Math.floor(i / 2)] < data; i = Math.floor(i / 2)) {
this.data[i] = this.data[Math.floor(i / 2)];
}
this.data[i] = data;
}
# 最大堆的删除
删除永远都是删除整个完全二叉树的树根,即第一位。删除后需要找一位替代者,将最后一位值替代树根值,然后向下调整比较,最大值作为树根的值。
- shiftDOWN 操作
isEmpty() {
return this.size === 0;
}
deleteMax() {
if (this.isEmpty()) {
console.log("heap is empty!");
return;
}
// 删除最大值,第一位
const MAXITEM = this.data[1];
// 获取最后一个元素值,将最后一个元素移动到第一位替代删除掉的值。
this.data[1] = this.data[this.size--];
// 释放内存空间
this.data.pop();
// 记录最后一个元素值最新的索引号。
let lastIndex = 1;
// 存储最新与元素比较索引值。
let child = 1;
for (; lastIndex * 2 <= this.size; lastIndex = child) {
// 左子树为2n,右子树为2n+1
child = lastIndex * 2;
// 获取最新判断值
const tmp = this.data[lastIndex];
// 获取左右子树最大值,只对比最大值进行调整。
if (this.data[child] < this.data[child + 1]) child++;
// 如果调整值小于子树,则将子树提升至父树位置。
if (tmp < this.data[child]) {
this.data[lastIndex] = this.data[child];
this.data[child] = tmp;
}
}
return MAXITEM;
}
# 最大堆的建立
给出 N 个元素,按照最大堆要求进行存放在数组中。例如,排序可以通过建立最大堆方式实现。
方法一:通过插入操作,将元素一个个插入到新的堆中,进行建立操作。时间复杂度为 O(NlogN)
方法二:在线性时间复杂度下建立堆;将 n 个元素按输入顺序存放,先满足完全二叉树的结构特性,然后调整各节点的位置,以满足堆的有序特性。
shiftDOWN 操作
make(data) {
if (!Array.isArray(data)) return;
// 初始化
this.data = [...this.data, ...data];
this.size += data.length;
for (let i = this.size; i > 1; i--) {
let childIndex;
let parentIndex = Math.floor(i / 2);
for (; parentIndex * 2 <= this.size; parentIndex = childIndex) {
const tmp = this.data[parentIndex];
childIndex = parentIndex * 2;
if (this.data[childIndex] < this.data[childIndex + 1]) childIndex++;
if (tmp < this.data[childIndex]) {
this.data[parentIndex] = this.data[childIndex];
this.data[childIndex] = tmp;
}
}
}
}
# 示例代码
/**
* 堆操作
*
* compare: Function 比较操作,可以用于最大堆或最小堆等建立
*
* data: T[] 堆初始化数据
*/
class Heap<T> {
private size: number = 0;
constructor(private data: T[], private compare: (a: T, b: T) => boolean) {
this.size = this.data.length;
}
/**
* 返回堆大小
*/
public getSize(): number {
return this.size;
}
/**
* 判断堆是否为空
*/
public isEmpty(): boolean {
return this.size === 0;
}
/**
* 添加新元素
* @param value 添加的元素
*/
public add(value: T) {
this.data.push(value);
this.size++;
// 增加元素进行shiftUP 操作
this._shiftUP(this.getSize() - 1);
}
/**
* 查找优先级最高元素
*/
public findMax(): T | undefined {
if (this.getSize() === 0) return;
return this.data[0];
}
/**
* 队首进行出队(优先级最高)
*/
public extractMax(): T {
// 获取队首
let ret = this.findMax();
// 交换队尾和队首
this._swap(0, this.getSize() - 1);
// 移除队尾,交换后的队尾元素
this.data.pop();
this.size--;
// 新队首进行shiftDOWN调整
this._siftDOWN(0);
return ret;
}
toString() {
console.log(this.data);
}
/**
* 交换两个元素位置
* @param i 元素索引
* @param j 元素索引
*/
private _swap(i: number, j: number) {
[this.data[i], this.data[j]] = [this.data[j], this.data[i]];
}
/**
* 返回父元素索引值
* @param index 索引
*/
private _parent(index: number) {
return Math.floor((index - 1) / 2);
}
/**
* 返回指定索引左子树的索引值
* @param index 索引
*/
private _leftChild(index: number) {
return 2 * index + 1;
}
/**
* 返回指定索引的右子树索引值
* @param index 索引
*/
private _rightChild(index: number) {
return 2 * index + 2;
}
/**
* 将指定索引的值进行 shiftUP 调整
* @param k 索引值
*/
private _shiftUP(k: number) {
// shiftUP 调整操作
while (k > 0 && this.compare(this.data[k], this.data[this._parent(k)])) {
this._swap(k, this._parent(k));
k = this._parent(k);
}
}
/**
* 将指定索引的值进行 shiftDOWN 调整
* @param k 索引值
*/
private _siftDOWN(k: number) {
// shiftDOWN 调整操作
// 先判断左子树,然后判断右子树,那个优先级高操作哪个子树
while (this._leftChild(k) < this.size) {
let j = this._leftChild(k);
if (
this._rightChild(k) < this.size &&
this.compare(this.data[this._rightChild[k]], this.data[j])
) {
j++;
}
// 正常无需调整
if (this.compare(this.data[k], this.data[j])) return;
// 交换位置
this._swap(k, j);
// 继续下一层对比操作
k = j;
}
}
}