// 堆的底层实际上是一棵完全二叉树。
// 每个的节点元素值不小于其子节点 - 最大堆 type = max
// 每个的节点元素值不大于其子节点 - 最小堆 type = min
// 可以用数组实现
// 父节点 i 的左子节点在位置 2 * i + 1
// 父节点 i 的右子节点在位置 2 * i + 2
// 子节点 i 的父节点在位置 Math.floor((i -1) / 2)
class Heap {
constructor(values = [], type = 'max') {
[this.data, this.type] = [values, type]
Object.defineProperties(this, {
// 长度
length: {
configurable: false,
enumerable: true,
get() {
return this.data && this.data.length || 0
},
set() {
return console.warn(`property length is readonly`);
}
}
})
if(this.length) this.create()
}
create() {
// 得到第一个非叶子节点
const notLeafNode = Math.floor(this.data.length / 2) - 1
// 以这个节点为起点开始调整节点位置
for(let i = notLeafNode; i >=0; i--) {
this.adjust(i)
}
}
// 调整节点的位置
// 公式: 第i个节点的左子节点:2*1+1;第i个节点的右子节点:2*1+2;
adjust(curIndex, data = this.data, length = this.length) {
// 从当前节点(curIndex)开始遍历,
// 左子节点(i = 2 * curIndex + 1),每次遍历完成,当前节点置换为左子节点(i = 2 * i + 1),临界条件是当前遍历的节点是叶子节点了(i < length)
for(let i = 2 * curIndex + 1; i < length; i = 2 * i + 1) {
// i 为左节点, i+1为右节点,如果右节点存在的话,要比较左节点和右节点的大小
// 如果是max堆,且右节点大于左节点,把i设设置成右节点的索引,即i++
// 如果是min堆,且右节点小于左节点,把i设设置成右节点的索引,即i++
if(i + 1 < length) {
if((this.type === 'max' && data[i + 1] > data[i]) || (this.type === 'min' && data[i + 1] < data[i])) i++
}
// 如果是max堆,且当前节点小于子节点中的最大值,交换两个值的位置,curIndex置换为子节点中最大值的索引
// 如果是min堆,且当前节点大于子节点中的最大值,交换两个值的位置,curIndex置换为子节点中最小值的索引
if((this.type === 'max' && data[curIndex] < data[i])
|| (this.type === 'min' && data[curIndex] > data[i])) {
[data[curIndex], data[i]] = [data[i], data[curIndex]]
curIndex = i
} else {
break
}
}
}
// 由于堆属于优先队列,只能从末尾添加
// 添加后有可能破坏堆的结构,需要从下到上进行调整
push(value) {
this.data.push(value);
if (this.length > 1) {
// 从最后一个节点开始上遍历
let index = this.length - 1;
// target为父节点
let target = Math.floor((index - 1) / 2);
while (target >= 0) {
if ((this.type === 'min' && this.data[index] < this.data[target]) ||
(this.type === 'max' && this.data[index] > this.data[target])) {
[this.data[index], this.data[target]] = [this.data[target], this.data[index]]
index = target;
target = Math.floor((index - 1) / 2)
} else {
break
}
}
}
}
// 由于堆属于优先队列,只能从头部移除
// 移除头部后,使用末尾元素填充头部,从头部开始调整节点位置
pop() {
if(!this.length) return null
if(this.length === 1) return this.data.pop()
const result = this.data[0]
this.data[0] = this.data.pop()
this.adjust(0)
return result
}
// 堆排序
// max 从大到小,先构建最小堆,然后循环:交换顶部和尾部,从新排序
// min 从小到大,先构建最大堆,然后循环:交换顶部和尾部,从新排序
// 复杂度分析:adjust函数中相当于堆的每一层只遍历一个结点,因为具有n个结点的完全二叉树的深度为[log2n]+1,
// 所以adjust的复杂度为 O(logn),而外层循环共有 f(n) 次,所以最终的复杂度为 O(nlogn)。
static sort(data = [], type = 'max') {
let result = new Heap(data, type === 'max' ? 'min' : 'max')
// 如果是最小堆,根 节点总是最小的,如果是最大堆,根节点总是最大的。
// 根节点与最后一个节点交换
// 从根节点开始调整,并且最后一个结点已经为当
// 前最大值,不需要再参与比较,所以第三个参数
// 为 i,即比较到最后一个结点前一个即可
for(let i = result.data.length - 1; i >= 0; i--) {
[result.data[0], result.data[i]] = [result.data[i], result.data[0]]
result.adjust(0, result.data, i)
}
return result.data
}
}
const heap = new Heap([5,6,7,8,1,2,3,4,9])
console.log('最大堆数据:', heap.data)
console.log('堆顶数据出队列:', heap.pop())
console.log('移除堆顶后的数据-最大堆:', heap.data)
heap.push(9)
console.log('9入堆后的数据-最大堆:', heap.data)
console.log('堆排序-从小到大:', Heap.sort([5,6,7,28,1,22,34,4,9], 'min'))
console.log('堆排序-从大到小:', Heap.sort([5,6,7,28,1,22,34,4,9], 'max'))
