【数据结构与算法分析——C语言描述】第六章:优先队列(堆)

第六章:优先队列(堆)
[TOC]算法

思考以下场景,老师布置了不少做业,如今你须要将做业打印出来,你将做业文件依照队列的形式放入待打印列表中,但此时,你但愿最重要(或者是立刻就要上交)的做业优先打印出来.此时,队列结构显然不能知足咱们的需求,这时候咱们考虑一种名为优先队列(priority queue)的数据结构,或者称之为堆.数组

6.1 模型数据结构

  • 优先队列至少容许如下两种操做:
  1. Insert(插入):等价于队列中 Enqueue(入队).ide

  2. DeleteMin(删除最小者):找出、返回和删除优先队列中的最小元素.等价于队列中 Dequeue(出队).

【数据结构与算法分析——C语言描述】第六章:优先队列(堆)

6.2 一些简单的实现函数

  • 使用一个简单链表再表头以 $ O(1) $ 执行插入操做,并遍历该链表以删除最小元,这须要 $ O(N) $ 的时间.测试

  • 另外一种方法,始终让表表示排序转台,这会使得插入操做花费 $ O(N) $ 时间,而 DeleteMin 操做花费 $ O(1) $ 时间.*考虑到 DeleteMin 操做并很少于 Insert 操做,所以在这两者之间,第一种方法可能更好.ui

  • 再一种方法,使用二叉查找树,这样保证了这两种操做的时间复杂度都是 $ O(log N) $ . 尽管插入是随机的,而操做不是,这个结论依旧成立.反复删除左子树的节点会损害树的平衡,可是考虑到右子树是随机的,在最坏的状况下,即 DeleteMin 将左子树删空的状况下,右子树拥有的元素最多也就是它应有的二倍,这只是在指望的深度上增长了一个常数.

6.3 二叉堆spa

  • 二叉堆(binary heap):如同二叉查找树同样,二叉堆有两个性质, 结构性 和 堆序性 ,如同AVL树同样,对二叉堆的一次操做可能破坏其中一个性质.同时,须要提醒的是,二叉堆能够简称为堆(heap),由于用二叉堆实现优先队列十分广泛.

6.3.1 结构性质3d

  • 彻底二叉树(complete binary tree):彻底填满的二叉树,有可能的例外是在底层,底层上的元素从左到右依次填入.以下图.

【数据结构与算法分析——C语言描述】第六章:优先队列(堆)

一棵高度为 $ h $ 的彻底二叉树有 $ 2^h $ 到 $ 2^{h+1} - 1 $ 个节点,这意味着,对于拥有 $ N $ 个节点的彻底二叉树而言,它的高度为 $ \lfloor logN \rfloor $ ,显然为 $ O(logN) $.
十分重要的一点,因为彻底二叉树具备规律性,因此它能够用数组来表示.能够代替指针.以下.指针

【数据结构与算法分析——C语言描述】第六章:优先队列(堆)
对于数组中任意一个位置 $ i $ 上的元素,其左儿子在位置 $ 2i $ 上,右儿子在位置 $ 2i + 1 $ 上,它们的父亲在位 置$ \lfloor \frac{i}{2} \rfloor $ 上.
用数组表示的优势再也不赘述,但有一个缺点须要提醒,使用数组表示二叉堆须要实现估计堆的大小,但这对于典型状况不成问题.

//优先队列的声明
struct HeapStruct;
typedef struct HeapStruct *PriorityQueue;
struct HeapStruct{
    int Capacity;
    int Size;
    ElementType *Elements;
};
PriorityQueue Initialize( int MaxElements);
void Destroy( PriorityQueue H);
void MakeEmpty( PriorityQueue H);
void Insert( ElementType X, PriorityQueue H);
ElementType DeleteMin( PriorityQueue H);
ElementType FindMin( PriorityQueue H);
int IsEmpty( PriorityQueue H);
int IsFull( PriorityQueue H);
PriorityQueue Initialize( int MaxElements){
    PriorityQueue H;
    if( MaxElements < MinPQSize)
        Error(" Priority queue size is too small");
    H = malloc( sizeof( struct HeapStruct));
    if( H = NULL)
        FatalError(" Out of space");
    H->Elements = malloc( ( MaxElements + 1) * sizeof( ElementType));
    if( H->Elements == NULL)
       FatalError(" Out of space");
    H->Capacity = MaxElements;
    H->Size = 0;
    H->Elements[0] = MinData;//标记
    return H;
}

6.3.2 堆序性质

  • 堆序(heap order):使操做被快速执行的性质,依据堆的性质,最小元老是能够在根处找到.

  • 在一个堆中,任意子树也是一个堆.在一个堆中,对于每个节点 $ X $, $ X $ 的父亲中关键字小于或者等于 $ X $ 中的关键字,根节点除外,由于它没有父亲.
    【数据结构与算法分析——C语言描述】第六章:优先队列(堆)
    6.3.3 基本的堆操做
  • Insert 插入 考虑将一个元素 $ X $ 插入堆中,咱们将在下一个位置创建一个空穴,不然该堆将再也不是彻底二叉树.

  • 若是 $ X $ 能够放到该空穴中而不影响堆的序,那么插入操做完成.

  • 不然,咱们将这个空穴的父亲节点上的元素移动到该空穴上,这样,空穴便随着根的方向一直向上进行,直到知足步骤 1 为止.这种策略叫上滤(percolate up).以下图所示,尝试插入关键字 14 .
    【数据结构与算法分析——C语言描述】第六章:优先队列(堆)

【数据结构与算法分析——C语言描述】第六章:优先队列(堆)

void Insert( ElementType X, PriorityQueue H){
   int i;
   if( IsFull( H)){
       Error(" Priority queue is full");
       return ;
   }
   for( i = ++H->Size; H->Elements[i/2] > X; i/=2)
       H->Elements[i] = H->Elements[i/2];
   H->Elements[ i] = X;
}

若是插入的元素是新的最小值,那么它将被推到顶端.在顶端, $ i = 1 $ ,咱们能够跳出循环,能够在 $ i = 0 $ 处放置一个很小的值使得循环停止.这个值咱们称之为标记(sentinel),相似于链表中头节点的使用,经过添加一条哑信息(dummy piece of infomation),避免每次循环都要执行一次测试,从而节省了一点时间.
若是插入的元素是新的最小元,已知过滤到根部,那么这种插入的时间复杂度显然是 $ O(logN) $ .

  • DeleteMin 删除最小元 DeleteMin 的操做相似于 Insert 操做.找到最小元是很容易的,而删除它是比较困难的.当删除一个最小元时,在根节点处产生一个空穴,因为如今堆少了一个元素,所以堆中最后一个元素 $ X $ 必须移动到该堆的某个位置.
  1. 若是 $ X $ 能够移动到空穴中去,那么 DeleteMin 操做完成.

  2. 鉴于 1. 中的步骤不太可能发生,所以咱们考虑 $ X $ 不能移动到空穴中的状况,咱们将空穴的两个儿子中较小的一个(考虑到是最小堆)移入空穴中,这样一来就把空穴向下推了一格,重复这个步骤直到 $ X $ 能够被放到空穴中.这种策略叫作下滤(percolate down).以下例:

【数据结构与算法分析——C语言描述】第六章:优先队列(堆)

【数据结构与算法分析——C语言描述】第六章:优先队列(堆)

【数据结构与算法分析——C语言描述】第六章:优先队列(堆)

其中须要注意的一点是,当堆中存在偶数个元素时,此时将会遇到一个节点只有一个儿子的状况,咱们没必要须保证节点不总有两个儿子,所以这就涉及到一个附加节点的测试.

ElementType DeleteMin( PriorityQueue H){
   int i, Child;
   ElementType MinElement, LastElement;
   if( IsEmpty( H)){
       Error(" Priority queue is empty");
       return H->Elements[0];
   }
   MinElement = H->Elements[1];
   LastElement = H->Elements[ H->Size--];
   for( i=1; i*2 <= H->Size; i = Child){
       Child = i * 2;
       if( Child != H->Size && H->Elements[ Child + 1] < H->Elements[ Child])
           Child++;
       //if语句用来测试堆中含有偶数个元素的状况
       if( LastElement > H->Elements[ Child])
           H->Elements[i] = H->Elements[ Child];
       else break;
   }
   H->Elements[i] = LastElement;
   return MinElement;
}

这种算法的最坏运行时间为 $ O(logN) $ ,平均而言,被放到根除的元素几乎下滤到堆的底层.

6.3.4 其余堆的操做

  • 对于最小堆(min heap)而言,求最小元易如反掌,可是求最大元,却不得不采起遍历的手段.对于最大堆找最小元亦是如此.

  • 事实上,一个堆所蕴含的关于序的信息不多,所以,若是不对堆进行线性搜索,是没有办法找到任何特定的关键字的.为了说明这一点,在下图所示的大型堆结构(具体元素没有给出).

【数据结构与算法分析——C语言描述】第六章:优先队列(堆)

咱们看到,关于最大值的元素所知道的惟一信息是:该元素位于树叶上.可是,有近半数的元素位于树叶上,所以该信息是没有什么用处的.出于这个缘由,若是咱们要知道某个元素处在什么位置上,除了堆以外,还要用到诸如散列表等某些其余的数据结构.

若是咱们假设经过某种其余方法得知每个元素的位置,那么有几种其余的操做的开销将变小.
下列三种这样的操做均以对数最坏情形时间运行.

  • DecreaseKey 下降关键字的值
    DecreaseKey(P, $ \Delta $ , H)操做将下降在位置 P 处关键字的值,下降的幅度为 $ \Delta $( $ \Delta > 0 $ ),因为这可能会破坏堆的序,所以必须经过上滤对堆进行调整.该操做堆系统管理程序是游泳的,系统管理程序可以使它们的程序以最高的优先级来运行.

  • IncreaseKey 增长关键字的值
    IncreaseKey(P, $ \Delta $ , H)操做将增长在位置 P 处关键字的值,增值的幅度为 $ \Delta $( $ \Delta > 0 $ ).能够经过下滤来完成.许多调度程序自动地下降正在过多小号CPU时间的进程的优先级.

  • Delete 删除
    Delete(P, H)操做删除堆中位置 P 上的节点.这经过首先执行DecreaseKey(P, $ \infty $ , H),而后再执行 DeleteMin(H).当一个进程被用户停止(非正常停止)时,它不准从优先队列中除去.

  • BuildHeap 构建堆
    BuildHeap(H)操做把 N 个关键字做为输入并把它们放到空堆中.显然,这可使用 N 个 Insert 操做来完成.因为每一个 Insert 操做将会花费平均 $ O(1) $ 时间和最坏 $ O(logN) $ 时间,所以该操做的总的运行时间是 $ O(N) $ 平均时间而不是 $ O(NlogN) $ 最坏情形时间.因为这是一种特殊的执行,没有其余操做的干扰,也让且咱们已经直到了该指令可以以线性平均时间运行,所以,指望可以保证线性时间界的考虑是合乎情理的.
    下面咱们看平均时间是怎么获得的.
    通常的算法是将 N 个关键字以任意顺序放入树中,保持结构特性.此时,若是 percolateDown(i) 从节点 i 下滤,那么执行下列代码建立一棵具备堆序的树(heap-ordered tree)

for( i = N/2; i>0; i--)
   PercolateDown(i);

以下例

![](https://s4.51cto.com/images/blog/202102/27/84cc55ee9fc133cb35c8b999228432a6.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

![](https://s4.51cto.com/images/blog/202102/27/eb4d101c84e39263a3ffba3f53d81aff.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

![](https://s4.51cto.com/images/blog/202102/27/cc5b1f1a0d1969625c2d53d73b2ee99e.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

为了肯定 BuildHeap 的运行时间的界,咱们肯定虚线条数的界,这能够经过计算树中全部节点的高度和来获得,它是虚线的最大条数,如今咱们说明,该和为$ O(N) $

* 定理:包含 $ 2^{b+1} - 1 $ 个节点高为 $ b $ 的理想二叉树(perfect binary tree)的节点的高度的和为 $ 2^{b+1} - 1 - ( b + 1 ) $ 证实:容易看出如下规律:

![](https://s4.51cto.com/images/blog/202102/27/cc5f85aae2115248ce59bd2b2001f549.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

则全部节点的高度和 $ S = 2^{0} * b + 2^{1} *( b - 1 ) + ... + 2^{b-1} * 1+ 2^{b} * 0 = $
利用裂项相消,获得
$ S = 2^{b+1} - 1 - ( b + 1 ) $

**6.4 优先队列的应用**
**6.4.1 选择问题**
选择问题(selection problem):从一组 N 个数而要肯定其中第 k 个最大者.

* 算法一: 把这些元素依次读入数组并给他们排序,同时返回适当的元素.该算法的时间复杂度为 $ O(N^2) $

* 算法二:先把前 k 个元素读入数组并按照递减的顺序排序,以后,将剩下的元素逐个读入,当新元素读入时,若是它小于数组中的第 k 个元素则忽略,不然就将它放到数组中正确的位置上.同时将数组中的一个元素挤出数组,当算法停止的时候,第 k 个位置上的元素做为关键字返回便可.该算法的时间复杂度是 $ O(kN) $,当 $ k = \lceil \frac{N}{2} \rceil $ 时,该算法时间复杂度为 $ O(N^2) $ .注意,对于任意的 k ,咱们能够求解对称的问题:找出第 (N - k + 1) 个最小的元素,从而 $ k = \lceil \frac{N}{2} \rceil $ 实际上时这两种算法最困难的状况,此时 k 处的值被称为中位数(median).

* 算法三:为简单起见,假设咱们只考虑找出第 k 个最小的元素.咱们将 N 个元素读入一个数组,而后堆数组应用 BuildHeap 算法,最后,执行 k 次 DeleteMin 操做.从该堆中最后提取的元素就是咱们须要的答案.显然,改变堆序的性质,咱们能够求原式问题:找出第 k 个最大的元素. 算法的正确性显然. 考虑算法的时间复杂度,若是使用 BuildHeap ,构造堆的最坏情形用时为 $ O(N) $ ,而每次 DeleteMin 用时 $ O(logN) $ .因为有 k 次 DeleteMin,所以咱们获得总的运行时间为 $ O(N + klogN) $ .

1. 若是 $ k = O(\frac{N}{logN} ) $ ,那么运行时间取决于 BuildHeap 操做,即O(N).

1. 对于大的 k 值,运行时间为 $ O(klogN) $.

1. 若是 $ k = \lceil \frac{N}{2} \rceil $,那么运行时间为 $ \Theta(NlogN) $

* 算法四:回到原始问题:找出第 k 个最大的元素.在任意时刻咱们都将维持 k 个最大元素的集合 S 。在前 k 个元素读入以后,当再读入一个新的元素时,该元素将与第 k 个最大元素进行比较,记这第 k 个最大的元素为 $ Sk $ .注意, $ Sk $ 是集合 S 中最小的一个元素.若是新元素比 $ Sk $ 大,那么用新元素替代集合 S 中的 $ Sk $ .此时,集合 S 中将有一个新的最小元素,它与偶多是刚刚添加进的元素,也有可能不是.当输入终止时,咱们找到集合 S 中的最小元素,并将其返回,这边是答案. 算法四思想与算法二相同.不过,在算法四中,咱们用一个堆来实现集合 S ,前 k 个元素经过调用依次 BuildHeap 以总时间 $ O(k) $ 被置入堆中.处理每一个其他的元素花费的时间为 $ O(1) + O(logk) $,其中 $ O(1) $ 部分是检查元素是否进入 S 中, O(logk)部分是再必要时删除 $ S_k $ 并插入新元素.所以总时间是 $ O(k + ( N - k )logk) = O( Nlogk) $.若是 $ k = \lceil \frac{N}{2} \rceil $,那么运行时间为 $ \Theta(NlogN) $.

**6.4.2 时间模拟**
* 咱们假设有一个系统,好比银行,顾客们到达并站队等在哪里直到 k 个出纳员中有一个腾出手来.咱们关注在于一个顾客平均要等多久或队伍可能有多长这样的统计问题.

* 对于某些几率分布以及 k 的取值,答案均可以精确地计算出来.然而伴随着 k 逐渐变大,分析明显地变得困难.

* 模拟由处理中的时间组成.这里有两个事件.

* 一个顾客的到达.

* 一个顾客的离开,从而腾出一名出纳员.

* 咱们可使用几率函数来生成一个输入流,它由每一个顾客的到达时间和服务时间的序偶组成,并经过到达时间排序.咱们没必要使用一天中的精准时间,而是经过名为滴答(tick)的单位时间量. 进行这种模拟的一种方法是在启动处在 0 滴答处到额一台摸拟钟表.让钟表一次走一个滴答,同时查询是否有一个事件发生.若是有,那么咱们处理事件,搜集统计资料.当没有顾客留在输入流中且全部出纳员都闲置,模拟结束. 可是这种模拟问题,它运行的时间不依赖顾客数量或者时间数量,而是依赖于滴答数,可是滴答数又不是实际输入.不妨假设将时钟的单位改为滴答的千分之一并将输入中的全部时间乘以 1000 ,那么结果即是,模拟用时增长了1000倍. 避免这种问题的关键在于在每个阶段让重表直接走到下一个事件时间,从概念上来看这是容易作到的.在任一时刻,可能出现的下一个时间或者是下一个顾客到达,或者是有一个顾客离开,同时一个出纳员闲置.因为能够得知将发生事件的全部时间,所以咱们只须要找出最近的要发生的事件并处理这个事件.

* 若是这个事件是有顾客离开,那么处理过程包括搜集离开的顾客的统计资料及检验队列看看是否还有其余顾客在等待.若是有,那么咱们加上这位顾客,处理所须要的统计资料,计算该顾客将要离开的时间,并将离开事件假到等待发生的事件集中区.

* 若是事件是有顾客到达,那么咱们检查闲置的出纳员.若是没有,那么咱们把该到达时间放置到队列中去;不然,咱们分配顾客一个出纳员,计算顾客离开的时间,并将离开事件加到等待发生的事件集中区. 在等待的顾客队伍能够实现为一个队列.因为咱们须要找到最近的将要发生的事件,合适的办法是将等待发生的离开的结合编入到一个优先队列中.下一个事件是下一个到达或者下一个离开.

* 考虑以上算法的时间复杂度,若是有 C 个顾客(所以有 2C 个事件 )和 k 个出纳员,那么模拟的运行时间将会是 $ O( Clog(k+1)) $ ,由于计算和处理每一个事件花费时间为 $ O(logH) $ ,其中 $ H = k + 1 $ 为堆的大小.

**6.5 d-堆**
* d-堆是二叉堆的推广,它像一个二叉堆,可是全部的节点都有 d 个儿子(注意,二叉堆是一个2-堆).
![](https://s4.51cto.com/images/blog/202102/27/515dcac882840cac77b522fc5737b1cd.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)  
     如上图所示,3-堆。

**6.6 左式堆**
* Merge 合并操做,堆而言,合并操做是最困难的操做。

* 考虑到堆结构没法用数组实现以 $ O(N) $ 高效的合并操做。所以,全部支持高效合并的高级数据结构都须要使用指针。

* 左式堆(leftist heap):它与二叉树之间的惟一区别是,左式堆不是理想平衡的,而其实是趋向于很是不平衡。它具备相同的堆序性质,若有序性和结构特性。

**6.6.1 左式堆的性质**

* 零路径长(null path length, NPL):从任一节点 $ X $ 到一个没有两个儿子的节点的最短路径长。所以,具备 0 个或 1 个儿子的节点的 Npl 为 0,而 Npl(NULL) = -1.
注意,任一节点的零路径长比它诸儿子节点的零路径长的最小值多 1 。这个结论也适用少于两个儿子的节点,由于 NULL 的零路径长为 -1 .

* 性质:对于左式堆中的每个节点 $ X $ ,左则日子的零路径长至少与右儿子的零路径长同样大。以下图所示:

![](https://s4.51cto.com/images/blog/202102/27/de33d3750adbb1d21d42c8ba48e451b4.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

在图示中,右边的树并非左式堆,由于他不知足左式堆的性质。
左式堆的性质显然更加使树向左增长深度。确实有可能存在由左节点造成的长路径构成的树(实际上这更加便于合并操做),故此,咱们便有了左式堆这个名称。

* 定理:
在右路径上有 $ r $ 个节点的左式树必然至少有 $ 2^r - 1 $ 个节点。
证实:数学概括法。
若$ r = 1 $ ,则必然至少存在一个树节点;
假设定理对 $ r = k $ 成立,考虑在右路径上有 $ k + 1 $ 个节点的左式树,此时,根具备在右路径上含 $ k $ 个节点的右子树,以及在右路径上至少包含 $ k $ 个节点的左式树(不然它便不是左式树)。对这两个子树应用概括假设,得知每棵子树上最少含有 $ 2^k - 1 $ 个节点,再加上根节点,因而这颗树上至少有有 $ 2^{k+1} - 1 $ 个节点。
原命题得证。

* 推广:从上述定理咱们当即能够获得,$ N $ 个节点的左式树有一条右路径最多包含 $ \lfloor log(N+1) \rfloor $ 个节点。

**6.6.1 左式堆的操做**
* 合并 首先,注意,插入只是合并的一个特殊情形。首先,咱们给出一个简单的递归解法,而后介绍如何可以非递归地施行该解法。以下图,咱们输入两个左式堆 $ H1 $,$ H2 $. 

![](https://s4.51cto.com/images/blog/202102/27/4f01e619a985552565ce887cbbb100eb.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
除去使用数据、左指针、右指针外还须要一个只是零路径长的项。

1. 若是这两个堆中有一个是空的,那么咱们能够直接返回另外一个非空的堆。

1. 不然,想要合并两个堆,咱们须要比较它们的根。回想一下,最小堆中根节点小于它的两个儿子,而且子树都是堆。咱们将具备大的根值得堆与具备小的根值得堆的右子树合并。在本例中,咱们递归地将 $ H2 $ 与 $ H1 $ 中根在 8 处的右子堆合并,获得下图:

![](https://s4.51cto.com/images/blog/202102/27/e873b4021ad8c96b95e350d9f7ea4562.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

3.注意,由于这颗树是经过递归造成的,咱们有理由说,合成的树依然是一棵左式树。如今,咱们让这个新堆成为 $ H_1 $ 中根的右儿子。以下图:图片

4.最终获得的堆依然知足堆序的性质,可是,它并非左式堆。由于根的左子树的零路径长为 1 ,而根的右子树的零路径长为 2 .左式树的性质在根处遭到了破坏。不过,很容易看到,树的其他部分必然是左式树。这样一来,咱们只要对根部进行调整便可。
方法以下:只要交换根的作儿子和右儿子,以下图,并更新零路径长,就完成了 Merge . 新的零路径长是新的右儿子的零路径长加 1 .注意,若是零路径长不更新,那么全部的零路径长将都是 0 ,而堆也再也不是左式堆,只是随机的。

![](https://s4.51cto.com/images/blog/202102/27/6d7531fe91e2137082ad28f9ff09e587.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

struct TreeNode;
typedef struct TreeNode *PriorityQueue;
struct TreeNode{
   ElementType   Element;
   PriorityQueue Left;
   PriorityQueue Right;
   int           Npl;
};
PriorityQueue Initialize( void);
ElementType FindMin( PriorityQueue H);
int IsEmpty( PriorityQueue H);
PriorityQueue Merge( PriorityQueue H1, PriorityQueue H2);
PriorityQueue Merge1( PriorityQueue H1, PriorityQueue H2)
#define Insert( X, H)( H = Insert1( ( X), H)
//宏Insert 完成一次与二叉堆兼容的插入操做
PriorityQueue Insert1( ElementType X, PriorityQueue H);
//Insert1 左式堆的插入例程
PriorityQueue DeleyeMin1( PriorityQueue H);
//合并
PriorityQueue Merge( PriorityQueue H1, PriorityQueue H2){
   if( H1 == NULL)
       return H2;
   if( H2 == NULL)
       return H1;
   if( H1->Element < H2->Element);
       return Merge1( H1, H2);
   else
       return Merge1( H2, H1);
}
PriorityQueue Merge1( PriorityQueue H1, PriorityQueue H2){
   if( H1->Left == NULL)
       H1->Left = H2;
   else{
       H1->Right = Merge( H1->Right, H2);
       if( H1->Left->Npl < H1->Right->Npl)
           SwapChildren( H1);
       H1->Npl = H1->Right->Npl + 1;
   }
   return H1;
}
//插入
PriorityQueue Insert1( ElementType X, PriorityQueue H){
   PriorityQueue SingleNode;
   SingleNode = malloc( sizeof( struct TreeNode));
   if( SingleNode == NULL)
       FatalError(" Out of space");
   else{
       SingleNode->Element = X;
       SingleNode->Npl = 0;
       SingleNode->Left = SingleNode->Right = NULL;
       H = Merge( SingleNode, H);
   }
   return H;
}
//删除
PriorityQueue DeleyeMin1( PriorityQueue H){
   PriorityQueue LeftHeap, RightHeap;
   is( IsEmpty( H)){
       Error(" Priority queue is empty");
       return H;
   }
   LeftHeap = H->Left;
   RightHeap = H->Right;
   free( H);
   return Merge( LeftHeap,RightHeap);
}

**6.7 斜堆**
* 斜堆(skew heap):斜堆是具备堆序的二叉树,可是不存在对树的结构限制。它是左式堆的自调节形式,但不一样于左式堆,关于任意节点的零路径长的任何信息不作保留。斜堆的右路径在任什么时候候均可以任意长,所以,全部操做的最坏情形运行时间为 $ O(N) $ . 与左式堆相同,斜堆的基本操做也是 Merge 合并。可是有一处例外,对于左式堆,咱们查看是否左儿子和右儿子知足左式堆堆序性质并交换那些不知足性质者;对于斜堆,除了这些右路径上全部节点的最大者不交换它们的左右儿子以外,交换是无条件的。

**6.8 二项队列**
**6.8.1 二项队列结构**

* 二项队列(binomial queue):一个二项队列不是一棵堆序的树,而是堆序树的集合,称为森林(forest).

* 堆序树中的每一棵都是有约束的形式,叫作二项树(binomial tree).

* 每个高度上之多存在一棵二项树。高度为 0 的二项树是一颗单节点树;高度为 k 的二项树 $ Bk $ 是经过将一棵二项树 $ B{k-1} $ 附接到另外一棵二项树 $ B{k-1} $ 的根上而构成的。以下图:二项树 $ B0、B一、B二、B三、B4 $ .

![](https://s4.51cto.com/images/blog/202102/27/e6225bd42d261d4b7d85cab2f96d8434.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

从图中看到,二项树 $ Bk $ 由一个带有儿子 $ B0 , B1, B2,..., B{k-1} $ 的根组成。高度为 k 的二项树刚好有 $ 2^k $ 个节点,而在深度 d 处的节点数为 $ Ck^d $ .

* 若是咱们把堆序施加到二项树上并容许任意高度上最多有一棵二项树,那么咱们可以用二项树的集合唯一地表示任意大小地优先队列。
for example:大小为 13 的优先队列能够用森林 $ B0 , B2, B3 $ 表示。咱们能够把这种表示写成 1101 ,这不只以二进制表示了 13 也表述 $ B1 $ 树不存在的事实。

**6.8.2 二项队列的操做**

* FindMin:能够经过搜索全部树的树根找出。因为最多有 $ logN $ 棵不一样的树,所以找到最小元的时间复杂度为 $ O(logN) $ . 另外,若是咱们记住当最小元在其余操做期间变化时更新它,那么咱们也可保留最小元的信息并以 $ O(1) $ 时间执行该操做。

* Merge:合并操做基本上是经过将两个队列加到一块儿来完成的。考虑两个二项队列 $ H1,H2 $ ,他们分别具备六个和七个元素,见下图。

![](https://s4.51cto.com/images/blog/202102/27/93f83bbdeb022740f9ac92d50f535a63.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
令 $ H_3 $ 是新的二项队列。

* 因为 $ H1 $ 没有高度为 0 的二项树而 $ H2 $ 拥有,所以咱们就用 $ H2 $ 中高度为 0 的二项树做为 $ H3 $ 的一部分。

* 因为 $ H一、H2 $ 都拥有高度为 1 的二项树,所以咱们令两者合称为 $ H_3 $ 中高度为 2 的二项树。

* 现存有三棵高度为 2 的树,咱们选择其中两个和合成高度为 3 的树,另一棵放到 $ H_3 $ 中。

* 因为如今 $ H一、H2 $ 不存在高度为 3 的树,合并结束。
$ H_3 $ 以下图:
![](https://s4.51cto.com/images/blog/202102/27/00470538ab98e0bc763e03ab51d3ced7.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

考虑 Merge 操做的时间复杂度,因为几乎使用任意合理的实现方法合并两棵二项树均花费常数时间,而总存在 $ O(logN) $ 棵二项树,所以合并在最坏情形下花费时间为 $ O(logN) $ .为了使操做更高效,咱们须要将这些树放到按照高度排血的二项队列中。

* Insert:插入操做其实是特殊情形的合并,咱们只须要建立一棵单节点树并执行一次合并操做。这种操做的最坏运行时间也是 $ O(logN) $ .更加准确地说,若是元素将要插入的那个优先队列不存在的最小的 $ B_k $ ,那么运行时间与 i+1 成正比.

* DeleteMin:经过首先找出一棵具备最小根的二项树来完成。令该树为 $ Bk $ ,并令原始的优先队列为 $ H $ ,咱们从 H 的树的森林中除去二项树 $ Bk $ ,造成新的二项树队列 $ H' $ ,再除去 $ Bk $ 的根,获得一些二项树 $ B0 , B1, B2,..., B{k-1} $ ,它们共同造成优先队列 $ H'' $ .合成 $ H' $ 与 $ H'' $ ,操做结束。
例如,假设有二项队列 $ H3 $ ,以下图:

![](https://s4.51cto.com/images/blog/202102/27/d2828c083076facf5fcac6d7d1ee688e.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

其中最小的根是 12,所以咱们获得两个优先队列 $ H' $ 和 $ H'' $ ,以下图:

![](https://s4.51cto.com/images/blog/202102/27/5d2be1654633284900ce5c447484d4f7.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

最后,合并 $ H' $ 和 $ H'' $ ,完成 DeleteMin 操做。

![](https://s4.51cto.com/images/blog/202102/27/79e83a037fedb85f914e35c17ebc8765.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

分析时间复杂度,注意,DeleteMin 操做将原队列一分为二,找出含有最小元素的树并建立队列 $ H' $ 和 $ H'' $ 花费时间为 $ O(logN) $ 时间,合并 $ H' $ 和 $ H'' $ 又花费时间为 $ O(logN) $ 时间。所以,整个 DeleteMin 操做的时间复杂度为 $ O(logN) $ .

**6.8.3 二项队列的实现**
* 二项树的每个节点包含一个数据,第一个儿子以及右兄弟。二项树中的诸儿子以递增的次序排列

![](https://s4.51cto.com/images/blog/202102/27/f3b692da1d03abb20141ee699bb2c6d1.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)

//声明
typedef struct BinNode Position;
typedef struct Collection
BinQueue;
struct BinNode{
   ElementType Element;
   Position LeftChild;
   Position NextSibling;
};
struct Collection{
   int CurrentSize;
   BinTree TheTrees[ MaxTrees];
};
//合并两颗一样大小的二项树
BinTree CombineTrees( BinTree T1, BinTree T2){
   if( T1->Element > T2->Element)
       return ConbinTrees( T2, T1);
   T2->NextSibling = T1->LeftChild;
   T1->LeftChild = T2;
   return T1;
}
//合并
BinQueue Merge( BinQueue H1, BinQueue H2){
   BinTree T1, T2, Carry = NULL;
   int i,j;
   if( H1->CurrentSize + H2->CurrentSize > Capacity)
       Error("Merge would exceed capacity");
   H1->CurrentSize += H2->CurrentSize;
   for( i = 0, j = 1; j <= H1->CurrentSize; i++){
       T1 = H1->TheTrees[i];
       T2 = H2->TheTrees[i];
       switch( !!T1 + 2 !!T2 + 4 !!Carry){//Carry 是上一步骤获得的树           case 0:     //No trees;           case 1:               break;           case 2:               H1->TheTrees[i] = T2;               H2->TheTrees[i] = NULL;           case 4:     //Only Carry               H1->TheTrees[i] = Carry;               break;           case 3:               Carry = CombineTrees( T1, T2);               H1->TheTrees[i] = H2->TheTrees[i] = NULL;               break;           case 5:               Carry = CombineTrees( T1, T2);               H1->TheTrees[i] = NULL;               break;           case 6:               Carry = CombineTrees( T1, T2);               H2->TheTrees[i] = NULL;               break;           case 7:     //All three               H1->TheTrees[i] = Carry;               Carry = CombineTrees( T1, T2);               H2->TheTrees[i] = NULL;               break;       }   }   return H1;}//删除最小元并返回ElementType DeleteMin( BinQueue H){   int i,j;   int MinTree;   BinQueue DeletedQueue;   Position DeletedTree, OldRoot;   ElementType MinItem;   if( IsEmpty( H)){       Error(" Empty binimial queue");       return -Infinity;   }   MinItem = I

相关文章
相关标签/搜索