优先队列详细介绍

2020-08-17 Algorithm Structure

堆 (Heap) 又被为优先队列 (Priority Queue),通过堆可以按照元素的优先级取出元素,而不是按照元素进入队列的先后顺序取出元素。

这里详细介绍其实现。

简介

堆的一个经典的实现是完全二叉树 (Complete Binary Tree),这样实现的堆成为二叉堆 (Binary Heap),二叉树要求前 n-1 层必须填满,第 n 层也必须按照从左到右的顺序被填满。

二叉堆的性质包括了:A) Max Heap 每个子树的 root key 要比两个 child key 大;B) Min Heap 每个子树的 root key 要比两个 child key 小。

但是二叉堆对左右两个元素的大小没有限制,如下以最小堆为例,示例如下。

                 0/2
          /               \
        1/31             2/4
      /     \           /     \
    3/17    4/23     5/20    6/10
   /   \
 7/22 8/8

在校验时,只需要遍历所有含叶子节点的节点,检查是否满足条件即可,叶子节点可以忽略。

PushDown

也有被称为 heapify() 的,就是从上到下保证满足堆的性质,一般用作删除。实际上就是对每个节点,选择 left 和 right 中 key 最小的节点,与该节点进行交换;然后依次类推,例如如下。

                 0/2
          /               \
        1/31             2/20
      /     \           /     \
    3/17    4/23     5/4      6/10
   /   \
 7/22 8/8

将 1 作为起点,最小的是 3/17 ,那么与之交换,成为。

                 0/2
          /               \
        1/17             2/20
      /     \           /     \
    3/31    4/23     5/4      6/10
   /   \
 7/22 8/8

再以被交换的 3 节点,此时需要与 8 交换,变成。

                 0/2
          /               \
        1/17             2/20
      /     \           /     \
    3/8    4/23     5/4      6/10
   /   \
 7/22 8/31

注意,此时只是保证添加的元素满足堆条件,例如 31 已经添加到了合适位置,但是对于 3/8 却不满足条件。也就是说,通过单次 PushDown 操作不一定会同时满足条件。

PushUp

从底层向上,一般用于添加,例如将 4 的 15 替换为 3 ,那么就可以逐层向上检查。

                0/4
          /               \
        1/7               2/10
      /     \           /     \
    3/8    4/3        5/20   6/12
   /
 7/22

构建

直接通过数组构建时,因为只有一半的节点是有叶子节点的,所以,只需要检查一半的节点确保其满足条件即可,详见 heap_node_max() 实现,例如如下节点,返回 4 。

                  0
          /               \
         1                 2
      /     \           /     \
     3       4        5        6
   /   \   /
  7    8   9

构建时执行 PushDown 即可,构建过程以如下示例为例。

----- 原始数组如下
                0/12
          /               \
        1/15             2/20
      /     \           /     \
    3/22   4/2        5/4      6/10
   /   \
 7/7  8/8

----- 首先从 3 开始
                0/12
          /               \
        1/15             2/20
      /     \           /     \
    3/7    4/2        5/4      6/10
   /   \
 7/22 8/8

----- 然后检查 2 节点
                0/12
          /               \
        1/15             2/4
      /     \           /     \
    3/7    4/2        5/20   6/10
   /   \
 7/22 8/8

----- 接着是 1 节点
                0/12
          /               \
        1/2              2/4
      /     \           /     \
    3/7    4/15       5/20   6/10
   /   \
 7/22 8/8

----- 最后一个 0 节点
                0/2
          /               \
        1/12             2/4
      /     \           /     \
    3/7    4/15       5/20   6/10
   /   \
 7/22 8/8

Extract

获取根节点,这跟是最大堆还是最小堆有关,移除根节点之后,把最后一个节点放到根节点,移动之后可能会违反堆的特性,所以,需要利用 DownPush 进行检查。

                0/2
          /               \
        1/7               2/4
      /     \           /     \
    3/8    4/15       5/20   6/10
   /   \
 7/22 8/12

移除第一个节点后,将最后一个节点替换到最开始节点,然后执行 PushDown 操作。

                0/12
          /               \
        1/7               2/4
      /     \           /     \
    3/8    4/15       5/20   6/10
   /
 7/22

最终结果是。

                0/4
          /               \
        1/7               2/10
      /     \           /     \
    3/8    4/15       5/20   6/12
   /
 7/22

Insert

只需要添加到最后,然后 PushUp 即可。

Remove

当删除节点的数值时,原来的位置就会出现一个孔,填充这个孔的方法是把最后的叶子的值赋给该孔,并下调到合适位置,最后把该叶子删除。

实现

因为是完全二叉树,通常通过数组完成,对于 n 叉堆来说,下标为 x 的元素,其孩子节点的下标范围是 [nx+1, nx+n],比如 2 叉堆,下标为 x 的元素,其孩子节点的下标为 2x+12x+2

如下简单以二叉堆作为示例,有如下特点 (中括号假设开始为 1 ,小括号的开始为 0 ):

  • 堆的根节点存放在数组位置 array[1] (0) 的地方;
  • 父节点 i 的左子节点在位置 array[2*i] (2*i+1)
  • 父节点 i 的右子节点在位置 array[2*i+1] (2*i+2)
  • 子节点 i 的父节点在位置 array[floor(i/2)] (floor((i-1)/2))
  • 最多非子节点数为 floor(i/2) floor((i - 1)/2)

上述的 floor() 表示取整,例如 5/2=2 。在实现时,可以将 0 作为临时存储,那么第一个元素就可以 1 开始。

                  0                                       1
          /               \                        /              \
         1                 2                      2                3
      /     \           /     \               /      \          /      \
     3       4        5        6             4        5        6         7
   /   \   /   \    /   \    /   \         /   \    /   \    /    \    /    \
  7    8   9   10  11   12  13   14       8    9   10   11  12    13  14    15

             C 语言格式                                正常格式

在实现时,包括了两个主要操作:A) UpHeap 在添加的时候;B) DownHeap 以某个节点从上向下调整,通常删除节点时使用。

基本接口

一般会包含如下的几个接口:

  • insert() 添加元素。
  • extract() 获取最大、最小元素。
  • degrade() 用于降级某个元素。

其中的更新只是向上增长,例如最小堆是 Decrease 而最大堆是 Increase ,也就是只支持 PushUp 操作,这样可以保证只需要执行该操作即可,而执行 PushDown 可能会违反栈的规则。

总结

如果所有节点还没有满足栈性质,那么针对单个节点执行 PushDown 可能会违反规则,此时需要对所有的非叶子节点执行;而单个节点的 PushUp 操作仍然可以确保满足条件。