学习过程跟进黑皮书《数据结构与算法分析》,主要代码大致与树种例程相同,若有疏漏或错误,请务必提醒我,我会尽力修正。
左式堆Leftist Heap:
因为基础的堆结构由数组实现,并不支持合并等高级操作(有办法实现,但效率并不那么理想),为解决这些问题,左式堆提供了一些方案。
左式堆同样遵守最小堆的基本堆序——任意节点的关键字值低于其子树中的所有节点,但与之不同的是,左式堆的基本结构还包含了Npl(Null path length),即从该结点到达一个没有两个孩子的结点的最短距离。并要求:任意结点的左孩子的Npl大于或等于右孩子的Npl。
声明部分:(函数对于的作用已经写在注释里了)
//----------声明部分----------//
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:
PriorityQueue Initialize(void)
{
PriorityQueue H;
H = new TreeNode;
H->Left = H->Right = NULL;
H->Npl = 0;
return H;
}
规定NULL的Npl为-1,则对任何一个没有两个子树的节点,其Npl为0。
插入Insert:
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:
PriorityQueue DeleteMin1(PriorityQueue H)
{
if (IsEmpty(H))
exit;
PriorityQueue LeftHeap=H->Left, RightHeap=H->Right;
delete H;
return Merge(LeftHeap, RightHeap);
}
因为左式堆和最小堆有着同样的结构,所以最小值同样都是根节点,所以例程非常的简洁也很清晰。已经没必要做其他解释了。
合并堆Merge:
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是在不停的变动的,具体目标请以所指根节点为准。
经过一系列的比较,达到这行代码:
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,再次旋转。
合并完成。
很多时候,即便我仔细地捋顺了递归操作的流程,它的可读性仍然相当糟糕......但如果不去捋顺过程,又没办法改进其操作,甚至有的时候连利用都做不到。对于我这种出入数据结构的萌新来说,可能只能多看看代码来适应这种生活吧......