学习JavaScript中的二进制堆

113 阅读5分钟

二进制堆是一个高级别的数据结构概念,要理解二进制堆,你应该熟悉二进制搜索树或一般的树。用非常简单的话来说,二进制堆是一个部分有序的二进制树,完全满足的属性

堆的属性

这个属性可以被认为是定义树的一个约束条件,是构建树时必须遵循的某种结构。堆定义了父节点和它的子节点之间的关系,有两种类型的堆,因此在父节点和子节点之间可以存在两种类型的关系。

  • Max-Heap。父节点的值必须总是大于或等于子节点的值
  • 最小堆(Min-heap)。父节点的值必须总是小于或等于子节点的值。

最小堆的代表是

你可以看到,在这棵树上,父节点的值比子节点的值低。

最大堆的表示方法是:

(图片来自维基百科)

你可以看到,父节点的值比它们的子节点大。

阵列表示法

编程语言中的堆是以数组的形式表示的,从上面的max-heap树构建的堆数组的例子是。

var max-heap =[100,19,36,17,3,25,1,2,7];

当以数组的形式表示二进制堆时,你用下面的表达式来推导。

  • 左边的孩子 = i * 2
  • 右边的孩子 = i * 2 + 1
  • 父代 = i / 2

其中**"i "代表数组的索引。谈到索引,当我们使用数组实现堆结构时,我们在第一个索引中放一个"空",也就是索引0**。

堆的工作的视觉表现

对于迷你堆工作的虚拟表示,以及如何将值插入到堆中,我们可以通过点击这里前往旧金山大学的堆可视化工具

在堆中插入数值,你会注意到由于动画的作用,一个新的元素是如何被插入到堆中的。

堆的工作

一个二进制堆有两个主要功能。

  • 首先是在适当的位置添加新元素
  • 第二个功能是删除根值

在堆中添加一个新元素

一个新元素总是被添加在数组的末端,然后与它的父元素进行检查,如果它与堆的属性相悖,那么它就与它的父元素交换。该元素被检查,直到它与堆的根节点进行比较(根节点是堆的第一个节点)。

从堆中删除一个元素

每当你想从堆中移除或获取一个值时,你总是获取根节点的值。这就是为什么如果是最小堆,这个值就是最小的值,如果是最大堆,这个值就是最大的值。当根节点从堆中被移除时,数组的最后一个节点将取代它的位置,然后将它与它的子节点进行比较以匹配堆的条件。如果它不符合条件,就用它的子节点替换,然后再与更多的子节点进行检查。一个更好的解释方法是使用实时堆查看器,如下图所示。

你可以通过观察上面的gif图来观察删除的过程。

在JavaScript中实现二进制堆

我们将一步一步地实现最小堆,在这个过程中,我们先用以下几行代码创建一个新函数。

let MinHeap = function () {

// Rest of the min-heap code will be present inhere

}

第一步是创建一个数组,并将索引0的值设置为null

let heap = [null];

然后我们要用下面几行代码创建插入函数

this.insert = function (num) {
heap.push(num);
if (heap.length>2) {
letidx = heap.length - 1;
while (heap[idx] = 1) {
          [heap[Math.floor(idx / 2)], heap[idx]] = [
            heap[idx],
            heap[Math.floor(idx / 2)],
          ];
if (Math.floor(idx / 2) >1) {
idx = Math.floor(idx / 2);
          } else {
break;
          }
        }
      }
    }
  };

在这个代码片断中发生了以下事情。

  • 一个新的元素num被添加在数组的最后一个索引上
  • 如果数组的长度大于2个元素,那么我们将新元素与它的父节点进行检查
  • 如果该元素小于它的父节点,那么它就被替换成它的父节点,否则我们就推断堆的顺序是正确的。
  • 如果该元素在上一步中被替换为其父节点,那么我们再次将其与新的父节点进行比较,直到我们推断出堆的顺序是正确的或者该元素成为根节点。

下一步是用以下几行代码来实现remove函数

this.remove = function () {
let smallest = heap[1];
if (heap.length>2) {
      heap[1] = heap[heap.length - 1];
heap.splice(heap.length - 1);
if (heap.length == 3) {
if (heap[1] > heap[2]) {
          [heap[1], heap[2]] = [heap[2], heap[1]];
        }
return smallest;
      }
leti = 1;
letleft = 2 * i;
letright = 2 * i + 1;
      while (heap[i] >= heap[left] || heap[i] >= heap[right]) {
if (heap[left] < heap[right]) {
          [heap[i], heap[left]] = [heap[left], heap[i]];
i = 2 * i;
        } else {
          [heap[i], heap[right]] = [heap[right], heap[i]];
i = 2 * i + 1;
        }
left = 2 * i;
right = 2 * i + 1;
if (heap[left] == undefined || heap[right] == undefined) {
          break;
        }
      }
    } elseif (heap.length == 2) {
heap.splice(1, 1);
    } else {
return null;
    }
return smallest;
  };

在上述代码片断中发生了以下步骤。

  • 我们删除根节点,因为它是堆中最小的节点
  • 如果堆只有两个元素,那么第二个元素将成为根节点
  • 如果堆有3个元素,那么第2和第3个元素中最小的一个就成为根节点
  • 如果元素超过3个,那么堆的最后一个元素将成为根节点。
  • 然后这个新的根节点与它的子节点进行比较,用较小的节点替换,并与新的子节点进行比较(对于替换,我们使用对象解构的方法)
  • 这个将元素与子节点进行比较的过程不断重复,直到它达到一个比两个子节点都小的点,或者它成为数组中的最后一个节点。

下一步是创建一个函数,将我们的堆数组显示在控制台,我们通过使用以下几行代码来实现。

this.show = function () {

console.log(heap);

};

实现min-heap数据结构的完整代码片断是。

letMinHeap = function () {
let heap = [null];

this.insert = function (num) {
heap.push(num);
if (heap.length>2) {
letidx = heap.length - 1;
while (heap[idx] = 1) {
          [heap[Math.floor(idx / 2)], heap[idx]] = [
            heap[idx],
            heap[Math.floor(idx / 2)],
          ];
if (Math.floor(idx / 2) >1) {
idx = Math.floor(idx / 2);
          } else {
break;
          }
        }
      }
    }
  };

this.remove = function () {
let smallest = heap[1];
if (heap.length>2) {
      heap[1] = heap[heap.length - 1];
heap.splice(heap.length - 1);
if (heap.length == 3) {
if (heap[1] > heap[2]) {
          [heap[1], heap[2]] = [heap[2], heap[1]];
        }
return smallest;
      }
leti = 1;
let left = 2 * i;
let right = 2 * i + 1;
while (heap[i] >= heap[left] || heap[i] >= heap[right]) {
if (heap[left] < heap[right]) {
          [heap[i], heap[left]] = [heap[left], heap[i]];
i = 2 * i;
        } else {
          [heap[i], heap[right]] = [heap[right], heap[i]];
i = 2 * i + 1;
        }
        left = 2 * i;
        right = 2 * i + 1;
if (heap[left] == undefined || heap[right] == undefined) {
break;
        }
      }
    } elseif (heap.length == 2) {
heap.splice(1, 1);
    } else {
returnnull;
    }
return smallest;
  };
this.show = function () {
console.log(heap);
  };

};

我们现在需要做的是使用我们刚刚创建的MinHeap函数创建一个新的min-heap,然后使用insert和display heap向其中添加元素。要做到这一点,我们制作一个新的变量,并使用以下几行代码将其映射到MinHeap上。

var newMinHeap = new MinHeap();

接下来,让我们用下面几行代码向堆中添加数值。

newMinHeap.insert(34);

newMinHeap.insert(61);

newMinHeap.insert(138);

newMinHeap.insert(82);

newMinHeap.insert(27);

newMinHeap.insert(35);

现在,我们调用show函数将堆阵列显示在控制台。

newMinHeap.show();

我们在控制台得到以下结果。

正如你所看到的,数组的第一个元素是空的。其余的节点都不比它们的子节点大。例如,如果我们以数值为35的节点为例。左边和右边的子节点是这样的。

你可以清楚地看到,**父节点(35)**比它的左子节点(82)和右子节点(61)也要小。同样,每一个父节点都比它的子节点小,因此我们可以推断出我们的代码正在完美地工作。

同样,只需改变比较条件,将父节点小于子节点改为父节点大于子节点,我们就可以用以下几行代码实现Max-heap

letMaxHeap = function () {
let heap = [null];
this.insert = function (num) {
heap.push(num);
if (heap.length>2) {
letidx = heap.length - 1;
while (heap[idx] > heap[Math.floor(idx / 2)]) {
if (idx>= 1) {
          [heap[Math.floor(idx / 2)], heap[idx]] = [
            heap[idx],
            heap[Math.floor(idx / 2)],
          ];
if (Math.floor(idx / 2) >1) {
idx = Math.floor(idx / 2);
          } else {
break;
          }
        }
      }
    }
  };

this.remove = function () {
let smallest = heap[1];
if (heap.length>2) {
      heap[1] = heap[heap.length - 1];
heap.splice(heap.length - 1);
if (heap.length == 3) {
if (heap[1] < heap[2]) {
          [heap[1], heap[2]] = [heap[2], heap[1]];
        }
return smallest;
      }
leti = 1;
let left = 2 * i;
let right = 2 * i + 1;
while (heap[i] <= heap[left] || heap[i]  heap[right]) {
          [heap[i], heap[left]] = [heap[left], heap[i]];
i = 2 * i;
        } else {
          [heap[i], heap[right]] = [heap[right], heap[i]];
i = 2 * i + 1;
        }
        left = 2 * i;
        right = 2 * i + 1;
if (heap[left] == undefined || heap[right] == undefined) {
break;
        }
      }
    } elseif (heap.length == 2) {
heap.splice(1, 1);
    } else {
returnnull;
    }
return smallest;
  };
this.show = function () {
console.log(heap);
  };

};

就这样,你已经成功地在JavaScript中实现了二进制堆。

结论

二进制堆是二进制树的顶层实现,其条件是每个父节点至少有两个子节点,并且是二进制树的完整结构。这意味着树的层次将从左边或左子开始填充,然后是右边。

二进制堆是高级数据结构的一部分,有两种类型的二进制树:其中一种被称为最小堆,而另一种被称为最大堆。在min-heap中,父节点的值比它们的子节点小,兄弟姐妹节点的值并不重要。

同样,在最大堆中,父节点的值总是大于它们的子节点,兄弟节点的值也不重要。在这篇文章中,我们了解了堆和它们在vanilla javascript中的实现,最后我们测试了我们的实现。