优先队列(堆)

First Post:

Last Update:

    学习过程跟进《数据结构与算法分析》,主要代码大致与树种例程相同,若有疏漏或错误,请务必提醒我,我会尽力修正。

目录:

  • [toc]优先队列(堆)
  • 最小堆HeapMin
  • 左式堆Leftist Heap
  • 二项队列Binomial-Queue

优先队列(堆):

    一种能够赋予每个节点不同优先级的数据结构。有“最小堆”和“最大堆”两种基础类型。实现的根本原理有两种,一种是“数组”,另外一种则是“树”(大多是指二叉树)。但在实现最大/最小堆时,使用数组更优。因为堆并不像树那样需要很多功能支持,自然也不需要用到指针(当然,高级结构还是会用到的,比如“左式堆”等,之后将有实现)。

    如果您此前已经看过堆的基本结构概念,那应该大致明白最小堆长什么样了,基础结构的堆就是一颗符合特定条件的二叉树罢了。

    特殊性质:对每一个节点的关键字,都要比其子树中的任何一个关键字都小(任何一个节点的关键字是其子树以及其自身中最小的那个)。这个条件是针对最小堆的,最大堆则反之。

    因为基础的堆结构只支持“插入”和“最小值出堆”这两种操作。在处理任务进程的时候,对应的也有“增加任务”和“处理任务量最少的任务”这种解释,或许这样更容易让人明白堆的作用。而最大堆则可将其解释为“处理优先级最高的任务”。(当然,实际上还需要对任务量/优先级进行变动,包括增/减关键字的大小这样的操作,自然也能够进行特定关键字的删改了)。

最小堆HeapMin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//-----------声明部分----------//
struct HeapStruct;
struct ElementType;
typedef struct HeapStruct* PriorityQueue;//堆指针
#define MinElements 1
#define Max 99999
bool IsEmpty(PriorityQueue H);//是否为空堆
bool IsFull(PriorityQueue H);//是否为满堆

void Insert(ElementType key, PriorityQueue H);//插入关键字
ElementType DeleteMin(PriorityQueue H);//删除最小值
PriorityQueue BuiltHeap(ElementType* Key, int N);//成堆
void PercolateDown(int i, PriorityQueue H);
PriorityQueue Initialize(int MaxElements);//建空堆
void IncreaseKey(int P, int Add, PriorityQueue H);//增加关键字值
void DecreaseKey(int P, int sub, PriorityQueue H);//降低关键字值
void Delete(int P, PriorityQueue H);//删除关键字
struct ElementType//关键字数据块
{
int Key;
};
struct HeapStruct//堆结构
{
int Capacity;
int Size;
ElementType* Element;
};
ElementType MinData;//最小数据块
//-----------声明部分----------//

     看注释大概就能明白了。但值得说明的是,因为最终是通过数组来实现的,而数组必须先行规定好它的尺寸,所以建立的堆也必须面临“被装满”的情况(当然,用new函数重新开辟也行,谁让这是C++呢)

建立空堆Initialize:

1
2
3
4
5
6
7
8
9
10
11
12
PriorityQueue Initialize(int MaxElements)//形参为堆的总节点数
{
PriorityQueue H;
if (MaxElements < MinElements)
return NULL;
H = new HeapStruct;
H->Element = new ElementType[MaxElements + 1];
H->Capacity = MaxElements;
H->Size = 0;
H->Element[0] = MinData;
return H;
}

    注:我并没有把new函数失败的情形写出来,但那些内容并不影响对数据结构的学习。对这方面有需求请自行添加。

    注:MinData是一个最小数据块,同时也只是一个冗余块。在之后的任何操作中,都不会对存有MinData的Element[0]进行任何操作。只是通过占用[0]节点,使得之后的操作变得更加可行了。需要注意的是,这个0节点并不是根节点(当时没绕过来,在这里浪费了太多时间)。

插入Insert:

1
2
3
4
5
6
7
8
9
10
void Insert(ElementType key, PriorityQueue H)
{
int i;
if (IsFull(H))
exit;
++H->Size;
for (i = H->Size; H->Element[i / 2].Key > key.Key; i /= 2)
H->Element[i] = H->Element[i / 2];
H->Element[i] = key;
}

    for循环中的判断方式被称之为“上滤”,也是这种方式得以实现的重要规则。对于根节点从 Element[1] 开始的这个堆,Element[i]的左儿子必然是Element[2*i],除非它没有左儿子。

    回到这个函数,因为int型会自动取整舍弃小数位,所以 Element[i/2] 必定指向 Element[i] 的父节点,不论它是不是单数。

    而这个寻路条件则是在不断的比较子节点与父节点的大小。流程如下:

    ①先将新节点放在数组的最后一位(并不是指数组的末尾,而是按照顺序装填的最后一位),然后比较它与父节点的大小。

    ②若它小于父节点,那么将其与父节点交换位置,此时 i/=2 , Element[i]再次指向它。

    ③继续相同操作。直到父节点小于它,或是没有父节点为止。

    不得不承认,这种操作很棒。因为它让函数的最坏时间复杂度降到了logN(因为实际操作中,不一定都要上履到最顶层)。

最小值出堆DeleteMin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ElementType DeleteMin(PriorityQueue H)
{
int i, Child;
ElementType MinElement, LastElement;
if (IsEmpty(H))
return H->Element[0];
MinElement = H->Element[1];
LastElement = H->Element[H->Size--];
for (i = 1; i * 2 <= H->Size; i = Child)
{
Child = 2 * i;
if (Child != H->Size && H->Element[Child + 1].Key < H->Element[Child].Key)
Child++;
if (LastElement.Key > H->Element[Child].Key)
H->Element[i] = H->Element[Child];
else
break;
}
H->Element[i] = LastElement;
return MinElement;
}

    同“上滤‘相近,在这个函数中运用的方法为”下滤“。简要谈谈过程吧:

    ①声明各种各样的变量,并判断H是不是一个空堆。

    ②将堆中最小的值Element[1]拷贝到MinElement中,同理将最后一个值放进LastElement中。(这个Element[1]将会被新值替换,而这个LastElement则要用来填补某个空缺)

    ③从i=1开始,Child则指向根节点Element[1]的左儿子,同时比较根节点的左右儿子大小,将Child指向小的那一个,我是说,H->Element[Child]会指向小的那个。

    ④然后再判断最后一个数和 H->Element[Child] 的大小。如果最后一个比较大,那就把父节点Element[i]用它的子节点替代。

    ⑤重新回到循环,现在的 i 已经指向了本来的子节点,并开始重复上述从③开始的操作,直到当前Element[i]的子节点中较小的那一个Element[Child]比最后一个节点的值要小为止。

   ⑥将现在的父节点Element[i]用最后一个替代。

    或许从途中就会觉得有些怪异,这究竟是个怎么回事。

    事实上,经过上述操作直到步骤⑤,最终的Element[i]将会指向某片叶子,这片叶子是根据其上的操作逐层筛选出来的。最后通过

1
H->Element[i] = LastElement;

    将这个位置用最后一位来替代,并返回了刚开始拷贝好的最小值,实现了删除最小值的操作。当然,实际上,这个数组的最后一位仍然保存着某个关键字,但并不需要太担心,因为经过了H->Size–,当下次插入节点的时候,遇到合适的数值,将会直接把这个位置覆盖掉。并且,也如您所见,所有的操作单元均在[1,H->Size]的范围内,对于范围外的元素,即便它还留有关键字,也不会再造成影响了。

成堆BuiltHeap:

    通常,我们将会导入一整串数组,然后再利用它们来生成一个堆结构。实际上,当然也可以通过Insert来一个个安置。以下是没套用Insert的例程,主要通过递归来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
PriorityQueue BuiltHeap(ElementType *Key,int N)//Key指向将要导入的数据数组
{
int i;
PriorityQueue H;
H = Initialize(N);
for (i = 1; i <= N; i++)
H->Element[i] = Key[i - 1];
H->Size = N;
for (i = N / 2; i > 0; i--)
PercolateDown(i, H);
return H;
}
void PercolateDown(int i,PriorityQueue H)
{
int MinSon;
ElementType Tmp;
if (i <( H->Size / 2))
{
if (2 * i + 1 <= H->Size && H->Element[2 * i].Key > H->Element[2 * i + 1].Key)
MinSon = 2 * i+1;
else
MinSon = 2 * i;
if (H->Element[i].Key > H->Element[MinSon].Key)
{
Tmp = H->Element[i];
H->Element[i] = H->Element[MinSon];
H->Element[MinSon] = Tmp;
}
PercolateDown(MinSon, H);
}
}

    ①声明,并建立空堆,然后把所有元素全都不按规则的塞进去,再指定好H->Size。

    ②从最后一个具有“父节点”性质的节点进入下滤函数。过程与DeleteMin相近:选出子节点中小的,再与父节点比较,将较小的那一个放在父节点的位置,而较大的那一个下沉到子节点。并且再次进入这个函数。

    ③实现全部的过滤之后,返回H。

    递归在这里是非常好用的。在BuiltHeap函数中,for循环实现了对每一个具有“父节点”性质的节点进行下滤(这是根据数组节点的排列顺序实现的,父节点必然都能按顺序排下去)。而递归则实现了对整条路径的下滤操作。假设从根节点开始下滤,那么必然会进入PercolateDown(MinSon,H)中,将较小的那个子节点作为本次递归的新的父节点同样进行下滤。最终实现了堆序(Heap order)。

    剩下的就是一些无关紧要的函数了,看看思路就行。因为是我自己写的,可能会有错误,如有发现,还请务必告知我,我会尽量修正。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void DecreaseKey(int P,int sub,PriorityQueue H)//降低关键字的值
{
H->Element[P].Key -= sub;
int i;
ElementType Tmp;
for (i = P; H->Element[i / 2].Key > H->Element[i].Key; i /= 2)
{
Tmp = H->Element[i / 2];
H->Element[i / 2] = H->Element[i];
H->Element[i] = Tmp;
}
}
void IncreaseKey(int P, int Add, PriorityQueue H)//提高关键字的值
{
int i,Child;
ElementType Tmp;
H->Element[P].Key += Add;
for (i = P; 2 * i <= H->Size; i = Child)
{
Child = 2 * i;
if (Child != H->Size && H->Element[Child + 1].Key < H->Element[Child].Key)
Child++;
if (H->Element[i].Key > H->Element[Child].Key)
{
Tmp = H->Element[Child];
H->Element[Child] = H->Element[i];
H->Element[i] = Tmp;
}
else
break;
}
}
void Delete(int P,PriorityQueue H)//删除指定关键字
{
DecreaseKey(P, Max, H);
DeleteMin(H);
}

    原理同上面的其他函数一样的,建议自己实现一下。

左式堆Leftist Heap:

    因为基础的堆结构由数组实现,所以并不支持合并等高级操作(有办法实现,但效率并不那么理想),为解决这些问题,左式堆提供了一些方案。

    左式堆同样遵守最小堆的基本堆序——任意节点的关键字值低于其子树中的所有节点,但与之不同的是,左式堆的基本结构还包含了Npl(Null path length),即从该结点到达一个没有两个孩子的结点的最短距离。并要求:任意结点的左孩子的Npl大于或等于右孩子的Npl。

声明部分:(函数对应的作用已经写在注释里了)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//----------声明部分----------//
typedef struct TreeNode* PriorityQueue;//节点指针
struct TreeNode//节点结构
{
int Element;
PriorityQueue Left;
PriorityQueue Right;
int Npl;//Null Path Length
};
PriorityQueue Initialize(void);//建立空堆
PriorityQueue Merge(PriorityQueue H1, PriorityQueue H2);//合并堆(驱动例程)
static PriorityQueue Merge1(PriorityQueue H1, PriorityQueue H2);//合并堆(实际例程)
void SwapChildren(PriorityQueue H);(交换H的左右子树)
PriorityQueue Insert1(int key, PriorityQueue H);//插入节点
bool IsEmpty(PriorityQueue H);//是否为空堆
PriorityQueue DeleteMin1(PriorityQueue H);//删除最小值
//----------声明部分----------//

建立空堆Initialize:

1
2
3
4
5
6
7
8
PriorityQueue Initialize(void)
{
PriorityQueue H;
H = new TreeNode;
H->Left = H->Right = NULL;
H->Npl = 0;
return H;
}

规定NULL的Npl为-1,则对任何一个没有两个子树的节点,其Npl为0。

插入Insert:

1
2
3
4
5
6
7
8
9
10
PriorityQueue Insert1(int key, PriorityQueue H)
{
PriorityQueue SingleNode;
SingleNode = new TreeNode;
SingleNode->Element = key;
SingleNode->Npl = 0;
SingleNode->Left = SingleNode->Right = NULL;
H = Merge(SingleNode, H);
return H;
}

    区别于最小堆中的Insert函数,这里用的是Insert1。因为Insert没有返回值,也不需要返回值,所有那样做是没有问题的;但在左式堆中,将一个元素插入空堆时,需要返回新的根节点地址,所以应有一些区别。另外,这个函数首次出现了Merge函数。关于Merge函数将会放在最后,目前权且当它是一个合并两个堆,并返回新的根节点的函数即可。(目前我个人还不会写宏定义,但如果您已经学会了,不妨试着将Insert函数写成宏定义,书上是这样建议的)

删除最小值Delete:

1
2
3
4
5
6
7
8
PriorityQueue DeleteMin1(PriorityQueue H)
{
if (IsEmpty(H))
exit;
PriorityQueue LeftHeap=H->Left, RightHeap=H->Right;
delete H;
return Merge(LeftHeap, RightHeap);
}

    因为左式堆和最小堆有着同样的结构,所以最小值同样都是根节点,所以例程非常的简洁也很清晰。已经没必要做其他解释了。

合并堆Merge:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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);
}
static 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;
}
void SwapChildren(PriorityQueue H)//交换子树
{
PriorityQueue Tmp;
Tmp = H->Left;
H->Left = H->Right;
H->Right = Tmp;
}

    最后是关键的合并堆函数。Merge函数作为合并开始的入口被调用,而实现过程则放在Merge1函数中进行。SwapChildren函数是附带的,你当然也可以把它写在Merge1中。

    对于没有图的过程描述,我觉得实在有些难以想象。这里引用书上的例子(尽管这个例子并不方便,但在某些地方能起到很好的范例)。

     现在,不妨先假设根节点为3的堆为H1,另外一个为H2。现在将它们放入Merge(H1,H2)。注:以下所说的H1和H2是在不停的变动的,具体目标请以所指根节点为准。

    经过一系列的比较,达到这行代码:

1
H1->Right = Merge(H1->Right, H2);

    ①将H1->Right指向 根节点为8的堆 与 H2 合并的结果。

    同理,经过一系列的比较。现在,H1的根节点是6,H2的根节点是8。

    ②再次遇到相同的情况,H1->Right指向 根节点为7的堆 与 H2(根节点为8的堆) 合并的结果。

    再次经过一系列的比较。现在H1的根节点是7 ,H2的根节点是8。

    ③同上,令H1->Right 指向 H1(根节点为18的堆) 与 H2(根节点为8的堆)合并 的结果。

    上一行描述合并后的结果显而易见,只是将18放到了8的右儿子处罢了。然后返回新根 8 的地址。

    现在,③行处的H1->Right指向新根 8。即 7->8。

    判断Npl,并将左右子树进行一次交换。

    以上内容实现了 根节点为3的右子树与 根节点为6的堆 的合并过程。

    回到①行中方的H1->Right,其现在指向了新的根 6。判断Npl,再次旋转。

    合并完成。

    很多时候,即便我仔细地捋顺了递归操作的流程,它的可读性仍然相当糟糕……但如果不去捋顺过程,又没办法改进其操作,甚至有的时候连利用都做不到。对于我这种出入数据结构的萌新来说,可能只能多看看代码来适应这种生活吧……

二项队列Binomial Queue:

(以下不只是简介,还包括了一些个人理解,如果您学习过程遇到什么麻烦,不妨先看看)

    根据书上的描述,似乎是左式堆的一种改良版。虽然左式堆和斜堆每次操作都花费logN时间,且有效支持了插入、合并与最小值出堆,但其每次操作花费常数平均时间来支持插入。而在二项队列中。每次操作的最坏情况运行时间为logN,而插入操作平均花费常数时间。这算是在一定程度上优化了斜堆。

    其结构就如名字一样,是“二项”。我们可以将其简单理解为“上项”和“下项”(这只是为了方便理解罢了,实际运用中自然不存在这种称呼,但我总要找个名字给它,不然描述起来还挺费劲的)。实际的样子当您看到图片的时候就能明白,我为什么要那样称呼它们了。

    并且,二项队列的样子也特殊一点。它是一种被称之为“森林”的结构,形象的说,它包括了许多中不同高度的二叉树(但每一种高度的树只有一颗,一旦出现两颗同样高度,它们就会被立刻合并成新的高度,这也是特色之一)。并且,它也有最小堆的特性关键字的数值随高度递减,每一个根节点的值都比子树中任何一个节点的关键字小。

    如图,这便算是一个简单的二项队列结构。上面是一个数组,数组中存放有指针。而下面的则是许多的树(剥去数组,你看到的才是真正的二项队结构,数组只是从计算机中实现的一种方法罢了。并且,B3中的那颗树和我们实际实现的有些不同,具体的情况后面会写。但目前,权且当它就长这个样吧(或许这才是本该有的结构,但计算机不方便这样做,所以之后会有另外一个实现的样子))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//-------------声明部分---------------//
typedef struct BinNode* Position;//位置指针
typedef struct BinNode* BinTree;//树指针
typedef struct Collection* BinQueue;//队列指针
#define MaxTrees 5 //数组的长度,也同时规定了二叉树的高度
#define Capacity ((1<<MaxTrees)-1)//容量是2^0+2^1+......+2^(MAXTREES-1)
BinQueue Initialize(void);//建立空队列
BinTree CombineTrees(BinTree T1, BinTree T2);//合并高度相同的树
BinQueue Merge(BinQueue H1, BinQueue H2);//合并两个队列
int DeleteMin(BinQueue H);
void Insert(int X, BinQueue H);
int IsEmpty(BinQueue H);

struct BinNode //树节点
{
int Key;
Position LeftChild;
Position NextSibling;
};
struct Collection //森林
{
int CurrentSize; //已容纳量
BinTree TheTrees[MaxTrees];//容纳二叉树的数组
};
//-------------声明部分---------------//

    因为书上没有说明一些变量的作用,所以我自己绕了一会,在这里顺便说明一下吧:

    CurrentSize:已容纳量。指的是整个队列的节点数。比方说上图中的的容纳量就是15(对应总共15个节点)。

    Capacity:队列容量。指的是一个二项队列结构最高能容纳的节点数。比方说上图的队列容量就是(B3——15)(也因为我画的不太好,所以B3看起来高度不像3,但会意一下就行,实在不行去找找其他大佬的图也行)。(但写法是一个等比数列求和结果,很明显,每个高度的节点数是等比增加的)

    LeftChild/NextSibling:连接指针。这个东西具体到后面看见实际的图片时,自然会懂。

这幅图为实际做出的结构,以下说明的时候请经常对照以方便理解。高度相同的节点我已经尽量画在同一水平线了,也如您所见,B1没有节点,B3的高度确实是3(建立在B0处的节点高度设定为0的基础上)。

关于LeftChild和NextSibling指针已经标出(取首字母表示)。

建立队列Initialize:

1
2
3
4
5
6
7
8
BinQueue Initialize(void)
{
BinQueue H = new Collection;
for (int i = 0; i < MaxTrees; i++)
H->TheTrees[i] = NULL;
H->CurrentSize = 0;
return H;
}

    没什么好说的,但因为书上没有,加上我当时不太明白几个参数的作用,所以绕了好一会,贴在这里以防万一。(至少如果不明白CurrentSize是什么,就没办法让它等于0了……)

插入节点Insert:

1
2
3
4
5
6
7
8
9
10
11
void Insert(int X, BinQueue H)
{
BinQueue temp = initialize();
temp->CurrentSize = 1;
temp->TheTrees[0] = new BinNode;
temp->TheTrees[0]->Key = X;
temp->TheTrees[0]->LeftChild = NULL;
temp->TheTrees[0]->NextSibling = NULL;
Merge(H, temp);
delete temp;
}

    从这个函数可以看出,所谓的插入节点,实际上是将新节点当作了一个只有B0结构的二项队列,然后将其合并。目前,我们只需要将Merge函数视作一个合并二项队列的函数即可,关于这个函数会在下面讲到。

最小值出队DeleteMin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
int DeleteMin(BinQueue H)
{
int i, j;
int MinTree;
BinQueue DeleteQueue;
Position DeleteTree, OldRoot;
int MinItem;//ElementType

if (IsEmpty(H))
return NULL;

MinItem=INFINITY;
for (i = 0; i < MaxTrees; i++)
{
if (H->TheTrees[i] && H->TheTrees[i]->Key < MinItem)
{
MinItem = H->TheTrees[i]->Key;
MinTree = i;
}
}
DeleteTree = H->TheTrees[MinTree];
OldRoot = DeleteTree;
DeleteTree = DeleteTree->LeftChild;
delete OldRoot;

DeleteQueue = initialize();
DeleteQueue->CurrentSize = (1 << MinTree) - 1;
for (j = MinTree - 1; j >= 0; j--)
{
DeleteQueue->TheTrees[j] = DeleteTree;
DeleteTree = DeleteTree->NextSibling;
DeleteQueue->TheTrees[j]->NextSibling = NULL;
}
H->TheTrees[MinTree] = NULL;
H->CurrentSize -= (DeleteQueue->CurrentSize + 1);
Merge(H, DeleteQueue);
return MinItem;
}

    函数本身不算难,但有些冗长。姑且做些说明,但自己写出来是最有效的理解方式。

    ①一系列将要用到的声明。其中MinItem是将要出堆的Key(因为我设定的Key是int类型),再将MinItem设定为无限大(Infinity)。

    ②遍历队列数组,选出队列中最小的关键字节点。用MinTree标记其对应的索引,MinItem拷贝其数值。

    ③将标记好的最小值节点拷贝到DeleteTree与OldRoot,再把DeleteTree指向其左儿子。删除最小值节点。

    ④将刚才拷贝的左儿子新建到另外一个队列里,设定好相关的数值,最后把两个队列合并。

    值得注意的是,for循环是将失去了根节点的堆重新整合到新队列中。这个操作看起来有些抽象,但实际上是可行的。不妨带入B3节点来试探一下,删去了根节点后,它被拆分成了B0,B1,B2三棵树进入新队列了。最开始的那幅图其实很好的说明了问题,那张图的B3有这明显的复制粘贴B2的痕迹,但事实就如描述一样,它们真的就是像复制粘贴一样的结构。所有你可以试着去拆分一下,Bk去掉根节点必然会变成B0,B1,B2……Bk-1颗树。

    以及另外一个注意点:

1
H->CurrentSize -= (DeleteQueue->CurrentSize + 1);

    其实不太必要在这个地方纠结太久,但以防万一还是说明一下。这行代码减去的数量将在Merge函数中补齐,先后的总节点数差距确实是 1 ,可以自行验证一下。如果缺乏这条函数,Merge将会导致CurrentSize与实际不符。(之所以减去那个量,是因为Merge会补回DeleteQueue->CurrentSize的数量,和这段语句正好相差 1 )

合并队列Merge:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
BinTree CombineTrees(BinTree T1,BinTree T2)
{
if (T1->Key > T2->Key)
return CombineTrees(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)
exit;
H1->CurrentSize += H2->CurrentSize;
for (i = 0, j = 1; j <= H1->CurrentSize; i++, j *= 2)
{
T1 = H1->TheTrees[i]; T2 = H2->TheTrees[i];
switch(!!T1+2*!!T2+4*!!Carry)
{
case 0://No tree
case 1://only h1
break;
case 2://only h2
H1->TheTrees[i] = T2;
H2->TheTrees[i] = NULL;
break;
case 4://only carry
H1->TheTrees[i] = Carry;
Carry = NULL;
break;
case 3://h1 and h2
Carry = CombineTrees(T1, T2);
break;
case 5://h1 and carry
Carry = CombineTrees(T1,Carry);
H1->TheTrees[i] = NULL;
break;
case 6://h2 and carry
Carry = CombineTrees(T2,Carry);
H2->TheTrees[i] = NULL;
break;
case 7://h1 and h2 and carry
H1->TheTrees[i] = Carry;
Carry = CombineTrees(T1,T2);
H2->TheTrees[i] = NULL;
break;
}
}
return H1;
}

    最后是关键性的合并函数Merge。这个例程还需要用到CombineTrees函数,用于合并高度相同的树。

    函数本身并不是很复杂,用了一个switch来判断情况,这个方式相当有趣,是很值得学习的一种想法。(!!符号似乎是用来判断存在性的(我不太清楚这样描述对不对,所有用“似乎”),若值存在且非0,则返回1,否则返回0)。

    以及比较有趣的是for循环的判断条件 j<=H1->CurrentSize 和 j*=2

    看起来有些抽象,解释起来也是。

    现在的H1->CurrentSize已经是合并结束后的总节点数量了,而这个数量直接关系到for循环需要抵达哪一个高度的数组格。(比如说,我的数组最高能到B20,但现在根本没有那么多树需要存放,最高的树高度只到B5,那如果全都扫一遍,岂不是浪费了很多时间?)

    所有才有 j*=2这个条件来制约。如您所见,每个高度的节点数实际上是固定的 2^Bk(2的Bk次方,k指高度,如B0,B1等)。这就涉及到了一些数学关系,所有我就不写这了。只需要捋一捋,我想很快就会发现这神奇的操作(如果最高到B5,那这个for循环进行4次之后,它的 j 就会超出范围导致for循环终止)。

    流程其实没必要细讲了,函数写的很清楚了,注释也有,笔记的目的已经达成了,那就到这吧。