priority heap通常是以数组来实现的,用数组下标作为堆的序号来实现heap。但是在C语言中数组是不能改变大小的(新标准里可能可以改变哈),于是便不能动态地增加堆的大小。
于是想到链表动态增加的特•。链表实现优先堆的难点在于,堆序的乘2除2操作要求能即时获得数组下标,而链表不能随机存取只能顺序存取,对于DeleteMin操作,getMin很容易,只要保存了Child和Parent节点的指针,上滤操作也没什么难度。但是Insert操作就呵呵了,因为并不知道当前堆的末尾在哪,遍历寻至末尾的话,复杂度又上升至O(N^2)。BuildHeap更不用说。
先看看堆树的特点
1
2 3
4 5 6 7
8 9 10 11 12 13 14 15
...
显然,对于堆序而言,序号确定了,在堆树中的位置也确定了。由此可见,如果知道当前堆的元素个数,那么最后一个元素的位置也就确定了。但是怎样找到这个位置呢?我在火车上睡不着,想出了这个方法。。。
先将序号转化成二进制:
1
10 | 11
100 101 | 110 111
1000 1001 1010 1011 | 1100...
...
用|来分隔树的左右两边。
观察后可以得出,位于左半部分的最高两位都是10,位于右半部分的最高两位都是11。
这是显然的,除开根的序号,对于堆树的每一层而言,左边部分的第一个,都是2、4、8、16...2^D。右边部分第一个都是3、6、12... 2^D+2^(D-1),也就是左边部分的序号加上这一层的节点个数的一半,就处于右边部分了:2+1,4+2,8+4,16+8...,换成二进制就是10XXX...+01XXX...=11XXX...,这就是前面观察的结论了。
首先要想到堆树是个二叉树,从根开始,不断地向左子树或者右子树寻找,便可以最终到达树底下,中途无非就是个判断是左还是右的问题。联系上面的观察结果,算法就很明朗了:
确定一个数处于堆树中的哪个位置,只要先把这个数用二进制表示,不断左移,取溢出位,从遇到的第一个1开始(第一个1表示遇到根了),遇到一个0便转向左子树,遇到一个1便转向右子树。简言之就是遇0则左,遇1则右。这个算法具有递归性,对子树又实施本算法,当到达叶子节点时,这个数便左移完了,这时也正好到达这个数在堆树中的位置。
所以,如果知道了堆的大小,便知道了如何从根走到末尾的位置,也就知道了新元素要Insert的位置。显然,这个算法的复杂度是``` 这里输入代码 NlogN的。代价是需要两个指针变量来指向LeftChild和RightChild,以及还要维持一个Parent指针,用来实现插入之后的上滤操作。
所以堆树节点的结构体可以是:
struct Node
{
ElementType Element;
Node * LeftChild;
Node * RightChild;
Node * Parent;
}Node;
因为堆树的位置是相对固定的,所以在进行Insert后的上滤操作和DeleteMin后的下滤操作时,在需要交换的地方,只交换Element就可以了,不必交换Node指针。可以想象成堆树就是一个框,元素就是放在框中的东西,交换时只交换东西而不必交换整个框。数组和链表的两种实现,不同之处在于数组实现的优先堆是用数组下标作为框,链表实现的优先堆是用结构体作为框而已。
实际上,数组实现的优先堆中,乘二和除二操作正是左移和右移运算。
实现了Insert操作,BuildHeap可以用Insert来间接实现,从一个空的堆树开始逐个Insert就可以了。有了LeftChild和RightChild以及Parent,DeleteMin的上滤操作也很容易实现。