JavaScript 数据结构(十):二叉堆和堆排序

JavaScript 数据结构系列目录

JavaScript 数据结构(一): 数组

JavaScript 数据结构(二): 栈

JavaScript 数据结构(三):队列

JavaScript 数据结构(四):双端队列

JavaScript 数据结构(五):链表

JavaScript 数据结构(六):集合

JavaScript 数据结构(七):字典

JavaScript 数据结构(八):散列表

JavaScript 数据结构(九):树

JavaScript 数据结构(十):二叉堆和堆排序

JavaScript 数据结构(十一):图



一、二叉堆的概述

二叉堆是一种特殊的二叉树,其有以下两个特性。

  1. 它是一颗完全二叉树,表示树的每一层都有左侧和右侧子节点(除了叶节点),并且最后一层的叶节点都尽可能的是左侧子节点,这叫做结构特性
  2. 二叉堆不是最小堆就是最大堆。最小堆允许你快速导出树的最小值,最大堆则相反。所有节点都 >= 或 <= 每个它的子节点。这叫做堆特性

其下图展示了一些合法与不合法的堆。

在这里插入图片描述

尽管二叉堆是二叉树,但其不一定是二叉搜索树(BST)。在二叉堆中,每个子节点都要 >= 父节点(最小堆)或 <= 父节点(最大堆)。但在二叉搜索树中,左侧子节点总比父节点小,右侧子节点也总比父节点大。

二、创建最小堆类

老样子,我们声明一个最小堆类。

class MinHeap {
	constructor() {
        this.heap = [];

        this.comparFn = (a,b) => {
			if ( a === b ) return 0;
			return a < b ? -1 : 1;
		};
    }
}

在构造函数里,我们声明了两个遍历 heap 与 comparFn 。

其中 heap 用来存储数据,comparFn 用来进行基本的比较。

在接下来的案例中,我们将使用数组来存储数据。

1、二叉树的数组表示

二叉树有两种表示方法,第一种是使用动态的表示方法,即指针。

这个方法我们在 上一篇文章 使用过。

第二种是使用数组,通过索引值检索父节点、左侧和右侧子节点的值。

下图展示了两种不同的表示方法。

在这里插入图片描述
要访问数组的二叉树节点,我们可以使用如下操作来使用 position。

  1. 它的左侧子节点位置是 2 * position + 1(如果位置存在)。
  2. 它的左侧子节点位置是 2 * position + 2(如果位置存在)。
  3. 它的左侧子节点位置是 position / 2(如果位置存在)。

用上面的操作来访问特定节点,我们可以这样写。

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);
}

写完这三个获取节点的位置后,我们来补充一些方法。

2、insert 方法

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);

3、size、isEmpty 与 findMinimum 方法

size() { return this.heap.length }

isEmpty() { return this.size() === 0 }

findMinimum() { return this.isEmpty() ? null : this.heap[0]; }

4、extract 方法

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啦。


相关文章

暂无

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值