优先队列(priority queue),也是一种重要的缓存结构。从原理上说,这种线性结构与二叉树没有直接关系。但是基于对一类二叉树的认识,可以做出优先队列的一种高效实现。

  • 注意,队列和优先队列的选择,要视具体的应用场景而定,两者也有可能共存于一个系统之中,比如海关(Customs)检查站模拟系统:
    到达车辆可能排队 ⇒ 队列
    事件(不同的事件) ⇒ 优先队列
  • 无论是队列、栈还是优先队列,都是从指定的方向进行进入集合和弹出集合的(也即规则是十分明确的),
    • 队列:尾部入,头部出;
    • 栈:头部入,头部出;
    • 优先队列:根元素弹出,新来的元素被调整在合适的位置;

1. 基本概念

作为缓存结构,优先队列与栈和队列类似:

  • 可以将数据元素保存其中;
  • 可以访问和弹出;

优先队列的特点是存入其中的每项数据都另外附有一个数值,表示这个项的优先程度,称为其优先级。

抽象地看,需要缓存的是一个有序集 S=(D,) 的元素,这里的 “” 是集合 D 上的一个全序(非严格的),表示元素的优先关系。允许 D 中不同元素具有相同的优先级,也就是说,

a,bD,ab,ba

这时,就说 ab 优先级相同。在这种应用情景下:

  • 如果要求保证优先级相同的元素先进先出(希望优先队列同时具有队列的 FIFO 性质),那就只能做出效率较低的实现
  • 如果只要求保证访问(或弹出)的总是当时存在的最优元素中的一个,不要求一定是其中最早进入优先队列的元素,那么就存在效率更高的实现

2. 基于连续表/链表的实现

class PriQue:
    def __init__(self, elist=[]):
        self._elems = list(elist)
        self._elems.sort(reverse=True)
    def enqueue(self, e):
        n = len(self._elems) - 1
        while n >=0:
            if e >= self._elems[n]:
                n -= 1
            else: break
        self._elems.insert(n+1, e)
    def dequeue(self):
        if not self._elems:
            raise ...
        self._elems.pop()

总而言之,采用线性表技术实现优先队列,无论采用怎样具体实现技术:

  • 连续表
  • 链表

在插入元素和取出元素的操作中,总有一种是具有线性复杂度(O(n)) 的操作,这一情况不能令人满意。

3. 树形结构和堆的性质

前文书说过,只要元素根据优先级顺序线性排列,就无法避免线性复杂性问题。这意味着,如果不改变数据的线性顺序存储方式,就无法突破 O(n) 的时间复杂度。要做出操作效率更高的优先队列,必须考虑其他数据结构组织方式。

采用树形结构实现优先队列的一种有效技术成为。从结构上看,堆就是结点里存储数据的完全二叉树,但堆中数据的存储要满足一种特殊的堆序:

  • 大顶堆
  • 小顶堆

保证堆中最优先的元素必定位于二叉树的根节点(堆顶),O(1) 时间便可得到。

在树的性质中,我们知道:一棵完全二叉树可以自然而且信息完全地存入一个连续的线性结构(例如连续表),堆是完全二叉树,因此堆也可以自然地存入一个连续表,以便通过下标即可访问任一节点的父节点、子节点。

4. 堆和完全二叉树的性质

  • Q1:在一个堆的最后加上一个元素(在相应连续表的最后增加一个元素),整个结构还是可以看做一棵完全二叉树,但它未必是堆(最后的元素未必满足堆序);
  • Q2:一个堆去掉堆顶(对应线性表位置 0 的元素),其余元素形成两个“子堆”;
  • Q3:给由 Q2 得到的表(两个子堆)加入一个根元素(存入位置 0),得到的结点序列又可看做完全二叉树,但未必满足堆序;
  • Q4:去掉一个堆中的最后一个元素(最下层的最右节点,也是线性表的最后一个元素),剩下的元素仍构成一个堆;

5. 优先队列的堆实现

解决插入和删除的关键操作称为筛选:

  • 插入元素 ⇒ 向上筛选

    不断用新加入的元素 e,与其父节点的数据比较,如果 e 较小,就交换两个元素的位置。通过这样的比较和交换,元素 e 不断上移。这一操作一直做到 e 的父节点的数据 e,或者 e 到达根节点时停止。

  • 弹出元素 ⇒ 向下筛选

    由于堆顶元素就是最优先元素,应该弹出的元素就是它。但弹出堆顶元素后,剩下的元素已经不再是堆:

    • 根据性质 Q2,剩余元素可看做两个子堆;
    • 又根据 Q3,只需填补一个堆顶元素就可以将它们做成一个完全二叉树,
    • 再根据 Q4,从原堆的最后取出一个元素,其余元素仍然是堆,把这个元素放在堆顶就得到一棵完全二叉树。

    在这种情况下,恢复堆的操作称为向下筛序:设两个子堆 A 和 B 加上根元素 e 构成一棵完全二叉树,现在需要把他们做成一个堆:

    • 用 e 与 A、B 两个子堆的堆顶元素(子树的根),最小者为整个堆的顶;
      • 若 e 不是最小,最小的必为 A 或 B 的根,设 A 的根最小,将其移到堆顶,相当于删除了 A 的顶元素;
      • 下面考虑把 e 放入去掉堆顶的 A,这是规模更小同一问题;
    • 如果某次比较 e 最小,以它为顶的局部树,已经成为堆,整个结构也为堆;
    • 或者 e 已经落到底,整个结构成为堆;
posted on 2016-08-22 16:25  未雨愁眸  阅读(312)  评论(0编辑  收藏  举报