Loading

优先级队列

示例:

  1.夜间门诊

  2.多任务操作系统

  3.Huffman编码

A 问题模式: 循优先级访问(call by priority),  按照这种优先级通过调度器将最高优先级的元素输入给服务端。

  

B 操作接口:

template <typename T>struct PQ {
    virtual void insert(T) = 0;  //按照优先级次序插入词条
    virtual T getMax() = 0 ; //取出优先级最高的词条 
    virtual T delMax()  = 0; //删除优先级最高的词条
};

 Stack和Queue,都是PQ的特例——其优先级完全取决于元素的插入次序。对于栈而言,越是晚到的元素,其优先级越高,更早的出栈被处理。而队列相反。

 

C 实现尝试:

 efficiency and cost

 C1  实现为Vector:  将每个新加入的元素放在向量的末尾。

     插入的时间成本:O(1)   获得元素值的时间成本是:O(n)   删除元素的时间成本是: O(n)

 C2  实现为Sorted Vector:  即优先级最大的元素放入向量的末尾。

    插入的时间成本:O(n)   获得元素值的时间成本是:O(1)   删除元素的时间成本是: O(1)

 C3  实现为List: 

    插入的时间成本:O(1)   获得元素值的时间成本是:O(n)   删除元素的时间成本是: O(n)

 C4  实现为Sorted List: 

   插入的时间成本:O(n)   获得元素值的时间成本是:O(1)   删除元素的时间成本是: O(1)

 C5  实现为BBST:  AVL Splay Red-black   杀鸡用了牛刀

     PQ = 1 x insert() + 0.5 x search() + 0.5 x remove() = 2/3 x BBST

 若只需要查找极值元素,则不必维护所有元素之间的全序关系,偏序足以。找一种实现简单、维护成本更低的实现方式。

 

 

D最终尝试:  完全二叉堆

 以向量为形,以树形结构为神,完全二叉树

    逻辑上,等同于完全二叉树。物理上,直接借助向量实现。

 逻辑节点与物理元素依层次遍历次序彼此对应:

   

   完全二叉堆实现:

template <typename T> class PQ_ComplHeap : pubilc PQ<T>, public Vector<T> {

protected:
    Rank percolateDown(Rank n, Rank i); //下滤
    Rank percolateUp(Rank i); //上滤
    void heapify(Rank n); //Floyd 建堆方法
public:
    PQ_ComplHeap(T* A, Rank n)  {//批量构造
       copyForm(A,0,n); heapify(n);     }
    
    void insert(T); //按照比较器确定的优先次序,插入词条
    T getMax() {return _elem[0] ; } //读取优先级最高的词条
    T delMax();  //删除优先级最高的词条
};
     

   堆序性:

  H[i] <= H[Parent(i)] : 任何一个节点在数值上都不会超过它的父亲。所以根据优先级队列的特性,最大元必然位于根节点位置。所以H[0]是全局最大元素。

template <typename T> T
PQ_Complate<T> :: getMax() {return _elem[0];}

 

 E完全二叉堆插入(上滤操作):  

   E1:算法

  为插入词条e,只需要将e作为末元素接入向量:1.结构性自然保持。2.若堆序性也被破坏,则完成。否则只能是e与其父节点违反堆序性,则只需要将e与其父节点互换位置即可。

  

 

 

   

 

   如果交换后,仍然违反逆序性,则只需要将e与新的父节点互换位置即可。如此反复交换的过程满足单调性。这一过程即所谓的上滤过程。

   而一旦过程终止,则堆序性在整个完全二叉堆中得到了完全的恢复。     

  E2实例:

  

 

 

   E3实现:

 

     

 

 

   E4效率:

   O(logn),但其中swap操作包含3次赋值操作。所以效率为3*logn.

   所以当新插入节点与其父节点需要交换时,我们只需将新插入词条e先备份,然后将其不断上移,最后与其父亲交换。

    

 

 

 

 

 F全二叉堆删除:  

   F1:算法

  最大元素始终在堆顶,所以我们只需将其摘除。

  在摘除之后,完全二叉堆的堆序性被破坏,此时我们先将末尾元素放置于堆顶。

  最后采用下滤操作:  

    1)e与孩子中的大者互换,若堆序性完成,则结束。

    2)否则e与孩子中的最大者继续交换,若堆序性完成,则结束。

 

 

 

 

    F2:实例

    

 

 

   F3:实现

  

 

 

   F4:效率

  其效率与树的高度O(h)具有关系,所以其时间复杂度应为3* O(logn),同样在这里,常数项系数可以得到改进,可以先保存根节点,然后再下滤,最终只做一次实质上的交换操作(3次赋值)

  在上滤过程中,新插入的节点e只需要与其父节点进行比价。在下滤过程中,根节点要与其孩子做两次比较。这在二叉堆中并不至关重要,但是在多叉堆中就至关重要。

 

 G全二叉堆批量建堆:  

   G1:算法

    对于任给的n个元素,我们希望将它们构建成一个堆,这样一个过程称之为heapification

   G2:自上而下的上滤实现   

PQ_ComplHeap(T*A, Rank n) {copyFrom(A,0,n); heapify(n);}
template <typename T> void PQ_ComplHeap<T>: heapify(Rank n) {
  for (int i =1; i < n; ++i)  //按层次遍历次序逐一
     percolateUp(i); //经过上滤插入各个节点
}    

   G3:自上而下的上滤的效率

    最坏情况下,即每个节点都需要上滤至根节点,所需成本线性正比于其深度。即便只考虑最底层(n/2个节点),深度均为O(logn),累计耗时O(nlogn),但是这么长的时间消耗足以全排序。

   G4:自下而上的下滤实现   

  

 

 

 

    G5:自下而上的下滤实例   

   

 

 

     G6:自下而上的下滤效率:

    最坏情况下,每个内部节点所需的调度时间,正比于其高度而非深度。高度是从下往上数。而深度与高度相反,从上往下数。

    对每个节点的高度求和,为O(n). 可以接受

    对每个节点的深度求和,为O(nlogn). 不可以接受  

    为什么会出现这种情况呢?因为深度是从上往下数的,而一个完全二叉堆,其处于下部的节点数较多,所以其累计的总和就更大。而以高度作为计算,位于完全二叉堆上部的节点较少,所以其累计的总和较少。

 

H堆排序

  还记得选择排序嘛!

  我们始终将整个序列分为两部分:一部分是已经排好序的部分,另一部分是待排序部分。所以我们反复遍历待排序部分的最大值,O(n2)

  H1使用完全二叉堆来替代原有的待排序部分:

  在此,我们使用完全二叉堆,来替代原有的待排序部分。

  初始化:heapify(),O(n),建堆

  迭代:delMax(),O(logn),取出堆顶并调整复原

  不变性:H <= S

 

 

   H2在向量内完成堆排序:

  已排序的部分构成向量的后端,而与之互补的前缀恰好构成一个完全二叉堆,堆中的最大元必然是0号元素,而接下来与之兑换的x必然是已排序单元秩为-1的元素,所以我们首先取出最大的元素,然后用x取而代之。

  然后将备份的最大元植入x,然后对新的根节点作下滤调整。

  

 

 

 

 

 

   规整为两部分:交换和下滤,直至堆变空。除了交换需要常数个辅助空间外,除此我们不需要更多的辅助空间。

  H2堆排序实现:

 

 

  H2堆排序实例:

 

I左式堆:

  I1堆合并:

    方法1:

      A.insert(B.delMax())   O(m*(logm + log(n+m))) = O(m*log(n+m))

      方法2:

      union(A,B).heapify(n+m) = O(m + n)

 

  I2堆单侧倾斜:

     保持堆序性,附加新条件。使得在堆合并过程中,只需要调整很少的部分节点,    O(logn)

     新条件  = 单侧倾斜: 节点分布偏向于左侧,合并操作只涉及右侧。

     如果真的存在这样一个堆,那么它断然不再是完全二叉堆,堆序性才是其本质性要求,而结构性则不是其本质要求,所以为了效率在此牺牲其结构性。

 

 

 

 

  I3空节点路径长度:从该节点到达一个没有两个孩子的节点的最短距离。NULL的NPL为-1,而左式堆的核心约束条件是左倾即任意节点的左0孩子的NPL大于等于右孩子的NPL。   

    npl(x) = 1 + npl(rc(x)),满足左倾性的堆,称之为左式堆 。左倾性与堆序性相容但是不矛盾。左式堆的子堆,必然是左式堆。左式堆倾向于更多节点分布于左侧分支。

    引入所有的外部节点:消除一度节点,转为真二叉树

    npl(x) = x到外部节点的最近距离

    npl(x) = 以x为根的最大满子树的高度、

    npl(Null) = 0; npl(x) = 1 + min(npl(lc(x), npl(rx(x))));

  I3右侧链:

    rChain(x) : 从节点x出发,向右不断前行所确立的那个分支就称之为右侧链。

    rChain(root)的终点,必然是全堆中最浅的外部节点。 

 

   I4左式堆的合并算法:左式堆不再满足结构性,物理结构不再保持紧凑性。

    以外部形式给出合并两个左式堆的函数:

 

 

 

 

  先将a的右式堆与b合并,合并所得的结果作为a的右子堆,然后比较a的左式堆和右式堆的npl值,如果比它小,则互换位置。

   I5左式堆的合并算法实现:

 

 

   I5左式堆的合并算法实例:

 

          https://next.xuetangx.com/learn/THU08091002048/THU08091002048/1158035/video/784339

  

   I5左式堆的插入算法:

    插入就是合并

 

   I5左式堆的删除算法:

 

   删除也是合并

 

 

 

 

posted @ 2019-12-18 17:10  三只猫-  阅读(477)  评论(0编辑  收藏  举报