了解最小堆与最大堆
堆是一种基于树的数据结构,允许在恒定时间内访问树中的最小和最大元素。所用的恒定时间是大O(1)。这与存储在堆中的数据无关。
有两种类型的堆。最小堆和最大堆。Min-heap用于访问堆中的最小元素,而Max-heap则用于访问堆中的最大元素。
前提条件
要跟上这篇文章,你应该具备以下条件。
-
在你的电脑上安装[Node.js]。
-
有JavaScript的基本知识。
建立项目
要建立这个项目,请克隆这个GitHub仓库。克隆后,你会发现两个文件夹:start和final。在本文中,我们将在start文件夹中工作,但如果你遇到了错误,请随时查看final文件夹中的最终代码。
迷你堆
在最小堆中,父节点或根节点通常比子节点的价值要小。最小的元素在恒定的时间内被访问,因为它的索引是1 。

根据下图,在每一级,最小的数字是父节点。
实施
在说明最小堆的时候,我们使用的是基于树的结构。但当存储在内存中时,我们使用基于数组的结构。请看下图,它显示了基于树的和基于内存的表示方法。

在迷你堆中,第一个元素是null ,然后用下面的公式来排列元素。
-
父节点。
i -
左结点。
i * 2 -
右节点。
i * 2 + 1 -
在任何一个节点,你都可以通过以下方式找到父节点
i / 2 -
i是数组中的索引。
对于最小堆,我们将插入一个元素,获得最低的元素,并删除一个元素。
插入一个元素
在迷你堆中插入一个元素时,我们使用堆排序算法。
该算法的工作原理是,首先将要插入的元素推到数组的末尾,然后通过遍历为该元素找到正确的位置。
在minHeap.js 文件中,在insert() 函数下,我们增加了以下功能来插入一个元素。
function insert(node) {
heap.push(node);
if (heap.length > 1) {
let current = heap.length - 1;
while (current > 1 && heap[Math.floor(current / 2)] > heap[current]) {
//swapping values
[heap[Math.floor(current / 2)], heap[current]] = [
heap[current],
heap[Math.floor(current / 2)],
];
current = Math.floor(current / 2);
}
}
return;
}
//testing functionality
insert(10);
insert(90);
insert(36);
insert(5);
insert(1);
console.log(heap.slice(1));
预期的输出。
[ 1, 5, 36, 90, 10 ]
从上面的函数中。
-
将元素推到数组的末端。
-
检查数组中的元素数量是否多于1。如果是,就按照下面的步骤进行。
-
获取被插入元素的索引。
-
在数组中循环,检查是否有一个比插入的元素大的父元素。
-
如果存在,就把它们交换。
检索最小元素
在min-heap数据结构中,最小元素的索引是1 。
在同一个文件中,在getMin() 函数下,我们把功能加起来。
function getMin(){
return heap[1];
};
//testing functionality
insert(10);
insert(90);
insert(36);
insert(5);
insert(1);
console.log(getMin());
预期的输出。
1
从上面的代码片断中,我们得到了存储在索引1 的最小元素。
移除一个元素
从最小堆数据结构中删除一个元素包括以下步骤。
-
首先删除最小的元素。
-
调整min-heap以保留顺序。
在同一个文件中,在remove() ,我们把这些功能加起来。
function remove() {
if (heap.length > 2) {
//assign last value to first index
heap[1] = heap[heap.length - 1];
//remove the last value
heap.splice(heap.length - 1);
if (heap.length === 3) {
if (heap[1] > heap[2]) {
//swap them
[heap[1], heap[2]] = [heap[2], heap[1]];
}
return;
}
//get indexes
let parent_node = 1;
let left_node = parent_node * 2;
let right_node = parent_node * 2 + 1;
while (heap[left_node] && heap[right_node]) {
//parent node greater than left child node
if (heap[parent_node] > heap[left_node]) {
//swap the values
[heap[parent_node], heap[left_node]] = [
heap[left_node],
heap[parent_node],
];
}
//parent node greater than right child node
if (heap[parent_node] > heap[right_node]) {
// swap
[heap[parent_node], heap[right_node]] = [
heap[right_node],
heap[parent_node],
];
}
if (heap[left_node] > heap[right_node]) {
//swap
[heap[left_node], heap[right_node]] = [
heap[right_node],
heap[left_node],
];
}
parent_node += 1;
left_node = parent_node * 2;
right_node = parent_node * 2 + 1;
}
//incase right child index is undefined.
if (heap[right_node] === undefined && heap[left_node] < heap[parent_node]) {
//swap.
[heap[parent_node], heap[left_node]] = [
heap[left_node],
heap[parent_node],
];
}
}
// if there are only two elements in the array.
else if (heap.length === 2) {
// remove the 1st index value
heap.splice(1, 1);
} else {
return null;
}
return;
}
//testing functionality
insert(10);
insert(90);
insert(36);
insert(5);
insert(1);
remove();
console.log(heap.slice(1));
预期的输出
[ 5, 10, 36, 90 ]
从上面的函数中。
-
检查数组是否有两个以上的元素。如果没有,删除第一个索引中的元素。如果有,继续下面的步骤。
-
将最后一个值分配给第一个索引。
-
从数组中删除最后一个值。
-
检查该数组是否还有三个元素。如果是
true,检查第一个元素是否比第二个元素大。如果满足条件,就把它们交换。如果有三个以上的元素,继续下面的步骤。 -
定义父结点、左结点和右结点的索引。
-
循环浏览同时具有左子值和右子值的数组。如果父节点的值大于左节点的值或右节点的值,将它们交换。如果左节点的值大于右节点的值,也将它们交换。
-
如果没有右节点的值,但父节点的值大于左节点的值,则交换它们的值。
Max-heap
在一个最大堆中,父节点或根节点通常比子节点大。最大元素可以在恒定的时间内被访问,因为它的索引是1 。

根据上图,在每一级,最大的数字是父节点。
实施
同样地,当说明最大堆时,我们使用基于树的结构,但当在内存中表示时,我们使用基于数组的结构。请看下图,它显示了基于树的和基于内存的表示。

类似地,在一个最大堆中,第一个元素是null ,然后在排列元素时使用以下公式。
-
父节点。
i -
左边的节点。
i * 2 -
右边的节点。
i * 2 + 1 -
在任何节点,你都可以通过以下方式找到父节点
i / 2
i 是数组中的索引。
对于最大堆,我们将插入一个元素,得到最大的元素,并删除一个元素。
插入一个元素
在最大堆中,我们在插入元素时也使用堆排序算法。
在maxHeap.js 文件中,在insert() 函数下,我们增加了以下功能来插入元素。
function insert(node) {
//insert first at the end of the array.
heap.push(node);
if (heap.length > 1) {
//get index
let current = heap.length - 1;
//Loop through checking if the parent is less.
while (current > 1 && heap[Math.floor(current / 2)] < heap[current]) {
//swap
[heap[Math.floor(current / 2)], heap[current]] = [
heap[current],
heap[Math.floor(current / 2)],
];
//change the index
current = Math.floor(current / 2);
}
}
}
//testing functionality
insert(10);
insert(100);
insert(120);
insert(1000);
console.log(heap.slice(1));
预期的输出。
[ 1000, 120, 100, 10 ]
从上面的函数中。
-
将元素推到数组的末端。
-
检查数组中是否有多于一个元素。如果有,继续下面的步骤。
-
获取该元素的位置索引。
-
在数组中循环,检查是否有一个父节点的值小于插入的元素。
-
如果有,交换数值并更新数组中元素的索引。
获取最大的元素
在一个最大堆中,获取最大的元素意味着访问索引为1 的元素。
在同一个文件中,在getMax() 函数下,我们把这些功能加起来。
function getMax(){
return heap[1];
};
//testing functionality
insert(10);
insert(100);
insert(120);
insert(1000);
console.log(getMax());
预期的输出。
1000
在上面的函数中,我们要返回存储在索引1 的元素。
移除一个元素
从一个最大堆中移除一个元素包括以下步骤。
-
移除第一个元素,它通常是最大的。
-
按顺序重新排列其余的元素。
在同一个文件中,在remove() 函数下,我们将把这些功能加起来。
function remove() {
//check if we got two elements in heap array.
if (heap.length === 2) {
//remove the Ist index value
heap.splice(1, 1);
} else if (heap.length > 2) {
//assign last value to first index
heap[1] = heap[heap.length - 1];
//remove the last item
heap.splice(heap.length - 1);
//check if the length is 3.
if (heap.length === 3) {
if (heap[2] > heap[1]) {
[heap[1], heap[2]] = [heap[2], heap[1]];
}
}
//setup needed indexes.
let parent_node = 1;
let left_node = parent_node * 2;
let right_node = parent_node * 2 + 1;
while (heap[left_node] && heap[right_node]) {
//parent node value is smaller than the left node value
if (heap[left_node] > heap[parent_node]) {
//swap
[heap[parent_node], heap[left_node]] = [
heap[left_node],
heap[parent_node],
];
//update the parent node index.
current = left_node;
}
if (heap[right_node] > heap[parent_node]) {
//swap
[heap[parent_node], heap[right_node]] = [
heap[right_node],
heap[parent_node],
];
//update the parent node index.
current = right_node;
}
if (heap[left_node] < heap[right_node]) {
//swap
[heap[left_node], heap[right_node]] = [
heap[right_node],
heap[left_node],
];
}
//update the left and right node
left_node = current * 2;
right_node = current * 2 + 1;
}
//no right child, but left child is greater than parent
if (heap[right_node] === undefined && heap[left_node] > heap[parent_node]) {
//swap
[heap[parent_node], heap[left_node]] = [
heap[left_node],
heap[parent_node],
];
}
} else {
return null;
}
return;
}
//testing functionality
insert(10);
insert(100);
insert(120);
insert(1000);
remove();
console.log(heap.slice(1));
从上面的函数中。
-
检查数组是否有两个以上的元素。如果没有,只需删除第一个索引中的元素。如果有,继续下面的步骤。
-
将最后一个值分配给第一个索引。
-
从数组中删除最后一个值。
-
检查该数组是否还有三个元素。如果是
true,检查索引二的元素是否比索引一的元素大。如果满足条件,将它们交换。如果剩余的元素超过三个,继续下面的步骤。 -
定义父结点、左结点和右结点的索引。
-
循环浏览有左节点值和右节点值的数组。如果父节点的值比左节点或右节点的值小,就把它们交换。另外,如果左节点的值小于右节点的值,就把它们交换一下。
-
如果没有右节点的值,但父节点的值小于左节点的值,则交换它们的值。
为什么我们需要堆?
- 降低时间复杂度。线性数据结构,如链接列表或数组可以访问大O(n)中存在的最小或最大元素,而堆可以访问大O(1)中存在的最小或最大元素。
这在处理大数据集时是至关重要的。n指的是数据集的数量。
堆的应用
-
它们已经被用于[操作系统]中,在优先级的基础上[进行工作调度]。
-
它们被用于[堆排序算法]中,以实现优先级队列。
-
在[Dijkstra算法]中用于寻找最短路径。
结论
随着时间复杂度的降低,min-heap和max-heap在处理数据集时很有效率,每一种都有自己的用例和实现。
在这篇文章中,我们已经涵盖了迷你堆、最大堆、我们为什么需要堆,以及堆的应用。