左式堆—流程分析与代码实现(C++)

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

左式堆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,再次旋转。

    合并完成。

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

猜你喜欢

转载自blog.csdn.net/Tokameine/article/details/113807373