[路飞]_前端算法第六十五弹-堆与队列

169 阅读8分钟

完全二叉树

在我们了解堆之前我们先了解一下完全二叉树,由于完全二叉树的性质,完全二叉树是可以用连续的数组空间进行储存的。所以完全二叉树的父子节点之间,可以用有序编号进行表示。子结点的编号也可以通过父节点的编号进行计算得到,所以完全二叉树没有必要去存储边的信息。

  • 编号为i的结点:(编号规则从1开始)
    • 左子节点的编号:2i+1
    • 右子结点的编号:2i+2
  • 可以有连续的空间存储

一维与二维的转换

如下图所示是一个完全二叉树

完全二叉树既然可以用一段连续的数组空间进行存储,那也就是意味着如上图这样的一个完全二叉树,实际上我们进行数据存储的时候是用的下方连续的数组空间进行存储的。下图中数组中相关位置的颜色对应的就是上方二叉树中相应颜色对应的相关节点。

接下来我们就要做一个思维方式上的转变,实际在写程序的时候我们用的是数组在进行存储。但是我们会把这段数组看成是一个树形结构。这表示我们任何一个数据结构,都有两层含义,一层是我们思维结构逻辑层面的,一层是我们实际的存储结构。树形图为思维逻辑结构(二维的树形结构),数组图为存储结构(一维的数组结构)。这就是我们看待同样事物不同的看法。在一般人眼中,他是一个以为数组,但是在一些人眼中,这是一个树形结构。下面我们就需要用到完全二叉树的思维逻辑去理解一段数组。

根据上面的数组,再根据完全二叉树的性质,我们不难画出一个树形结构。

其与上面的数组完全等价,可相互表示。

基于完全二叉树的一种数组结构。

堆的性质

堆有两个性质,大顶堆和小顶堆

  • 大顶堆:任意三元组根节点都大于两个子结点,我们叫这个堆为大顶堆
  • 小顶堆:如果任意一个三元组,根节点都小于任意一个子结点,我们称之为小顶堆

大顶堆中最大值应该是堆顶元素。根节点是任意子节点的祖先,大顶堆数值越向上越大。小顶堆恰好相反。

大顶堆第二大的子结点在哪?

大顶堆中,左子节点一定大于左子节点的任意子节点。右子结点一定大于右子结点下的任意子节点。但左右子节点的大小位置并不固定。所以一个大顶堆中第二大的子结点应该在第二层,为根节点的左右子节点中的任意一个。

在一个大顶堆中,第三大的值在哪里?

第三大的值意味着,在整个堆中,只有两个值大于目标值。反映到树形结构上,在堆上,只有父子之间有明确的大小关系,我们可以确定的是,在第三层上的任意元素都至少有两个元素大于他,那就是其位于第二层的父节点和位于第一层的祖父节点。其兄弟节点和其叔伯节点与其值的大小关系并不确定,所以我们能确定是的第三大的值应该在该大顶堆的第二或第三层中,而不能确定其具体位置。

同理可得,一个大顶堆第四大的值应该位于该大顶堆的第二到第四层中。

堆的插入

当想像一个堆中插入一个元素时,我们必须从其尾部插入,并向上进行调整。

想在堆中心插入一个数,相当于给完全二叉树新增一个节点。对于一个完全二叉树,新增一个节点,应该放置于最后一层的最左侧第一个空节点位置。相当于完全二叉树所对应的数组的末尾位置。在一个完全二叉树末尾新增一个节点,等价于在数组的末尾新增加一个元素。由于新添加的元素有可能违反堆的性质,这时我们需要进行相关的性质调整。

调整元素插入位置

调整过程中,新添加元素,将与其父元素进行对比,如果大于其父元素,则互换位置,并继续向上比较,直至新添加元素小于其父元素,确定其位置。

从而完成大顶堆的元素插入操作。

堆的弹出

堆弹出的元素,就是堆顶元素,对于大顶堆弹出的就是堆的最大值或者称作弹出的是数组中的最大值。当前最大值被弹出后,堆顶出现了空余。

由于堆是一个完全二叉树,当堆顶弹出一个元素,按上图假设堆中原有10个元素,此时弹出堆顶后,还剩余9个元素,堆应该占用数组的前九位。此时需将最末位元素提升至堆顶空位处。

经过一系列的替换之后,大顶堆重新满足堆的性质。

堆的排序

  1. 将堆顶元素与堆尾元素交换
  2. 将此操作看做是对顶元素弹出操作
  3. 按照头部弹出以后的策略调整堆

优先队列

普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高级先出的行为特征。通常采用堆数据结构来实现。

堆每次出的元素都是所有元素中的最大(小)值。如果将数值的大小关系,当做是元素的优先级的话,每次出队列的元素就是所有元素中优先级最高的元素,我们将这种队列称之为优先队列

堆是优先队列的一种实现方式。

一句话理解:堆

堆适合维护:集合最值

堆的代码实现

对于一个大顶堆,我们需要实现以下功能,堆的插入,堆的弹出,查看堆的顶部元素,堆的大小。

我们先实现一个类。

class LargeHeap {
  constructor() {
    this.cmp = new Array();
    this.count = 0;
  }
}

cmp用于记录堆,count记录堆的大小。

下面我们来实现push插入功能。

push (val) {
    let cmp = this.cmp
    cmp[this.count] = val;
    let index = this.count;
    this.count++;
    // index为val的下标,根据定义,需要将x向上进行调整
    // 假设当前节点的下标为i,其左孩子的下标为2i+1,右孩子的结点为2i+2;
    // ((2i+1)-1)/2=i,((2i+2)-1)/2=i,通过子结点寻找父节点
    // 下标从零还是从一开始,子结点的下标有区别
    let pre = Math.floor((index - 1) / 2);
    while (index != 0 && cmp[pre] < val) {
      [cmp[pre], cmp[index]] = [cmp[index], cmp[pre]]
      index = pre
      pre = Math.floor((index - 1) / 2);
    }
    this.cmp = cmp
    return;
  }

pop弹出功能的实现

pop () {
    // 弹出操作,将最后一位的元素值,提升到第一位
    if (this.size() == 0) return;
    let cmp = this.cmp;
    let num = cmp[0];
    [cmp[0], cmp[this.count - 1]] = [cmp[this.count - 1], cmp[0]];
    this.count--;
    let index = 0;
    let n = this.count - 1;
    while (index * 2 + 1 <= n) {
      let temp = index;
      if (cmp[temp] < cmp[index * 2 + 1]) temp = index * 2 + 1;
      if (index * 2 + 2 <= n && cmp[temp] < cmp[index * 2 + 2]) temp = index * 2 + 2;
      if (cmp[temp] == cmp[index]) break;
      [cmp[temp], cmp[index]] = [cmp[index], cmp[temp]];
      index = temp
    }
    this.cmp = cmp;
    return num;
  }

查看顶部元素和查看堆的大小的功能

// 获取堆顶元素
  top () {
    return cmp[0]
  }

  // 获取堆的大小
  size () {
    return this.count;
  }

完整代码

class LargeHeap {

  constructor() {
    this.cmp = new Array();
    this.count = 0;
  }

  push (x) {
    let cmp = this.cmp
    cmp[this.count] = x;
    let index = this.count;
    this.count++;
    // index为x的下标,根据定义,需要将x向上进行调整
    // 假设当前节点的下标为i,其左孩子的下标为2i+1,右孩子的结点为2i+2;
    // ((2i+1)-1)/2=i,((2i+2)-1)/2=i,通过子结点寻找父节点
    // 下标从零还是从一开始,子结点的下标有区别
    let pre = Math.floor((index - 1) / 2);
    while (index != 0 && cmp[pre] < x) {
      [cmp[pre], cmp[index]] = [cmp[index], cmp[pre]]
      index = pre
      pre = Math.floor((index - 1) / 2);
    }
    this.cmp = cmp
    return;
  }

  pop () {
    // 弹出操作,将最后一位的元素值,提升到第一位
    if (this.size() == 0) return;
    let cmp = this.cmp;
    let num = cmp[0];
    [cmp[0], cmp[this.count - 1]] = [cmp[this.count - 1], cmp[0]];
    this.count--;
    let index = 0;
    let n = this.count - 1;
    while (index * 2 + 1 <= n) {
      let temp = index;
      if (cmp[temp] < cmp[index * 2 + 1]) temp = index * 2 + 1;
      if (index * 2 + 2 <= n && cmp[temp] < cmp[index * 2 + 2]) temp = index * 2 + 2;
      if (cmp[temp] == cmp[index]) break;
      [cmp[temp], cmp[index]] = [cmp[index], cmp[temp]];
      index = temp
    }
    this.cmp = cmp;
    return num;
  }

  // 获取堆顶元素
  top () {
    return cmp[0]
  }

  // 获取堆的大小
  size () {
    return this.count;
  }

}

对于小顶堆,只需要判断方向与之相反即可。