d-堆
类似于二叉堆,但是它有d个儿子,此时,d-堆比二叉堆要浅很多,因此插入操作更快了,但是相对的删除操作更耗时。因为,需要在d个儿子中找到最大的,但是很多算法中插入操作要远多于删除操作,因此,这种加速是现实的。
除了不能执行find去查找一般的元素外,两个堆的合并也很困难。
左式堆
左式堆可以有效的解决上面说的堆合并的问题。合并就涉及插入删除,很显然使用数组不合适,因此,左式堆使用指针来实现。左式堆和二叉堆的区别:左式堆是不平衡的。它两个重要属性:键值和零距离
零距离(英文名NPL,即Null Path Length)则是从一个节点到一个没有两个儿子的节点(只有0个或1个儿子的节点)的路径长度。具有0个或1个儿子的节点的NPL为0,NULL节点的NPL为-1。
- 节点的左孩子的NPL >= 右孩子的NPL。
- 节点的NPL = 它的右孩子的NPL + 1。
- 在有路径上有r个节点的左式堆必然至少有2^r - 1个节点。
typedef int Type; typedef struct _LeftistNode{ Type val; int npl; // 零路经长度(Null Path Length) struct _LeftistNode *left; // 左孩子 struct _LeftistNode *right; // 右孩子 }LeftistNode, *LeftistHeap;
合并
合并操作是左倾堆的重点。插入式合并的特殊情况。
合并两个左倾堆(最小堆)的基本思想如下:
- 如果一个空左倾堆与一个非空左倾堆合并,返回非空左倾堆。
- 如果两个左倾堆都非空,那么比较两个根节点,取较小堆的根节点为新的根节点。将"较小堆的根节点的右孩子"和"较大堆"进行合并;该合并过程和上面的过程一样,这样递归合并下去,最终两个堆合并完成。 但是新推可能不再满足左式堆的性质,需要调整:(调整的过程是在合并的同时完成的)
- 如果新堆的右孩子的NPL > 左孩子的NPL,则交换左右孩子。
- 设置新堆的根节点的NPL = 右子堆NPL + 1
实现时,通过递归自底向上合并并调整使得满足左式堆的性质。
LeftistNode* mergeLeftist(LeftistHeap x, LeftistHeap y){ if (x == nullptr)return y; if (y == nullptr)return x; LeftistHeap l, r;//以l为根,l较小 if (x->val < y->val){ l = x; r = y; } else { l = y; r = x; } l->right = mergeLeftist(l->right,r);//合并l->right和r if (!l->left || l->left->npl < l->right->npl){//判断是否需要交换左右子树 LeftistHeap temp = l->left; l->left = l->right; l->right = temp; } //更新npl if (!l->right || !l->left)l->npl = 0; else l->npl = l->left->npl > l->right->npl ? l->right->npl + 1 : l->left->npl + 1; return l; }
合并左式堆的操作可以看出来,它的时间复杂度和有路径的长成正比,因此复杂度O(logn)
添加节点就可以看做是一个左式堆和一个单点的左式堆合并;
删除树根节点可以看做是删除树根后,左右子树的两个左式堆合并;
因此,他们都可以通过合并来实现。它对应的复杂度也是O(logn)
插入和删除的实现:
斜堆
斜堆是左式堆的自调节形式,左式堆和斜堆的关系类似于伸展树和AVL树的关系。斜堆具有堆序的性质,但是没有结构的限制,这样的话一次的操作最坏的情况时O(n),但是连续m次操作总的复杂度O(mlogn)。
与左式堆相同,斜堆的基本操作也是合并操作。但是斜堆没有零距离的属性,合并的方法也有区别:
- 如果一个空斜堆与一个非空斜堆合并,返回非空斜堆。
- 如果两个斜堆都非空,那么比较两个根节点,取较小堆的根节点为新的根节点。将"较小堆的根节点的右孩子"和"较大堆"进行合并。
- 合并后,交换新堆根节点的左孩子和右孩子。
- 这一步是斜堆和左倾堆的合并操作差别的关键所在,如果是左倾堆,则合并后要比较左右孩子的零距离大小,若右孩子的零距离 > 左孩子的零距离,则交换左右孩子;最后,在设置根的零距离。
斜堆的结构
typedef int Type; typedef struct _SkewNode{ Type val; struct _SkewNode *left; // 左孩子 struct _SkewNode *right; // 右孩子 }SkewNode, *SkewHeap;
合并的实现
SkewNode* mergeSkewHeap(SkewHeap x, SkewHeap y){ if (x == nullptr)return y; if (y == nullptr)return x; SkewHeap l, r;//以l为根,l较小 if (x->val < y->val){ l = x; r = y; } else { l = y; r = x; } SkewNode* temp = mergeSkewHeap(l->right, r);//合并l->right和r l->right = l->left;//交换左右子树 l->left = temp; return l; }
同样的道理,插入和删除根节点的操作都可以使用合并来实现。