在介绍左式堆的操作之1前,我们先回忆下左式堆的性质:对于堆中的每一个节点X,左儿子的零路径长至少与右儿子的零路长相同。并由此产生了以下结论:
1、左式堆是不平衡的,其偏重于向左增加深度。
2、对于任意一个节点X,从该节点出发持续向右,则会得到最短的零路径长。
3、在右路径上有 r 个节点的左式堆必然至少有2^r - 1个节点。
由于左式堆的实现是为了满足普通的二叉堆不能实现的 Merge 例程,因此,我们先考虑如何实现 Merge。
应当重点注意的是,合并之后的每个节点仍然满座左式堆的性质,因此,我们应该对每一个节点进行适当的 Merge 操作,进一步地来看,我们应该采用递归的思想对每一个节点进行操作。
如何进行递归,则应该考虑对于每个节点都能使用的操作以及对于特殊的情况应该执行的操作。
1、对于特殊情况:一个堆为空,返回另一个堆。应当注意的是,由于函数例程是递归的,因此此处一个堆为空的情况也能代表一个子节点为空的情况。
2、合并的具体思路:先考虑堆序性质:父节点必然小于子节点,因此对于要合并的两个堆来说,根节点存储的元素较大的一方必然会成为另一个堆的子节点。与特殊情况相同的是,这合并的两个堆并不一定是指原堆,而同时也可以指由根节点的子节点向下构成的堆。( 即擦掉根节点和另外一个儿子形成的新堆。)
因此,对于两个堆的合并,比较二者根节点的值的大小,进而将具有大的根的值的堆与具有小的根的值的堆的右子堆进行合并。同时考虑这是一个递归例程,因此我们会再次比较新的两个堆的根的值,从而再次合并,直到找到一个不存在根的堆。即为空的节点。同时,由于我们合并到了右子堆上,所以必然在合并后必然不符合左式堆的结构性质,从而我们将左子堆和右子堆交换位置。
以下是C语言的实现。
typedef int ElementType;
struct TreeNode {
ElementType Element;
PriorityQueue Left;
PriorityQueue Right;
int Npl;
};
typedef TreeNode* PriorityQueue;
PriorityQueue Merge(PriorityQueue H1, PriorityQueue H2)
{
if (H1 == NULL)return H2;
if (H2 == NULL)return H1;
if (H1->Element < H2->Element)
return Mergel(H1, H2);
else return Mergel(H2, H1);
}
static PriorityQueue Mergel(PriorityQueue H1, PriorityQueue H2)
{
if (H1->Left == NULL)H1->Left = H2;
else
{
H1->Right = Merge(H1->Right, H2);
if (H1->Left->Npl < H2->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;
free(Tmp);
}
在以上的代码中,Merge函数的目的是当出现空的根节点是返回另一个根节点。即在合并的过程中对特殊情况进行处理,或者说对在递归的过程中的对必然会出现的空节点的情形进行处理。同时,Merge函数还比较两个将要合并的堆的根的元素的大小。因为Mergel函数默认H1的根节点的元素小于H2的根节点元素,进而即Mergel函数会把H2合并为H1的子节点。
除了Merge例程之外,左式堆也能实现普通的二叉堆的基础Insert、Delete例程。
其中,Insert 例程可以看作一个节点与堆的合并。
PriorityQueue Insertl(ElementType X, PriorityQueue H)
{
PriorityQueue SingleNode;
SingleNode = (PriorityQueue)malloc(sizeof(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 Deletel(PriorityQueue H)
{
PriorityQueue LeftHeap, RightHeap;
if (IsEmpty(H))
{
FatalError("PriorityQueue queue is empty.");
return H;
}
LeftHeap = H->Left;
RightHeap = H->Right;
free(H);
return Mergel(LeftHeap, RightHeap);
}
void FatalError(const char* s)
{
printf("%s", s);
}
bool IsEmpty(PriorityQueue H)
{
return H == NULL;
}
之后介绍的是斜堆(skew heap)。
斜堆与左式堆的关系类似于伸展树与的关系。
因此,在介绍斜堆之前,我们先复习以下AVL树与伸展树。AVL树要求同一个父亲的左子树和右子树的高度差最多为1。
而伸展树则在AVL树的基础上,要求在一个节点被访问后,将该节点的树移向距离根更近的节点,以便于再次访问。
同时,AVL树具有的基本操作为 单旋转和双旋转。而伸展树则在此的基础上增加了 展开 的操作以便于把访问过的节点推向树根。
而斜堆是具有堆序性质的二叉树,但与二叉堆不同的是,其不保留关于零路径长的任何信息,因此就不需要使用SwapChildren来交换左右儿子。