上篇文章分享了堆排序的步骤,第一步建堆,第二步堆化,也就是堆调整。这篇文章分享的内容是建堆的第二种方式,以及堆节点的删除,以及复杂度分析
想要把算法学好,数据结构的基础是必不可少的
第一种建堆的方式是借用了插入排序的思想,将数组分成了两个部分,第一部分是已经建好了的堆,第二部分是等待建堆的元素。建堆的过程是不断地将第二部分的元素放入到第一部分中。这种方式的复杂度是 nlogn
下面是上篇文章中的代码实现,详细了解可以移步:🥳前端算法面试之堆排序-每日一练
javascript
class Heap {
constructor(data) {
this.data = data;
}
build() {
for (let i = 2; i < this.data.length; i++) {
this.heapfyTop(i);
}
}
heapfyTop(n) {
while (n > 1 && this.data[n] > this.data[Math.floor(n / 2)]) {
this.swap(n, Math.floor(n / 2));
n = Math.floor(n / 2);
}
}
swap(index1, index2) {
const temp = this.data[index1];
this.data[index1] = this.data[index2];
this.data[index2] = temp;
}
}
第二种建堆方式
第二种建堆的方式复杂度更加低。
- 首先直接将数组看成是一个完整的部分,一个堆。建堆的任务是将这个堆调整成大根堆。
- 然后从第一个非叶子节点开始,将这个看成是一个小的堆,并且往下检查这个小堆是否符合大根堆的定义,即如果有子节点大于父节点,就更换两者的位置。
- 检查剩下的非叶子节点,重复上面第二步的操作。
当所有的叶子都检查完了,完整的大根堆也就建好了。这种建堆的方式的时间复杂度是 n。下面看代码实现
javascript
class Heap{
//省略其他代码
build2() {
for (let i = Math.floor(this.data.length / 2); i > 0; i--) {
this.heapfyBelow(i, this.data.length);
}
}
heapfyBelow(n, end) {
// 是否是叶子节点
while (n * 2 <= end) {
let maxIndex = n;
// 是否有左孩子
if (n * 2 <= end && this.data[maxIndex] < this.data[n * 2]) maxIndex = n * 2;
// 是否有右孩子
if (n * 2 + 1 <= end && this.data[maxIndex] < this.data[n * 2 + 1]) maxIndex = n * 2 + 1;
if (maxIndex == n) break;
this.swap(n, maxIndex);
n = maxIndex;
}
}
}
build2 就是第二种建堆的方法。从第一个非叶子开始遍历Math.floor(this.data.length / 2)
,因为是完全二叉树,当树的节点为 n,那么第一个非叶子节点的下标就是 n/2 向下取整。
调整小堆的方法heapfyBelow
,接收两个参数,第一个参数 n 表示从哪个下标开始检查,第二个参数表示检查到哪里结束。因为整个数组都看成一个完整的堆,所以 end 一直是this.data.length
。heapfyBelow
的基本逻辑:看左右孩子是否大于当前节点,如果有就选一个最大的子节点,并与其更换位置。一直往下检查,直到最后一个非叶子节点。
测试代码:
javascript
const data = [-1, 21, 33, 5, 42, 123, 54, 65, 23, 33, 55];
const heap = new Heap(data);
heap.build2();
console.log(heap.data);
// [
// -1, 123, 55, 65, 42,
// 33, 54, 5, 23, 33,
// 21
// ]
结合完全二叉树的数组存放性质,可以看到上面的输出结果是完全符合大根堆定义的。
节点的删除
大根堆一般会被用来实现优先级队列,即无论放进去的次数,选出元素的时候总是选排在最前面的。像 react 中的优先级任务队列,总是会选择过期时间最长的任务来执行。而大根堆实现的任务队列总是会让过期时间最长的任务放在数组的最前面。
删除过程也很简单,直接将堆顶的节点替换成堆的最后一个节点,然后重新调整堆,以保持大根堆的性质
javascript
class Heap{
// 省略其他代码
/**
* 删除堆中的最大值(根节点)
* @returns {number} 删除的值
*/
pop() {
// 获取待删除的根节点
const data = this.data[1];
// 将堆的最后一个节点替换为根节点
this.data[1] = this.data.pop();
// 重新调整堆,使其保持大根堆的性质
this.heapfyBelow(1, this.data.length);
// 返回删除的值
return data;
}
}
测试代码:
javascript
const data = [-1, 21, 33, 5, 42, 123, 54, 65, 23, 33, 55];
const heap = new Heap(data);
heap.build2();
console.log(heap.data);
// [
// -1, 123, 55, 65, 42,
// 33, 54, 5, 23, 33,
// 21
// ]
console.log(heap.pop());
// 123
console.log(heap.data);
// [
// -1, 65, 55, 54, 42,
// 33, 21, 5, 23, 33
// ]
堆顶的元素被弹出,剩余堆依旧符合大根堆的性质
复杂度分析
堆排序分成两步,第一步建堆的时间复杂度是 n,第二步持续堆调整的时间复杂度是 nlogn,所以整体的时间复杂度是 n+nlogn,省略较小的因子,所以最后的时间复杂度是 nlogn。
对于元素的插入,先放到堆的最后一个位置,然后再调整堆。所以时间复杂度是 1+logn,即 logn
对于元素的删除,先替换堆顶的元素,然后再调整堆,和插入时间复杂度一样,也是 1+logn,即 logn
在优先级队列的应用场景中,涉及到获取最优先的元素,以及元素的插入删除操作,堆的时间复杂度是比其他的数据结构都要更好
为什么是 logn,因为一棵完全二叉树的高度 h 和节点 n 有这样的关系:h = logn
总结
这篇文章紧接上篇文章:🥳前端算法面试之堆排序-每日一练,分享了建堆的第二种方式,以及堆节点的删除,以及复杂度分析。文章思路清晰,文笔清楚,代码详实,是个不可多得好文章啊
什么问题可以评论区留言哦。我每天都会分享一篇算法小练习,喜欢就点赞+关注吧