概述
堆:类似完全二叉树的数据结构。但通常用数组实现。
- 大顶堆:所有父节点都比子节点大
- 小顶堆:所有父节点都比子节点小
堆的实现
以小顶堆为例介绍。如下图就是一个小顶堆↓,是类似完全二叉树的数据结构
堆有以下性质
- 堆维护堆顶永远是当前最值,小顶堆是最小值,大顶堆是最大值
- 堆只能在一头(堆顶)删除,在另一头(堆底)插入,和队列非常相似
由于堆的这种特性,经常被用来做排序和优先队列
下面讲一下堆是如何维护堆顶为当前最值的:
堆顶删除步骤
又称为下沉
- 堆顶出堆,将堆底的数据放到堆顶,如图1
- 比较左右2个子节点,与更小的节点交换(如果是大顶堆则与更大的交换),如图2
- 继续比较左右2个子节点,直到小于等于左右2个子节点或成为叶节点,如图3
堆底插入步骤
又称为上浮
- 新元素插入到堆底,如图4
- 与父节点比较,若该节点小于父节点,则与父节点交换,如图5
- 继续和父节点比较,直到大于等于父节点或成为根节点(堆顶)为止,如图6
如果再插入一个0,步骤图如下
—
代码实现
由于堆能维护堆顶为当前最值,因此如果不断取出堆顶元素,就能得到一个单调递增/减的数列,因此堆也经常用来做排序,平均时间复杂度O(NlogN)
虽然前面介绍是用二叉树的形式,但实际中往往使用数组进行堆的构造。因为堆是完全二叉树的形式,而由于完全二叉树的性质,其完全可以使用数组表示,且满足父节点编号=Math.floor(子节点编号/2)
(注意不是索引)。上面最后一张图用数组表示就是[0, 1, 2, 4, 5, 6, 3, 7]
构造代码如下:
// 小顶堆
class MinHeap {
constructor (array = []) {
this.heapArray = []
for (let index = 0; index < array.length; index++) {
this.push(array[index])
}
}
//下沉
sink () {
let curIdx = 0
let len = this.heapArray.length
let leftChild = 2 * (curIdx + 1) - 1
let rightChild = leftChild + 1
let minChild = leftChild
while (leftChild <= len - 1) { //叶节点跳出循环
// 获取最小的子节点索引
// 如果没有右节点,直接选择左节点,否则比较大小
minChild = rightChild > len - 1 || this.heapArray[leftChild] < this.heapArray[rightChild] ? leftChild : rightChild
// 交换节点
if (this.heapArray[curIdx] > this.heapArray[minChild]) {
[this.heapArray[curIdx], this.heapArray[minChild]] = [this.heapArray[minChild], this.heapArray[curIdx]]
curIdx = minChild
leftChild = 2 * (curIdx + 1) - 1
rightChild = leftChild + 1
} else {
return
}
}
}
//上浮
swim () {
let curIdx = this.heapArray.length - 1
while (curIdx > 0) {
let fatherIdx = Math.floor((curIdx + 1) / 2) - 1
if (this.heapArray[curIdx] < this.heapArray[fatherIdx]) {
[this.heapArray[curIdx], this.heapArray[fatherIdx]] = [this.heapArray[fatherIdx], this.heapArray[curIdx]]
curIdx = fatherIdx
} else {
return
}
}
}
pop () {
if (this.heapArray.length === 0) {
return null
} else if (this.heapArray.length === 1) {
return this.heapArray.pop()
}
let top = this.heapArray[0]
this.heapArray[0] = this.heapArray.pop()
this.sink()
return top
}
push (val) {
this.heapArray.push(val)
this.swim()
}
}
堆排序
前面已经说过,只要不断的将堆顶出列,即可得到单调的序列实现排序。
let heap = new MinHeap([10, 500, 200, 0, 100, 30, 20, 30, 80, 300]);
let sortList = []
while (heap.heapArray.length > 0) {
sortList.push(heap.pop())
}
console.log(sortList); // [ 0, 10, 20, 30, 30, 80, 100, 200, 300, 500 ]
时间复杂度为O(NlogN)
,空间复杂度为O(n)
当然也可以原地排序,空间复杂度为O(1)
,不过这种是大顶堆升序,小顶堆降序
排序逻辑: 先把乱序的数组自底向上调整成堆,再每次循环将堆顶和堆底交换,限制数组长度-1,然后堆顶下沉。代码如下:
这需要稍微改一下sink函数
let sink = (array, idx, len) => {
// array是待转换的数组,idx是下沉的节点索引,len是以idx为堆顶的数组长度
let curIdx = idx
let leftChild = 2 * (curIdx + 1) - 1
let rightChild = leftChild + 1
let minChild = leftChild
while (leftChild <= len - 1) { //叶节点跳出循环
// 获取最小的子节点索引
// 如果没有右节点,直接选择左节点,否则比较大小
minChild = rightChild > len - 1 || array[leftChild] < array[rightChild] ? leftChild : rightChild
// 交换节点
if (array[curIdx] > array[minChild]) {
[array[curIdx], array[minChild]] = [array[minChild], array[curIdx]]
curIdx = minChild
leftChild = 2 * (curIdx + 1) - 1
rightChild = leftChild + 1
} else {
return
}
}
}
let heapSort = (array) => {
let len = array.length
// 调整为小顶堆
for (let index = Math.floor(len / 2); index >= 0; index--) {
sink(array, index, len - index)
}
// 堆排序
for (let index = 0; index < len; index++) {
[array[len - index - 1], array[0]] = [array[0], array[len - index - 1]]
sink(array, 0, len - index - 1)
}
}
let array = [10, 500, 200, 0, 100, 30, 20, 30, 80, 300]
heapSort(array)
console.log(array) // [ 500, 300, 200, 100, 80, 30, 30, 20, 10, 0 ]
优先队列
其实实现了堆,优先队列的逻辑基本上也差不多,只要修改一下权重和优先规则即可。小顶堆就是一个数值越小优先级越大的优先队列
// 优先队列
class PriorityQueue {
constructor (array = [], cmp = (a, b) => a < b) {
this.heapArray = []
this.cmp = cmp // 网上也有 this.cmp = (a, b) => cmp(a, b) ,暂时不知道为什么要做一个函数的拷贝
for (let index = 0; index < array.length; index++) {
this.push(array[index])
}
}
//下沉
sink () {
let curIdx = 0
let len = this.heapArray.length
let leftChild = 2 * (curIdx + 1) - 1
let rightChild = leftChild + 1
let minChild = leftChild
while (leftChild <= len - 1) { //叶节点跳出循环
minChild = rightChild > len - 1 || this.cmp(this.heapArray[leftChild], this.heapArray[rightChild]) ? leftChild : rightChild
// 交换节点
if (this.cmp(this.heapArray[minChild], this.heapArray[curIdx])) {
[this.heapArray[curIdx], this.heapArray[minChild]] = [this.heapArray[minChild], this.heapArray[curIdx]]
curIdx = minChild
leftChild = 2 * (curIdx + 1) - 1
rightChild = leftChild + 1
} else {
return
}
}
}
//上浮
swim () {
let curIdx = this.heapArray.length - 1
while (curIdx > 0) {
let fatherIdx = Math.floor((curIdx + 1) / 2) - 1
if (this.cmp(this.heapArray[curIdx], this.heapArray[fatherIdx])) {
[this.heapArray[curIdx], this.heapArray[fatherIdx]] = [this.heapArray[fatherIdx], this.heapArray[curIdx]]
curIdx = fatherIdx
} else {
return
}
}
}
pop () {
if (this.heapArray.length === 0) {
return null
} else if (this.heapArray.length === 1) {
return this.heapArray.pop()
}
let top = this.heapArray[0]
this.heapArray[0] = this.heapArray.pop()
this.sink()
return top
}
push (val) {
this.heapArray.push(val)
this.swim()
}
}
let heap = new PriorityQueue([10, 500, 200, 0, 100, 30, 20, 30, 80, 300], (a,b) => a > b); // 试试数值越大优先级越高(大顶堆)
let sortList = []
while (heap.heapArray.length > 0) {
sortList.push(heap.pop())
}
console.log(sortList); // [ 500, 300, 200, 100, 80, 30, 30, 20, 10, 0 ]
参考: