JavaScript 数据结构(一): 数组
JavaScript 数据结构(二): 栈
JavaScript 数据结构(三):队列
JavaScript 数据结构(四):双端队列
JavaScript 数据结构(五):链表
JavaScript 数据结构(六):集合
JavaScript 数据结构(七):字典
JavaScript 数据结构(八):散列表
JavaScript 数据结构(九):树
JavaScript 数据结构(十):二叉堆和堆排序
JavaScript 数据结构(十一):图
二叉堆是一种特殊的二叉树,其有以下两个特性。
其下图展示了一些合法与不合法的堆。
尽管二叉堆是二叉树,但其不一定是二叉搜索树(BST)。在二叉堆中,每个子节点都要 >= 父节点(最小堆)或 <= 父节点(最大堆)。但在二叉搜索树中,左侧子节点总比父节点小,右侧子节点也总比父节点大。
老样子,我们声明一个最小堆类。
class MinHeap {
constructor() {
this.heap = [];
this.comparFn = (a,b) => {
if ( a === b ) return 0;
return a < b ? -1 : 1;
};
}
}
在构造函数里,我们声明了两个遍历 heap 与 comparFn 。
其中 heap 用来存储数据,comparFn 用来进行基本的比较。
在接下来的案例中,我们将使用数组来存储数据。
二叉树有两种表示方法,第一种是使用动态的表示方法,即指针。
这个方法我们在 上一篇文章 使用过。
第二种是使用数组,通过索引值检索父节点、左侧和右侧子节点的值。
下图展示了两种不同的表示方法。
要访问数组的二叉树节点,我们可以使用如下操作来使用 position。
用上面的操作来访问特定节点,我们可以这样写。
getLeftIndex(position) {
return 2 * position + 1; }
getRightIndex(position) {
return 2 * position + 2; }
getParentIndex(position) {
if ( position === 0 ) return null;
return Math.floor((position - 1) / 2);
}
写完这三个获取节点的位置后,我们来补充一些方法。
insert(val) {
if ( val === null ) return false;
this.heap.push(val);
this.siftUp(this.heap.length - 1);
return true;
}
在这个方法里,我们先验证 val 值是否有效,无效的情况下我们不做任何操作。
有效的情况下,那我们将 val 值存放入数组里。
并调用上移操作代码。
siftUp(position) {
let parent = this.getParentIndex(position);
while ( position > 0 && this.comparFn(this.heap[parent], this.heap[position]) === 1 ) {
swap(this.heap,parent,position);
position = parent;
parent = this.getParentIndex(position);
}
}
siftUp 方法接受插入值的位置作为参数,之后我们使用这个参数获取插入值的父元素。
如果插入的值小于它的父节点,那么我们将这个元素和父节点进行交换。
我们会重复这个过程,直到堆的根节点也经过了这次交换节点和父节点位置的操作。
其交换函数如下。
function swap(arr,a,b) {
let temp = arr[a];
arr[a] = arr[b];
arr[b] = temp;
}
写到这后,我们来看看 insert 方法的实际操作,参考下面的对数据结构。
假设我们想要向堆中插入一个值 - 1。
那么该算法会进行一些少量的上衣操作,如下图所示。
const minHeap = new MinHeap();
minHeap.insert(2);
minHeap.insert(3);
minHeap.insert(4);
minHeap.insert(5);
minHeap.insert(1);
size() {
return this.heap.length }
isEmpty() {
return this.size() === 0 }
findMinimum() {
return this.isEmpty() ? null : this.heap[0]; }
extract() {
if ( this.isEmpty() ) return null;
if ( this.size() === 1 ) return this.heap.shift();
let removeValue = this.heap.shift();
this.siftDown(0);
return removeValue;
}
该方法移除数组中的第一元素(堆的根节点)。
使用该方法有三种场景。
第一种堆为空,这种情况下我们不需要做任何操作。
第二种堆内只有一个节点,即根节点,这种情况下我们只需要删除根节点即可。
第三种情况就是堆内有多个节点,这种情况下我们删除完根节点后,还需要调用下移操作代码。
其下移操作代码如下。
siftDown(position) {
let element = position,
left = this.getLeftIndex(position),
right = this.getRightIndex(position),
size = this.size();
if ( left < size && this.comparFn(this.heap[element],this.heap[left]) === 1 )
element = left;
if ( right < size && this.comparFn(this.heap[element],this.heap[right]) === 1 )
element = right;
if ( position !== element ) {
swap(this.heap,position,element);
this.siftDown(element);
}
}
siftDown 方法接收移除元素位置的位置作为参数。这里我们将 position 复制到 element 变量中。然后我们同样要获取左侧与右侧子节点的值。
下移操作同上移操作不同,该方法表示将元素和最小子节点和最大子节点进行交换。如果元素比左侧子节点小,那么我们就交换它与左侧子节点。如果元素小于右侧子节点,那么同样与右侧节点进行交换。
不过在找到最小子节点后,我们要验证是否与 element 相同。毕竟和自己交换是没有意义的。
如果不相同,那么我们就将它和最小的子节点交换,并递归到 element 被放到正确的位置上。
下图展示了这个过程。
最大堆类和最小堆类的算法一摸一样,不同之处只是我们要把所有 > 的比较换成 < 的比较。
其类如下。
class MaxHeap extends MinHeap {
constructor() {
super();
this.comparFn = reverseCompare((a,b) => {
if ( a === b ) return 0;
return a < b ? -1 : 1;
});
}
}
const reverseCompare = comparFn => {
return (a,b) => comparFn(b,a);
}
为了解决将所有的 > 比较换成 < 的比较。
我们只需要将 b 和 a 进行比较即可。
这意味着我们不需要去修改任何 MinHeap 里的代码。
那么,至此本篇就End啦。
暂无