左式堆

1 相关定义
1.1 零路径长度定义:

到没有两个儿子的节点最短距离, 即零路径长Npl 定义为 从 X 到一个没有两个儿子的 节点的最短路径的长;也即, 非叶子节点到叶子节点的最少边数,其中NULL的零路径长为-1, 叶子节点的零路径长为0
零路径长的定义—— 非叶子节点到叶子节点的最少边数,非常重要,因为左式堆的定义是基于零路径长的定义的
在这里插入图片描述

1.2 左式堆定义:

一棵具有堆序性质的二叉树零路径长:左儿子 ≧ 右儿子 + 父节点 = min{儿子} +1
干货——左式堆的定义是建立在具有堆序性的二叉树上,而不是二叉堆上

2 merge操作原则:

根值大的堆与根值小的堆的右子堆合并;(干货——merge操作原则)

3 merge操作存在的三种情况(设堆H1的根值小于H2)
  • case1) H1只有一个节点;
  • case2) H1根无右孩子;
  • case3) H1根有右孩子;
补充(Complementary):左式堆合并操作详解(merge)

左式堆合并原则:大根堆H2与小根堆H1的右子堆合并 (干货——左式堆合并原则)
具体分三种情况(设堆H1的根值小于H2)
在这里插入图片描述

  • case1)H1只有一个节点(只有它自己而已): H1只有一个节点,若出现不满足 零路径长:左儿子≧右儿子,交换左右孩子;
    在这里插入图片描述
    :上例中(中间所示堆),左儿子的零路径长为-1, 而右儿子的零路径长为0,所以不满足左式堆的条件, 需要交换左右孩子;
  • case2)H1根无右孩子: H1根无右孩子,若出现不满足:零路径长:左儿子≧右儿子,需要交换左右孩子。
    在这里插入图片描述
    :上例中(中间所示堆),左儿子的零路径长为0, 而右儿子的零路径长为1,所以不满足左式堆的条件,需要交换;
  • case3)H1根有右孩子
    • step1)截取H1的右子堆R1, 和截取H2的右子堆R2;
    • step2)将R1 与 R2进行merge操作得到H3, 且取R1和R2中较小根作为新根; (Attention: 现在你将看到,截取后的H1 和 H2, 以及新生成的H3 都是 case2);
    • step3)比较H3的左右孩子,是否满足左式堆要求,如果不满足则交换左右孩子;
    • step4)将H3与没有右子堆的H1进行merge操作,也即最后将case3 转换为了 case2;

在这里插入图片描述
在这里插入图片描述
结论

现在才知道,左式堆的merge操作其实是一个递归的过程,

递归求解情形:

  • 将具有根植较大的堆H2与具有根值较小的堆H1的右子堆合并
  • 将新产生的堆作为H1的右子堆
  • 交换H1的左右子堆直至合理

看如下解析; (干货——这是最后解析 merge 操作啦)
在这里插入图片描述
在这里插入图片描述

注意
  • 左式堆是建立在具有堆序性的二叉树上;
  • 左式堆是建立在零路径长上;
  • 左式堆的核心操作是 merge, 无论insert 还是 deleteMin 都是基于 merge操作的;
  • 左式堆的merge操作执行后,还要update 左式堆根节点的零路径长, 左式堆根节点的零路径长 == min{儿子的零路径长} +1;
  • update 后, 还需要比较 左右零路径长 是否满足左式堆的定义, 如果不满足,还需要交换左式堆根节点的左右孩子;
4 左式堆的插入操作

在知道左式堆合并的基础上,插入操作就很简单了。插入操作可以看成左式堆和只具有一个节点的左式堆的合并操作。那么其最坏时间复杂度也是log(N)。

5 左式堆的删除操作

左式堆的删除操作在理解合并操作的基础上也十分简单,分为如下两个步骤:

  • 返回根节点值

  • 删除根节点,将左子树和右子树合并,合并后的结果就是删除操作后的新左式堆。

6 左式堆的建堆过程

左式堆的建堆过程就是对输入元素重复插入过程,最坏时间复杂度Nlog(N),平均时间复杂度O(N)。

7 左式堆的java代码实现。

二叉堆因为是完全二叉树的结构所以采用数组的存储结构,但是左式堆不是完全二叉树需采用链式的结构(需要支持合并操作,也需要采用链式存储结构)。

java代码的如下:

package DataStructureAndAlgorithms.DataStructure.LeftistHeap;

/**
 * 左式堆(递归)
 */
public class LeftistHeap1 {
    public static void main(String[] args) {
        LeftistHeap1 heap = new LeftistHeap1(11);//建立只有一个根节点值为11的左式堆
        for (int i = 1; i < 10; i++) {
            heap.insert(i);
        }
        while (!heap.isEmpty()) {
            System.out.print(heap.deleteMin() + " ");
        }
    }

    //左式堆的节点的定义
    private static class HeapNode {
        private int npl;//零路径长 null path length
        private HeapNode left;//左孩子节点
        private HeapNode right;//右孩子节点
        private int val;//当前节点元素值

        public HeapNode(int val) {
            this.val = val;
            left = null;
            right = null;
            npl = -1;
        }
    }
    private HeapNode root;

    public LeftistHeap1(int val) {
        root = new HeapNode(val);
    }

    //合并两侧左式堆
    private void merge(LeftistHeap1 heap) {
        if (heap != null) {
            root = internalMerge(root, heap.root);//第一次插入时,root 为新建的根节点 11,heap.root = 1
        }
    }

    private HeapNode internalMerge(HeapNode h1, HeapNode h2) {
        if (h1 == null) return h2;
        if (h2 == null) return h1;

        HeapNode result;//当前堆的根节点
        //大根堆的根节点作为新的根节点,小根堆作为新的右孩子
        if (h1.val >= h2.val) {
            h2.right = internalMerge(h2.right, h1);
            result = h2;
        } else {
            h1.right = internalMerge(h1.right, h2);
            result = h1;
        }
        //如果不满足结构性,则调整,另外子节点为空 --> npl = -1,
        int leftNPL = result.left == null ? -1 : result.left.npl;//左孩子的零路径长
        int rightNPL = result.right == null ? -1 : result.right.npl;//右孩子的零路径长
        //左式堆满足左子节点零路径长大于等于右子节点,否则左右交换
        if (leftNPL < rightNPL) {
            HeapNode temp = result.right;
            result.right = result.left;
            result.left = temp;
        }
        //更新npl值,当前节点的子节点到非叶子节点(没有两个子节点的节点)的最短距离 + 1
        result.npl = Math.min(leftNPL, rightNPL) + 1;
        return result;
    }

    /**
     * 插入操作可以看成左式堆和只具有一个节点的左式堆的合并操作
      * @param val
     */
    private void insert(int val) {
        LeftistHeap1 heap = new LeftistHeap1(val);
        merge(heap);
    }

    //对外暴露的删除函数
    private int deleteMin() {
        if (isEmpty())
            return -1; //-1这里代表错误码,堆为空不删除。
        HeapNode node = root;
        root = internalMerge(root.left, root.right);
        return node.val;
    }

    private boolean isEmpty() {
        return root == null;
    }
}

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
很抱歉,我作为一个语言模型,无法直接运行代码,但我可以给您提供一个基本的思路和算法: 1.中缀表达式转后缀表达式: 利用堆栈stack,从左到右扫描中缀表达式,遇到数字直接输出,遇到运算符时,与堆栈栈顶的运算符比较优先级,如果堆栈为空或者栈顶运算符为左括号,则将当前运算符入栈;如果当前运算符优先级高于栈顶运算符,则将当前运算符入栈;否则将栈顶运算符弹出并输出,再次与栈顶运算符比较,直到当前运算符大于栈顶运算符为止。遇到左括号直接入栈,遇到右括号则将堆栈中的运算符弹出并输出,直到遇到左括号为止。扫描完后,如果堆栈中还有运算符,则全部弹出并输出。 2.后缀表达式构造二叉树: 从左到右扫描后缀表达式,遇到数字则将其作为叶子节点插入二叉树中,遇到运算符则将其作为根节点插入二叉树中,同时将堆栈中的两个节点作为左右子节点插入根节点中。遍历完后,堆栈中剩余的节点即为根节点。 3.后序遍历二叉树求值: 从左到右后序遍历二叉树,遇到数字直接入栈,遇到运算符则弹出栈顶的两个元素进行计算,并将计算结果入栈,遍历完后,栈中剩余的元素即为表达式的真值。 4.打印二叉树构造过程、后缀表达式和后序遍历序列: 采用递归的方式遍历二叉树,每次遍历到一个节点时,先打印该节点的值,然后遍历左子树和右子树,最后打印该节点的运算符。打印后缀表达式时,直接遍历后缀表达式并输出即可。 5.输入变量的值,计算表达式的真值和打印二叉树求值过程: 采用递归的方式遍历二叉树,并根据输入的变量值进行计算。每次遍历到一个节点时,如果是数字则直接入栈,如果是运算符则弹出栈顶的两个元素进行计算,并将计算结果入栈。遍历完后,栈中剩余的元素即为表达式的真值。同时,每次计算时都打印出当前节点的值以及计算结果,以展示二叉树的求值过程。 6.显示表达式的真值表: 采用穷举法,枚举所有变量的取值组合,并计算表达式的真值。将所有取值组合及对应的真值输出即可。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值