「这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战」
前面我们讲了树,今天我们讲一种特殊的树,堆(Heap)。
ps:看没看完麻烦顺手点个赞~
堆的定义
- 堆是一种完全二叉树 -- 完全二叉树是除了最下层节点不满外,其他节点必须是满节点,且最下层所有节点靠左
- 堆的当前节点必须大于等于(小于等于)它的子节点 -- 当前节点大于等于子节点的堆叫做大顶堆,小于等于子节点的堆叫做小顶堆
堆的实现
因为堆是一个完全二叉树,我们可以方便的使用数组存储堆(数组从1位开始,是为了方便计数)
如何往堆中插入元素?
例如往上面的大顶堆中插入数字5,我们自下而上比较节点,不满足大顶堆的规则就交换,满足就跳出循环
const Heap = function() {
// 当前堆
this.heap = [null, 6, 5, 4, 3, 2, 1];
// 当前堆内数据量
this.count = 6;
this.add(5);
}
Heap.prototype.add = function(val) {
this.heap.push(val);
this.count++;
heapifyUp(this.heap, this.count);
}
// 自下往上堆化
const heapifyUp = function(heap, i) {
while (i >> 1 > 0 && heap[i >> 1] < heap[i]) {
swap(heap, i >> 1, i);
i = i >> 1;
}
}
const swap = function(arr, a, b) {
[arr[a], arr[b]] = [arr[b], arr[a]];
}
如何从堆顶删除数据
将堆顶数据删除,并将堆底数据放到堆顶,然后对堆进行自上而下的堆化。 将当前节点与左节点进行比较,选出最小值的下标,再与右节点比较,选出最小值下标,然后交换。若没有节点交换,跳出循环,否则将指针指向刚刚的最小节点,继续上述操作。
const Heap = function() {
this.heap = [null, 6, 5, 4, 3, 2, 1];
this.count = 6;
}
Heap.prototype.removeTop = function() {
// 交换堆顶和堆底
swap(this.heap, 1, this.count);
const topVal = this.heap.pop();
this.count--;
heapifyDown(this.heap, 1, this.count);
return topVal;
}
const heapifyDown = function(heap, i, n) {
while(true) {
let posi = i;
if (i * 2 <= n && heap[i * 2] > heap[i]) posi = i * 2;
if (i * 2 + 1 <= n && heap[i * 2 + 1] > heap[posi]) posi = i * 2 + 1;
if (i === posi) break;
swap(heap, i, posi);
i = posi;
}
}
const swap = function(arr, a, b) {
[arr[a], arr[b]] = [arr[b], arr[a]];
}
const heap = new Heap();
heap.removeTop(); // 6
heap.removeTop(); // 5
...
如何对数组进行堆化排序
假设有一个无序数组[2,3,6,4,5,1],需要进行升序排列,首先需要先建堆,然后对他进行排序
建堆方案有两种:
-
一种是类似新增数据的方案,自下而上从数组尾部进行堆化,因为堆化一个节点的时间复杂度为O(logn),顶部节点不需要排序,所以这种建堆的时间复杂度为(n-1) * O(logn) = O(nlogn)
-
还有一种方案是自上而下的方案,因为自上而下,底部所有叶子节点不需要堆化,从最后一个非叶子节点往下堆化,所以时间复杂度是不是为(n/2) * O(logn) = O(nlogn)?其实不是,因为子节点的堆化程度跟高度成正比,顶部的堆化程度最高,底部最低。 所以时间复杂度其实为O(n)
下面我们根据第二种建堆方案来排序:
// 建堆
const buildHeap = function(nums){
nums.unshift(null);
const n = nums.length;
for (let i = n >> 1; i > 0; i--) {
heapify(nums, n, i)
}
return nums;
}
// 堆化
const heapify = function(heap, n, i) {
while(true) {
let maxPos = i;
if (i * 2 <= n && heap[i * 2] > heap[i]) maxPos = i * 2;
if (i * 2 + 1 <= n && heap[i * 2 + 1] > heap[maxPos]) maxPos = i * 2 + 1;
if (maxPos === i) break;
swap(heap, maxPos, i);
i = maxPos;
}
}
// 排序
const sort = function(nums) {
let n = nums.length;
let heap = buildHeap(nums);
while (len > 1) {
swap(heap, 1, n);
n--;
heapify(heap, n, 1)
}
return heap.shift();
}
const swap = function(arr, a, b) {
[arr[a], arr[b]] = [arr[b], arr[a]];
}
排序时不需要额外创建空间,因为我们生成的是大顶堆,堆顶是最大值,将他与数组最末尾交换,然后堆化前n-1的堆,再将堆化后的堆顶与倒数第二位交换,如此即排序成功。
排序的时间复杂度是O(nlogn),所以总的排序方法的时间复杂度是O(n) * O(nlogn)=O(nlogn)
堆化排序是原地排序算法,空间复杂度是O(1),因为排序会交换位置,所以是非稳定排序算法。
建堆整体代码:
class Heap {
// type = greater, less
constructor(type, max, compare) {
this.heap = [];
this.type = type || 'less';
this.count = 0;
this.max = max || Infinity;
this.compare = compare || null;
}
}
Heap.prototype.push = function (val) {
if (this.count === this.max) return false;
this.heap[this.count++] = val;
this.heapifyUp();
return true;
}
Heap.prototype.pop = function () {
if (this.count === 0) return false;
const top = this.heap[0];
this.swap(0, this.count - 1);
this.count--;
this.heapifyDown();
return top;
}
Heap.prototype.heapifyDown = function () {
const compare = this.compare;
let i = 0;
while (i < this.count) {
let temp = i;
// 小顶堆
if (this.type === 'less') {
if (i * 2 + 1 < this.count && (compare ? compare(this.heap[i], this.heap[i * 2 + 1]) : this.heap[i] > this.heap[i * 2 + 1])) {
temp = i * 2 + 1;
}
if (i * 2 + 2 < this.count && (compare ? compare(this.heap[temp], this.heap[i * 2 + 2]) : this.heap[temp] > this.heap[i * 2 + 2])) {
temp = i * 2 + 2;
}
} else {
if (i * 2 + 1 < this.count && (compare ? compare(this.heap[i], this.heap[i * 2 + 1]) : this.heap[i] < this.heap[i * 2 + 1])) {
temp = i * 2 + 1;
}
if (i * 2 + 2 < this.count && (compare ? compare(this.heap[temp], this.heap[i * 2 + 2]) : this.heap[temp] < this.heap[i * 2 + 2])) {
temp = i * 2 + 2;
}
}
if (temp === i) break;
this.swap(temp, i);
i = temp;
}
}
Heap.prototype.heapifyUp = function () {
const compare = this.compare;
let i = this.count - 1;
while (i > 0) {
if (this.type === 'less') {
if (compare ? compare(this.heap[i], this.heap[(i - 1) >> 1]) : this.heap[i] < this.heap[(i - 1) >> 1]) {
this.swap(i, (i - 1) >> 1)
i = (i - 1) >> 1
} else {
break;
}
} else {
if (compare ? compare(this.heap[i], this.heap[(i - 1) >> 1]) : this.heap[i] > this.heap[(i - 1) >> 1]) {
this.swap(i, (i - 1) >> 1)
i = (i - 1) >> 1
} else {
break;
}
}
}
}
Heap.prototype.swap = function (a, b) {
[this.heap[a], this.heap[b]] = [this.heap[b], this.heap[a]]
}
Heap.prototype.get = function () {
return this.heap;
}
Heap.prototype.getTop = function () {
return this.heap[0];
}
Heap.prototype.getSize = function () {
return this.count;
}