堆之左式堆和斜堆

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;
}

同样的道理,插入和删除根节点的操作都可以使用合并来实现。

原文地址:https://www.cnblogs.com/yeqluofwupheng/p/7450643.html