如何用 JS 实现二叉堆

更新日期: 2021-03-04 阅读: 2.2k 标签: 算法
原文来自:https://zoo.team/article/binary-heap-with-js
作者:政采云前端团队

前言

二叉树(Binary Tree)是一种树形结构,它的特点是每个节点最多只有两个分支节点,一棵二叉树通常由根节点、分支节点、叶子节点组成,如下图所示。每个分支节点也常常被称作为一棵子树,而二叉堆是一种特殊的树,它属于完全二叉树。


二叉树与二叉堆的关系

在日常工作中会遇到很多数组的操作,比如排序等。那么理解二叉堆的实现对以后的开发效率会有所提升,下面就简单介绍一下什么是二叉树,什么是二叉堆。

二叉树特征

  • 根节点:二叉树最顶层的节点
  • 分支节点:除了根节点以外且拥有叶子节点
  • 叶子节点:除了自身,没有其他子节点

在二叉树中,我们常常还会用父节点和子节点来描述,比如上图中左侧节点 2 为 6 和 3 的父节点,反之 6 和 3 是 2 子节点。

二叉树分类

二叉树分为满二叉树(full binary tree)和完全二叉树(complete binary tree)。

  • 满二叉树:一棵深度为 k 且有 2 ^ k - 1个节点的二叉树称为满二叉树
  • 完全二叉树:完全二叉树是指最后一层左边是满的,右边可能满也可能不满,然后其余层都是满的二叉树称为完全二叉树(满二叉树也是一种完全二叉树)

二叉树结构

从图中我们可以看出二叉树是从上到下依次排列下来,可想而知可以用一个数组来表示二叉树的结构,从下标 index( 0 - 8 ) 从上到下依次排列。


  • 二叉树左侧节点表达式 index * 2 + 1。例如:以根节点为例求左侧节点,根节点的下标为0,则左侧节点的序数是1 ,对应数组中的值为1
  • 二叉树右侧节点表达式 index * 2 + 2。例如:以根节点为例求右侧节点,根节点的下标为0,则右侧节点的序数是2 ,对应数组中的值为 8
  • 二叉树叶子节点表达式 序数 >= floor( N / 2 )都是叶子节点(N是数组的长度)。例如:floor( 9 / 2 ) = 4 ,则从下标 4 开始的值都为叶子节点

二叉堆特征

二叉堆是一个完全二叉树,父节点与子节点要保持固定的序关系,并且每个节点的左子树和右子树都是一个二叉堆。


从上图可以看出

  • 图一:每个父节点大于子节点或等于子节点,满足二叉堆的性质
  • 图二:其中有一个父节点小于子节点则不满足二叉堆性质

二叉堆分类

二叉堆根据排序不同,可以分为最大堆和最小堆

  • 最大堆:根节点的键值是所有堆节点键值中最大者,且每个父节点的值都比子节点的值大
  • 最小堆:根节点的键值是所有堆节点键值中最小者,且每个父节点的值都比子节点的值小


如何实现二叉堆

通过上面的讲述想必大家对二叉堆有了一定的理解,那么接下来就是如何实现。以最大堆为例,首先要初始化数组然后通过交换位置形成最大堆。

初始化二叉堆

从上面描述,我们可以知道二叉堆其实就是一个数组,那么初始化就非常简单了。

  • class Heap{
  • constructor(arr){
  • this.data = [...arr];
  • this.size = this.data.length;
  • }
  • }

父子节点交换位置

图一中 2 作为父节点小于子节点,很显然不符合最大堆性质。maxHeapify 函数可以把每个不符合最大堆性质的节点调换位置,从而满足最大堆性质的数组。


调整步骤:

1.调整分支节点 2 的位置(不满足最大堆性质)

2.获取父节点 2 的左右节点 ( 12 , 5 ) ,从 ( 2 , 15 , 5 ) 中进行比较

3.找出最大的节点与父节点进行交换,如果该节点本身为最大节点则停止操作

4.重复 step2 的操作,从 2 , 4 , 7 中找出最大值与 2 做交换(递归)

  • maxHeapify(i) {
  • let max = i;
  • if(i >= this.size){
  • return;
  • }
  • // 当前序号的左节点
  • const l = i * 2 + 1;
  • // 当前需要的右节点
  • const r = i * 2 + 2;
  • // 求当前节点与其左右节点三者中的最大值
  • if(l < this.size && this.data[l] > this.data[max]){
  • max = l;
  • }
  • if(r < this.size && this.data[r] > this.data[max]){
  • max = r;
  • }
  • // 最终max节点是其本身,则已经满足最大堆性质,停止操作
  • if(max === i) {
  • return;
  • }
  • // 父节点与最大值节点做交换
  • const t = this.data[i];
  • this.data[i] = this.data[max];
  • this.data[max] = t;
  • // 递归向下继续执行
  • return this.maxHeapify(max);
  • }

形成最大堆

我们可以看到,初始化是由一个数组组成,以下图为例很显然并不会满足最大堆的性质,上述 maxHeapify 函数只是对某一个节点作出对调,无法对整个数组进行重构,所以我们要依次对数组进行递归重构。


1.找到所有分支节点 Math.floor( N / 2 )(不包括叶子节点)

2.将找到的子节点进行 maxHeapify 操作

  • rebuildHeap(){
  • // 叶子节点
  • const L = Math.floor(this.size / 2);
  • for(let i = L - 1; i >= 0; i--){
  • this.maxHeapify(i);
  • }
  • }

生成一个升序的数组


1.swap 函数交换首位位置

2.将最后一个从堆中拿出相当于 size - 1

3.执行 maxHeapify 函数进行根节点比较找出最大值进行交换

4.最终 data 会变成一个升序的数组

  • sort() {
  • for(let i = this.size - 1; i > 0; i--){
  • swap(this.data, 0, i);
  • this.size--;
  • this.maxHeapify(0);
  • }
  • }

插入方法

Insert 函数作为插入节点函数,首先

1.往 data 结尾插入节点

2.因为节点追加,size + 1

3.因为一个父节点拥有 2 个子节点,我们可以根据这个性质通过 isHeap 函数获取第一个叶子节点,可以通过第一个叶子节点获取新插入的节点,然后进行 3 个值的对比,找出最大值,判断插入的节点。如果跟父节点相同则不进行重构(相等满足二叉堆性质),否则进行 rebuildHeap 重构堆

  • isHeap() {
  • const L = Math.floor(this.size / 2);
  • for (let i = L - 1; i >= 0; i--) {
  • const l = this.data[left(i)] || Number.MIN_SAFE_INTEGER;
  • const r = this.data[right(i)] || Number.MIN_SAFE_INTEGER;
  • const max = Math.max(this.data[i], l, r);
  • if (max !== this.data[i]) {
  • return false;
  • }
  • return true;
  • }
  • }
  • insert(key) {
  • this.data[this.size] = key;
  • this.size++
  • if (this.isHeap()) {
  • return;
  • }
  • this.rebuildHeap();
  • }

删除方法

delete 函数作为删除节点,首先

1.删除传入index的节点

2.因为节点删除,size - 1

3.重复上面插入节点的操作

  • delete(index) {
  • if (index >= this.size) {
  • return;
  • }
  • this.data.splice(index, 1);
  • this.size--;
  • if (this.isHeap()) {
  • return;
  • }
  • this.rebuildHeap();
  • }

完整代码

  • /**
  • * 最大堆
  • */
  • function left(i) {
  • return (i * 2) + 1;
  • }
  • function right(i) {
  • return (i * 2) + 2;
  • }
  • function swap(A, i, j) {
  • const t = A[i];
  • A[i] = A[j];
  • A[j] = t;
  • }
  • class Heap {
  • constructor(arr) {
  • this.data = [...arr];
  • this.size = this.data.length;
  • this.rebuildHeap = this.rebuildHeap.bind(this);
  • this.isHeap = this.isHeap.bind(this);
  • this.sort = this.sort.bind(this);
  • this.insert = this.insert.bind(this);
  • this.delete = this.delete.bind(this);
  • this.maxHeapify = this.maxHeapify.bind(this);
  • }
  • /**
  • * 重构堆,形成最大堆
  • */
  • rebuildHeap() {
  • const L = Math.floor(this.size / 2);
  • for (let i = L - 1; i >= 0; i--) {
  • this.maxHeapify(i);
  • }
  • }
  • isHeap() {
  • const L = Math.floor(this.size / 2);
  • for (let i = L - 1; i >= 0; i--) {
  • const l = this.data[left(i)] || Number.MIN_SAFE_INTEGER;
  • const r = this.data[right(i)] || Number.MIN_SAFE_INTEGER;
  • const max = Math.max(this.data[i], l, r);
  • if (max !== this.data[i]) {
  • return false;
  • }
  • return true;
  • }
  • }
  • sort() {
  • for (let i = this.size - 1; i > 0; i--) {
  • swap(this.data, 0, i);
  • this.size--;
  • this.maxHeapify(0);
  • }
  • }
  • insert(key) {
  • this.data[this.size++] = key;
  • if (this.isHeap()) {
  • return;
  • }
  • this.rebuildHeap();
  • }
  • delete(index) {
  • if (index >= this.size) {
  • return;
  • }
  • this.data.splice(index, 1);
  • this.size--;
  • if (this.isHeap()) {
  • return;
  • }
  • this.rebuildHeap();
  • }
  • /**
  • * 交换父子节点位置,符合最大堆特征
  • * @param {*} i
  • */
  • maxHeapify(i) {
  • let max = i;
  • if (i >= this.size) {
  • return;
  • }
  • // 求左右节点中较大的序号
  • const l = left(i);
  • const r = right(i);
  • if (l < this.size && this.data[l] > this.data[max]) {
  • max = l;
  • }
  • if (r < this.size && this.data[r] > this.data[max]) {
  • max = r;
  • }
  • // 如果当前节点最大,已经是最大堆
  • if (max === i) {
  • return;
  • }
  • swap(this.data, i, max);
  • // 递归向下继续执行
  • return this.maxHeapify(max);
  • }
  • }
  • module.exports = Heap;


示例

相信通过上面的讲述大家对最大堆的实现已经有了一定的理解,我们可以利用这个来进行排序。

  • const arr = [15, 12, 8, 2, 5, 2, 3, 4, 7];
  • const fun = new Heap(arr);
  • fun.rebuildHeap(); // 形成最大堆的结构
  • fun.sort();// 通过排序,生成一个升序的数组
  • console.log(fun.data) // [2, 2, 3, 4, 5, 7, 8, 12, 15]


总结

文章中主要讲述了二叉树、二叉堆的概念,然后通过代码实现二叉堆。我们可以通过二叉堆来做排序和优先级队列等。

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://fly63.com/article/detial/10168

相关推荐

JavaScript字符串压缩_js实现字符串压缩

设计一种方法,通过给重复字符计数来进行基本的字符串压缩。例如,字符串 aabcccccaaa 可压缩为 a2b1c5a3 。而如果压缩后的字符数不小于原始的字符数,则返回原始的字符串。 可以假设字符串仅包括a-z的字母

js实现将一个正整数分解质因数

js 把一个正整数分解成若干个质数因子的过程称为分解质因数,在计算机方面,我们可以用一个哈希表来存储这个结果。首先,你需要一个判断是否为质数的方法,然后,利用短除法来分解。

js之反转整数算法

将一个整数中的数字进行颠倒,当颠倒后的整数溢出时,返回 0 ;当尾数为0时候需要进行舍去。解法:转字符串 再转数组进行操作,看到有人用四则运算+遍历反转整数。

js求数组中的最大差值的方法总汇

有一个无序整型数组,如何求出这个数组中最大差值。(例如:无序数组1, 3, 63, 44最大差值是 63-1=62)。实现原理:遍历一次数组,找到最大值和最小值,返回差值

js实现生成任意长度的随机字符串

js生成任意长度的随机字符串,包含:数字,字母,特殊字符。实现原理:可以手动指定字符库及随机字符长度,利用Math.round()和Math.random()两个方法实现获取随机字符

js生成32位uuid算法总汇_js 如何生成uuid?

GUID是一种由算法生成的二进制长度为128位的数字标识符。GUID 的格式为“xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx”,其中的 x 是 0-9 或 a-f 范围内的一个32位十六进制数。在理想情况下,任何计算机和计算机集群都不会生成两个相同的GUID。

js从数组取出 连续的 数字_实现一维数组中连续数字分成几个连续的数字数组

使用原生js将一维数组中,包含连续的数字分成一个二维数组,这篇文章分2种情况介绍如何实现?1、过滤单个数字;2、包含单个数字。

Tracking.js_ js人脸识别前端代码/算法框架

racking.js 是一个独立的JavaScript库,实现多人同时检测人脸并将区域限定范围内的人脸标识出来,并保存为图片格式,跟踪的数据既可以是颜色,也可以是人,也就是说我们可以通过检测到某特定颜色,或者检测一个人体/脸的出现与移动,来触发JavaScript 事件。

js实现统计一个字符串中出现最多的字母的方法总汇

给出一个字符串,统计出现次数最多的字母。方法一为 String.prototype.charAt:先遍历字符串中所有字母,统计字母以及对应显示的次数,最后是进行比较获取次数最大的字母。方法二 String.prototype.split:逻辑和方法一相同,只不过是通过 split 直接把字符串先拆成数组。

js实现斐波那契数列的几种方式

斐波那契指的是这样一个数列:1、1、2、3、5、8、13、21、34......在数学上,斐波纳契数列以如下被以递归的方法定义:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=2,n∈N*);随着数列项数的增加,前一项与后一项之比越来越逼近黄金分割的数值0.6180339887..…

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!