• 从队列、二叉树到优先队列


    优先队列(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 已经落到底,整个结构成为堆;
  • 相关阅读:
    python 文件目录/方法
    python文件
    python模块
    python数据结构
    python函数
    python迭代器和生成器
    python循环语句
    python控制语句 if
    python数字
    个人课程总结
  • 原文地址:https://www.cnblogs.com/mtcnn/p/9424151.html
Copyright © 2020-2023  润新知