算法学习in js:基于二叉堆的优先队列

682 阅读6分钟

前言

对于应用程序而言,经常需要有序的元素,但是有时他们并不要求完全有序,而仅仅要求其中最大或最小的一个元素。同时,数组也可能随时添加新的元素,接着再取其中最大或最小的元素。

举个例子,操作系统中对事件的处理,每次都会处理优先级最高的事件,同时新的事件又可能不断地推入。

当然你可以说每次推入时重新进行排序,但是这样是否会效率太低呢?是否有更高效的方法?

抽象出来,我们需要的数据结构应该满足如下两个操作:删除(取出)最大元素插入元素。这种数据类型便是优先队列

优先队列是一种抽象数据类型,本文我们将实现一个基于二叉堆的优先队列。

以下是代码先放:CodePen打开

二叉堆定义

二叉堆(英语:binary heap)是一种特殊的堆,二叉堆是完全二叉树或者是近似完全二叉树。二叉堆满足堆特性:父节点的键值总是保持固定的序关系于任何一个子节点的键值,且每个节点的左子树和右子树都是一个二叉堆。
当父节点的键值总是大于或等于任何一个子节点的键值时为“最大堆”。当父节点的键值总是小于或等于任何一个子节点的键值时为“最小堆”。——摘自维基百科——二叉堆

看图例

有同学可能会有疑问,那堆和树又有什么关系,在知乎上看到一张很好的图


对一棵二叉树来说,二叉树的每个节点大于等于(或小于等于)它的子节点时,它被称为堆有序

简单说,二叉堆是一组堆有序的二叉树,并在数组中按照层级存储。

二叉堆有如下特性可以帮助我们实现优先队列:

  • 数组无需指针,可直接通过下标访问
  • k节点的父节点为下标int( k/2 ),两个子节点下标为2*k,2*k+1(**注意:**数组0位置不放元素,从1位置开始放)
  • 一颗大小为N的二叉树高度为lgN(向下取整)

此文中我用最大堆作为例子

二叉堆的算法

了解二叉堆定义和特性后回到之前想要实现的操作:删除最大元素插入元素

对于删除最大元素,我们想要实现的效果是:每次删除最大元素后,经过调整,堆的根节点仍然是最大元素。

对于插入元素,我们想要实现的效果是:每次插入元素后,经过调整,堆的根节点任然是最大元素。

两个操作在我们调整之前均使得堆的有序性被破坏,而我们需要将堆的状态重新恢复,就要进行调整,这个调整的过程就叫做堆的有序化(reheapifying)

那么我们的思路就变成这样:

  • 删除最大元素:删除根节点——>有序化——>堆状态恢复
  • 插入元素:插入节点——>有序化——>堆状态恢复

由此,现在我们来认识有序化,在学会有序化后,删除最大元素与插入元素的操作也就不难了。

堆的有序化

有序化过程中会有两种情况:

  1. 某个节点优先级上升(比如在堆底插入一个优先级很高的元素),就需要由下至上恢复堆
  2. 某个节点优先级下降(比如根节点替换为一个优先级较低的元素),就需要由上至下恢复堆

由下至上恢复堆(上浮,swim)

某个节点的优先级提升,它打破了原来的堆秩序,我们需要将他**上浮(swim)**至适当的位置,如此便能恢复原来的堆秩序。

首先,此节点和父节点比较,若比父节点更大,则与此父节点交换;交换后,再与新的父节点比较,若比新的父节点大,则继续交换......直至遇到比其大的父节点或成为根节点。如图所示

此部分的代码如下

  swim(k) {
    //上浮
    while (k > 1 && this.less(Math.floor(k / 2), k)) {
      this.exch(Math.floor(k / 2), k);
      k = Math.floor(k / 2);
    }
  }

由上至下恢复堆(下沉,sink)

某个节点的优先级下降了,它打破了原来的堆秩序,我们需要将他**下沉(sink)**至适当的位置,如此便能恢复原来的堆秩序。

在下沉的过程中,同时需要比较两个子节点的大小,选择较大者与原节点交换。

此部分代码如下

  sink(k) {
    //下沉
    while (2 * k <= this.pqIndex) { //this.pqIndex表示现指向的空位置或超出数组原容量的新位置
      let j = 2 * k;
      j < this.pqIndex && this.less(j, j + 1) ? j++ : null; //寻找两个子节点最大的
      if (!this.less(k, j)) {
        //若上面比下面都大,则下沉完成
        break;
      }
      this.exch(k, j);
      k = j;
    }
  }

插入元素与删除最大元素

在了解有序化及其上浮下沉操作后,我们把它应用在插入元素与删除最大元素之中

对于插入元素,我们仅需把元素插入,并对其上浮即可

  insert(ele) {
    this.pq[this.pqIndex] = ele;
    this.swim(this.pqIndex); //对插入元素上浮
    this.pqIndex++;
  }

对于删除最大元素,将根节点(假设值为A)与最末位节点(假设值为B)交换,将A删除,然后对新的根节点(B)进行下沉

  delMax() {
    //删除并返回最大元素
    this.pqIndex--;
    const maxEle = this.pq[1]; //记录最大元素
    const lastEle = this.pq[this.pqIndex]; //末尾元素
    this.pq[1] = lastEle; //将末尾元素置于根节点
    this.pq[this.pqIndex] = null; //删除交换后的根节点
    this.sink(1);
    return maxEle;//返回已被删除的最大元素
  }

总结

如上基于二叉堆的优先队列,使得我们对于插入元素操作不需要超过(lgN + 1)次比较,删除最大元素操作不需要超过2lgN次比较。。在现代计算中,输入的N极大非常常见,相对于之前的重排序方案效率上可谓极大的提升。

同时,此基于二叉堆的优先队列排序可以进一步引申出堆排序

参考资料

《算法(第4版)》