JavaScript实现堆结构
创始人
2025-05-28 20:09:34
0

说在前面

计算机领域里,到处都是算法,算法的运用是如此常见,如此自然,如此平凡,乃至像空气一样,会被绝大多数人,甚至是计算机专业的人忽视。从我们打开计算机(或者手机,平板电脑)开始,一系列算法就开始运转起来。从操作系统的调度算法,帮助我们顺畅地使用操作系统;到网络连接过程中各种协议的运转,帮助我们畅游信息世界;从我们使用搜索引擎,一个简单的关键字就可以在毫秒级别的时间返回数以亿计的信息,按照优先级排列展现到我们眼前;到浏览器将枯燥的 html, css 和 js 文本转换成直观的网页,供我们轻松阅读浏览;从看似平凡的文字处理工具帮助我们排版,修订;到图像工具中各种神奇的滤镜帮助我们磨皮修片;从游戏,影视作品中炫酷的特效;到最新的交互科技——无论是 AR 还是 VR,越来越普遍的应用,算法无处不在。
说实话,现在,我的这个“学习计算机,必须要懂算法”的观点在逐渐转变。这是因为,计算机的软件行业也在渐渐走向成熟。软件行业已经慢慢成熟到了:如果不会算法,也完全可以有所作为的程度。这也是很多人觉得算法没必要的一个主要原因。
但是,大家一定要提醒自己。虽然我说学习算法对你来说不一定有用,但与此相对应的,要想取得成功,就一定有别的什么,是有用的。算法不是技术领域的唯一的核心竞争力,但无论是一个人,一个企业,还是做一份事业,都需要有核心竞争力。什么都没有,肯定是不行的。
说到算法,一定离不开数据结构,所以对于一些基础的数据结构,我们还是要掌握的,今天就让我们一起先来看看

什么是堆?

堆的定义

堆的数据结构是完全二叉树或一堆数组,因为堆在逻辑上是一棵完全二叉树,在物理结构上是一个一维数组。堆将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。常见的堆有二叉堆、斐波那契堆等。 堆是非线性数据结构,相当于一维数组,有两个直接后继。堆就是用数组实现的二叉树,所以它没有使用父指针或者子指针。堆根据“堆属性”来排序,“堆属性”决定了树中节点的位置。

堆的性质

  • 1、堆中某个节点的值总是不大于或不小于其父节点的值;
  • 2、堆总是一棵完全二叉树

堆的常用用途:

  • 构建优先队列
  • 支持堆排序
  • 快速找出一个集合中的最小值(或者最大值)

实现思路

使用数组实现堆

堆总是一棵完全二叉树,如下图:
image.png

根据堆的这个性质,我们可以使用数组来实现,我们只需要记住父子节点之间的关系即可,假设一个节点的下标为 n,我们可以得到以下信息:

  • 1、父节点下标
fatherIdx = Math.floor((n - 1) / 2);
  • 2、左子节点下标
leftInd = n * 2 + 1;
  • 3、右子节点下标
rightInd = (n + 1) * 2 = leftInd + 1;

所以我们可以直接将该二叉树转换为数组来进行存储,根据以上公式我们可以快速的在数组中找到每个元素的父子元素。

image.png

初始化

初始化一个空数组用于存储堆数据,接受传入一个自定义比较函数,并将传入用于初始化的数据插入堆数组中。

/***  堆初始化*  @param Array array 用于初始化的数组*  @param Function compareFun 自定义比较函数*  */
constructor(array = [], compareFun = null) {this.queue = [];if (compareFun) {this.compareFun = compareFun;}for (let index = 0; index < array.length; index++) {this.push(array[index]);}
}

插入元素

将元素插入到堆数组的最后,因为堆中某个节点的值总是不大于或不小于其父节点的值,所以我们需要根据其比较函数来对堆数组进行调整,将底部插入的元素进行上浮操作,直到满足堆的性质:堆中某个节点的值总是不大于或不小于其父节点的值

我们以小顶堆为例:

  • 1.如果我们发现插入的新元素之后,新插入的元素比起父节点元素值要小,这样就破坏了我们的堆结构,这样就不构成小顶堆。因此我们需要对该结构进行调整。

  • 2.由于堆的物理结构是一个数组,所以我们可以通过数组下标的形式找到我们孩子节点的父节点。前面我们已经知道父节点与子节点的下标关系为fatherIdx = Math.floor((n - 1) / 2);。当我们找到父节点时,我们进行大小比较,如果子节点小于父节点,此时就要进行交换元素。再让子节点到父节点的位置,重新计算父节点。如果子节点大于父节点,此时即说明调整结束

  • 3.持续循环比较,如果子节点已经上浮到堆顶,说明向上调整结束

image.png

push(node) {this.queue.push(node);this.swim();
}
//上浮
swim() {let curIdx = this.queue.length - 1;let fatherIdx = Math.floor((curIdx - 1) / 2);while (this.compareFun(this.queue[fatherIdx], this.queue[curIdx])) {[this.queue[curIdx], this.queue[fatherIdx]] = [this.queue[fatherIdx],this.queue[curIdx],];curIdx = fatherIdx;fatherIdx = Math.floor((curIdx - 1) / 2);}
}

获取堆顶元素值

因为每次插入一个元素的时候我们我们都会对其进行一个上浮操作,所以数组第一个元素就是符合规则的栈顶元素,我们只需要判断堆是否为空,不为空则返回数组的第一个元素即可。

front() {if (this.queue.length == 0) return null;return this.queue[0];
}

弹出堆顶元素

我们要弹出堆顶元素,实际上就是要删除堆顶的数据,但是我们并不能直接删除堆顶的数据。如果直接删除堆顶的数据,就会破坏堆结构,并且复原的复杂度较高。在这里我们介绍一种方法不仅解决了删除堆的问题,并且复杂度较低。

  • 1、首先我们要将堆顶的数据跟最后一个数据交换,然后删除数组最后一个数据,再进行向下调整算法。
  • 2、向下调整算法具体步骤(过程步骤如下图):
    • a.我们将堆顶元素和数组最后一个元素交换后,此时堆顶的元素是数组的最后一个元素,我们要进行向下调整。定义 parent 为堆顶元素,查找 2 个子节点中较小的一个节点作为孩子节点。由于堆是数组结构实现,我们可以首先找到左孩子节点,让左孩子和右孩子进行比较,较小的作为 child 的最后值。
    • b.如果孩子小于父亲,则交换,并继续往下调整。让 parent 到 child 的位置,再重新计算孩子。
    • c.当孩子节点下标大于等于元素总个数时,循环结束。

注:循环过程中一旦成堆,则跳出循环。

pop() {if (this.queue.length == 0) return null;if (this.queue.length == 1) return this.queue.pop();const top = this.queue[0];this.queue[0] = this.queue.pop();this.sink();return top;
}
//下沉
sink() {let curIdx = 0;let minInd = this.getMinInd(curIdx);while (this.compareFun(this.queue[curIdx], this.queue[minInd])) {[this.queue[curIdx], this.queue[minInd]] = [this.queue[minInd],this.queue[curIdx],];curIdx = minInd;minInd = this.getMinInd(curIdx);}
}
//获取较小的子元素下标
getMinInd(curIdx) {let leftInd = curIdx * 2 + 1;let rightInd = leftInd + 1;let minInd = leftInd;if (rightInd < this.queue.length &&this.compareFun(this.queue[leftInd], this.queue[rightInd]))minInd = rightInd;return minInd;
}

堆的分类

大顶堆

在上面实现的堆的基础上,我们传入自定义的比较函数即可:

// 大顶堆
const { Heap } = require("./heap");
class MaxHeap {constructor(array = []) {this.oHeap = new Heap(array, (a, b) => {return a < b;});}get queue() {return this.oHeap.queue;}front() {return this.oHeap.front();}pop() {return this.oHeap.pop();}push() {this.oHeap.push();}
}

小顶堆

在上面实现的堆的基础上,我们传入自定义的比较函数即可:

// 小顶堆
const { Heap } = require("./heap");
class MinHeap {constructor(array = []) {this.oHeap = new Heap(array, (a, b) => {return a > b;});}get queue() {return this.oHeap.queue;}front() {return this.oHeap.front();}pop() {return this.oHeap.pop();}push() {this.oHeap.push();}
}

堆的应用

堆排序

堆排序其实就是利用了堆的性质,先将所有元素构造一个堆,不断的弹出堆顶元素即可达到堆排序的效果。

const { MaxHeap } = require("@jyeontu/structure-jyeontu");
const list = [1, 3, 2, 4, 6, 3, 0, 9, 7];
const res = [];
const myMaxHeap = new MaxHeap(list);
while (myMaxHeap.front() != null) {res.push(myMaxHeap.pop());
}
console.log(res);
//[9, 7, 6, 4, 3, 3, 2, 1, 0]

我们也可以利用这个性质来给堆的实现代码添加一个单元测试:

const { Heap, MinHeap, MaxHeap } = require("../../src/Heap/index");
const assert = require("assert");
describe("Heap", function () {describe("checkMaxHeap", function () {it("checkOrder", function () {const list = [1, 3, 2, 4, 6, 3, 0, 9, 7];const orderList = [...list].sort((a, b) => a - b);const myMaxHeap = new MaxHeap(list);while (myMaxHeap.front() != null) {assert.equal(myMaxHeap.pop(), orderList.pop());}});});describe("checkMinHeap", function () {it("checkOrder", function () {const list = [1, 3, 2, 4, 6, 3, 0, 9, 7];const orderList = [...list].sort((a, b) => b - a);const myMinHeap = new MinHeap(list);while (myMinHeap.front() != null) {assert.equal(myMinHeap.pop(), orderList.pop());}});});
});

源码地址

Gitee: https://gitee.com/zheng_yongtao/structure-jyeontu/tree/master/src/Heap

使用

目前我已经将代码上传到了 npm 上,大家可以直接npm install @jyeontu/structure-jyeontu进行安装,安装完成之后即可直接使用,如下:

const { MaxHeap } = require("@jyeontu/structure-jyeontu");
const list = [1, 3, 2, 4, 6, 3, 0, 9, 7];
const res = [];
const myMaxHeap = new MaxHeap(list);
while (myMaxHeap.front() != null) {res.push(myMaxHeap.pop());
}
console.log(res);
//[9, 7, 6, 4, 3, 3, 2, 1, 0]

说在后面

🎉 这里是 JYeontu,现在是一名前端工程师,有空会刷刷算法题,平时喜欢打羽毛球 🏸 ,平时也喜欢写些东西,既为自己记录 📋,也希望可以对大家有那么一丢丢的帮助,写的不好望多多谅解 🙇,写错的地方望指出,定会认真改进 😊,在此谢谢大家的支持,我们下文再见 🙌。

相关内容

热门资讯

高芙逆转萨巴伦卡 首夺法网女单... 2025年法网女单决赛在世界前二之间展开,2号种子高芙经过三盘激战以6-7(5) 6-2 6-4逆转...
“特朗普私下质疑:马斯克的言行... 【文/观察者网 陈思佳】近日,美国总统特朗普和亿万富翁马斯克公开决裂,在社交媒体上隔空对骂,引发各界...
纪委监委跟进调查 涉性侵的湖南... 纪委监委跟进调查 涉性侵的湖南机场集团原董事长邱继兴被撤销政协委员 经济观察报 记者 李微敖 涉嫌在...
微山县公安局西平镇派出所开展防... 民警向群众普及法律知识 大众网记者 朱晨 通讯员 张家瑞 马浩 济宁报道 5月15日,微山县公安局西...
仲裁裁决书生效后另行起诉,是否... 鲁法案例【2025】180 (图源网络 侵删) 案情简介 2016年9月,A公司成立,为自然人独资...
HereWeGo再等等!罗马诺... 直播吧06月07日讯 名记罗马诺最新消息,阿森纳接近签下门将凯帕,Here We Go即将到来! 罗...
特朗普:美国离了谁都照样转,除... 据@CCTV国际时讯 报道,美国总统特朗普和刚离开政府效率部负责人岗位的埃隆·马斯克当地时间6月5日...
小米:犯罪团伙操纵近万账号诋毁... 小米:犯罪团伙操纵近万账号诋毁小米,犯罪金额巨大 据@小米法务部 5月19日消息,2025年5月15...
警方通报“武大学生故意伤害事件... 6月6日,武汉市公安局洪山区分局通报,6月4日17时许,辖区武汉大学某食堂内发生一起故意伤害案件。民...