二叉堆与优先队列

二叉堆与优先队列二叉堆 定义 二叉堆是完全二叉树,且满足所有节点都≥(最大堆)或者≤(最小堆)其子节点的条件。 特点 所有节点都≥(最大堆)或者≤(最小堆)其子节点 存储方式 使用数组来存储二叉堆,从根节点出发,按根

二叉堆

定义

二叉堆是完全二叉树,且满足所有节点都≥(最大堆)或者≤(最小堆)其子节点的条件。

特点

  • 所有节点都≥(最大堆)或者≤(最小堆)其子节点

存储方式

使用数组来存储二叉堆,从根节点出发,按根节点-左子节点-右子节点的顺序依次存入数组中,如下图所示

image.png

使用数组来存储二叉堆,有如下特点:

节点索引index,可以获取其子节点的索引

  • 左子节点的索引是 2 * index + 1
  • 右子节点的索引是 2 * index + 2

节点索引index,可以获取其父节点的索引

  • 父节点的索引是 (index - 1) / 2

二叉堆的操作

二叉堆的自调整

上浮

如果节点不满足二叉堆的要求,则二叉树会自动调整自身结构,上浮操作是指节点从下到上调整位置

  // 上浮操作
  /** * * @param {*} arr : 二叉堆的物理底层存储数组 */
  // 这里使用了循环结构,也可以使用递归,每次递归都去比较子节点与父节点并交换双方的位置
  shiftUp(arr) {
    let childIndex = arr.length - 1;
    let parentIndex = Math.floor((childIndex - 1) / 2);
    const temp = arr[childIndex];
    // 开始上浮操作, 循环直到堆顶或者父节点不再大于子节点
    while(childIndex > 0 && arr[parentIndex] > temp) {
      // 子节点的值改写为父节点(即父节点下移)
      arr[childIndex] = arr[parentIndex];
      // 更新子节点的索引(即子节点跟父节点交换位置)
      childIndex = parentIndex;
      // 更新父节点的索引,下次循环继续比较父节点
      parentIndex = Math.floor((childIndex - 1) / 2);
    }
    // 循环结束后,将新节点的值赋予上浮操作的终点节点;
    arr[childIndex] = temp;
  }

下沉

  // 下沉操作
  /** * * @param {*} arr * @param {*} : 要下沉的节点 */
  shiftDown(arr, index) {
    let temp = arr[index];
    let parentIndex = index;
    let childIndex = 2 * parentIndex + 1;
    while (childIndex < arr.length) {
      // 首先要判断父节点与左右子节点的哪个进行交换, 要找到左右节点中更小的
      if (childIndex + 1 < arr.length && arr[childIndex + 1] < arr[childIndex]) {
        // 右节点更小,则让childIndex指向右节点
        childIndex ++;
      }
      if (temp < arr[childIndex]) {
        // 父节点比子节点更小,无需交换,直接退出循环
        break;
      }
      arr[parentIndex] = arr[childIndex];
      parentIndex = childIndex;
      childIndex = 2 * parentIndex + 1;
    }
    arr[parentIndex] = temp;
  }

插入节点

插入节点的操作可以看做是将新节点作为叶子节点插入,然后开始二叉堆自身调整的’上浮’操作。

  // 插入节点,时间复杂度为O(log(n)),其中n为二叉堆的容量(节点树)
  // 因为插入操作每次上浮一层,容量为n的二叉堆最大的可能上浮层数为log(n)
  /** * * @param {*} arr : 二叉堆的物理底层存储数组 * @param {*} node :要插入的新节点, 节点的值即为node */
  insert(arr, node) {
    arr.push(node);
    this.shiftUp(arr);
  }

删除节点

删除节点,即将二叉堆的堆顶节点删除,具体操作是让堆的最后一个节点顶替堆顶节点,然后执行下沉操作

  // 删除节点, 从堆顶删除节点,时间复杂度同样为o(log(n))
  /** * * @param {*} arr : 二叉堆,这里是用数组来实现物理存储 */
  pop(arr) {
    let res = this.arr[0];
    this.arr[0] = this.arr.pop();
    this.shiftDown(this.arr, 0);
    return res;
  }

构建二叉堆

将一个无序的数组构建成一个二叉堆,包含了两种方式

依次将无序数组的元素插入二叉堆

constructor(array) {
    this.arr = [];
    for(let i = 0; i < array.length; i ++) {
      this.insert(this.arr, array[i]);
    }
  }
时间复杂度分析

这种方式下的时间复杂度为O(nlog(n)), 推导如下:

同一层的节点的最多交换次数是相同的,最后一层的叶子节点不需要执行交换,因为它的下面没有节点了。时间复杂度即为:O(sum(交换次数)) = O(每层节点数 * 该层节点的最多交换次数)。

n个节点的二叉堆层数为k = log(n)

S = 0 + 1 + 1 + 2 + 2 + 2 + 2 + ... + k + k + ... + k; (2个1,4个2,...., k-1个k)
求得S = k * 2^k - 2^(k-1) + 2;S = nlog(n) - n/2 + 2;

将无序数组看做是不符合规则的二叉堆,从最后一个非叶子节点开始依次下沉操作

constructor(array) {
  this.arr = array;
  for(let i = Math.floor((this.arr.length - 1) / 2); i >=0; i --) {
    this.shiftDown(this.arr, i);
  }
}
时间复杂度分析

这种方式下的时间复杂度为O(n),推导如下:

同一层的节点的最多交换次数是相同的,最后一层的叶子节点不需要执行交换,因为它的下面没有节点了。时间复杂度即为:O(sum(交换次数)) = O(每层节点数 * 该层节点的最多交换次数)。 n个节点的二叉堆层数为k = log(n), 层数d与d层的节点数dSum满足节点数dSum = 2 ^ (d - 1), 层数d的节点最多交换次数为log(k-d),因此总的交换次数满足如下公式:

S = 2^0 * (k - 1) + 2^1 * (k -2) + ... + 2^(k-2) * [k- (k - 1)];
2S =                2^1 * (k - 1)+ ... + 2^(k-2) * [k - (k - 2)] + 2^(k-1) * [k - (k - 1)];
两式相减得到
S = 2^k - k - 1;
由于k = log(n),因此
S = n - log(n) - 1;

所以时间复杂度为O(S) = O(n);

两种方式在于会不会改变传入的数组。

二叉堆的应用

  • 高效快速找到最大值或最小值(时间复杂度O(1))
  • 找出第k个最大(小)元素(构建一个二叉堆,节点树为k(堆的容量为k))
  • 找出前k个最大(小)元素(构建一个二叉堆,节点树为k(堆的容量为k))

找出第k个最大元素(见LeetCode 215)

  • 构建一个最小堆,容量为k,将元素依次插入堆中
  • 当堆的容量超过k时,删除堆顶,保持堆的容量不变
  • 当所有元素都插入完毕时,堆顶元素即为第k个最大元素

找出前k个高频元素(见LeetCode 347)

优先队列

定义

  • 最大优先队列:队列不再是先进先出,不管入队情况如何,值最大的元素先出队
  • 最小优先队列:队列不再是先进先出,不管入队情况如何,值最小的元素先出队

实现

最大(小)优先队列可以使用最大(小)堆来实现。

// 最大优先队列,使用最大堆来实现
class PriorityQueue {
  /** * * @param {*} n : 队列长度 */
  constructor (n) {
    this.arr = [];
    this.size = n;
  }
  enQueue(element) {
    this.insert(this.arr, element);
    if (this.arr.length > this.size) {
      this.size ++;
    }
  }
  deQueue() {
    if (this.arr.length === 0) {
      throw new Error('no element!');
    }
    return this.pop();
  }
}

今天的文章二叉堆与优先队列分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/22497.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注