Skip to content

约 1504 字大约 5 分钟

2019-06-08

本篇仅讨论 二叉堆

概述

堆(heap) 是一种完全二叉树结构,满足以下性质:

  • 堆序性:每个节点的值必须满足特定顺序关系

    • 最大堆:父节点值 ≥ 子节点值(根节点最大)
    • 最小堆:父节点值 ≤ 子节点值(根节点最小)
  • 结构完整性:除最后一层外,其他层节点必须全满,且最后一层节点靠左排列

堆的实现

插入操作

插入操作 是指向二叉堆中插入一个元素,要保证插入后也是一棵完全二叉树。

最简单的方法就是,最下一层最右边的叶子之后插入。如果最下一层已满,就新增一层。 插入之后如果不满足堆性质,则采用 向上调整

如果这个节点的权值大于它父节点的权值,就交换,重复此过程直到不满足或者到根。 可以证明,插入之后向上调整后,没有其他接点会不满足堆性质。

向上调整 的时间复杂度是 O(logn)O(\log n)

插入操作

删除操作

删除操作 指删除堆中最大的元素,即删除根结点。

但是如果直接删除,则变成了两个堆,难以处理。 所以不妨考虑 插入操作的逆过程,设法将根节点移到最后一个结点,然后直接删掉。 然而实际上不好做,我们通常采用的方法是,把根节点和最后一个节点直接交换。 于是直接删掉(在最后一个节点处的)根结点,但是新的根节点可能不满足堆性质。 这时候可以采用 向下调整 :

在该节点的子节点中,找一个最大的,与该节点交换,重复此过程直到底层。 可以证明,删除并向下调整后,没有其他节点不满足堆性质。

时间复杂度 O(logn)O(\log n)

核心特性

  • 数组表示:堆通常使用数组存储(利用完全二叉树特性)

    索引计算(设当前索引为 i):

    parentIndex = Math.floor((i - 1) / 2)
    leftChildIndex = 2 * i + 1
    rightChildIndex = 2 * i + 2

最大堆实现

最小堆实现

最小堆实现进需要在 最大堆 的基础上进行修改,调整比较逻辑:

class MinHeap {
  private heap: number[] = []

  // ...(索引计算和swap方法同MaxHeap)

  private siftUp() {
    let index = this.heap.length - 1
    while (index > 0) {
      const parentIndex = this.getParentIndex(index)
      if (this.heap[index] < this.heap[parentIndex]) {
        this.swap(index, parentIndex)
        index = parentIndex
      }
      else {
        break
      }
    }
  }

  private siftDown() {
    let index = 0
    const length = this.heap.length

    while (true) {
      const leftChildIndex = this.getLeftChildIndex(index)
      const rightChildIndex = this.getRightChildIndex(index)
      let smallest = index

      if (leftChildIndex < length
        && this.heap[leftChildIndex] < this.heap[smallest]) {
        smallest = leftChildIndex
      }

      if (rightChildIndex < length
        && this.heap[rightChildIndex] < this.heap[smallest]) {
        smallest = rightChildIndex
      }

      if (smallest !== index) {
        this.swap(index, smallest)
        index = smallest
      }
      else {
        break
      }
    }
  }

  // 其他方法与MaxHeap类似
}

时间复杂度

操作时间复杂度说明
insert()O(logn)O(log n)最坏情况下上浮整棵树高度
extractMax()O(logn)O(log n)最坏情况下下沉整棵树高度
peek()$O(1) $直接访问数组首元素
buildHeap()O(n)O(n)Floyd 算法自底向上堆化
heapSort()O(nlogn)O(n log n)每次 extractMax 为 O(logn)O(log n)

注意

虽然单个插入操作是 O(logn)O(log n) ,但将 n 个元素插入空堆的总体时间复杂度是 O(nlogn)O(n log n) ,而 Floyd 建堆算法只需 O(n)O(n)

适用场景

  • 优先队列

    class PriorityQueue {
      private heap = new MaxHeap()
    
      enqueue(val: number) { this.heap.insert(val) }
      dequeue() { return this.heap.extractMax() }
    }
  • 堆排序

    • 时间复杂度:O(n log n)
    • 空间复杂度:O(1)(原地排序)
    const arr = [4, 10, 3, 5, 1]
    const sorted = MaxHeap.heapSort(arr) // [1, 3, 4, 5, 10]
  • Top K 问题

    function findTopK(nums: number[], k: number): number[] {
      const minHeap = new MinHeap() // 最小堆实现类似
      for (const num of nums) {
        minHeap.insert(num)
        if (minHeap.size() > k)
          minHeap.extractMin()
      }
      return minHeap.toArray()
    }
  • Dijkstra 算法:优先队列优化最短路径搜索

相关问题

LeetCode - 堆(优先队列)

堆排序与选择问题

  • 703. 数据流中的第 K 大元素LeetCode简单
  • 215. 数组中的第K个最大元素LeetCode中等
  • 347. 前 K 个高频元素LeetCode中等

多堆结构与复杂规则处理

  • 23. 合并 K 个升序链表LeetCode困难
  • 295. 数据流的中位数LeetCode困难
  • 239. 滑动窗口最大值LeetCode困难

综合场景

  • 313. 超级丑数LeetCode中等
  • 786. 第 K 个最小的素数分数LeetCode中等
  • 871. 最低加油次数LeetCode中等