左偏树/左倾堆

学习的参考视频
原代码的引用文章

一.为什么要了解左偏树?

主要是一般的堆虽然插入、删除、取最值表现较好,但涉及到两个堆进行合并时,时间复杂度较高——左偏堆就是用来解决这个合并的问题的,当然后续还会学习斐波那契堆,那个也能有较好的时间复杂度

二.左偏树长什么样子?

在这里插入图片描述

三.左偏数的性质

  • 如上图所示,节点内的数字表示键值,这个键值的特点就是当前节点的键值一定小于子节点的键值(和小顶堆一样);
  • 其次是节点上面的蓝色数字,表示“距离”,“距离”的定义是里当前节点最近的外节点的距离,外节点就是没有子节点或者只有一个子节点的节点(比如上图的50和42这种),当然如果节点本身是外节点,那节点的“距离”就是0;
  • 左偏树保证节点的左子节点的距离不小于右子节点的距离——即左子节点的距离等于右子节点的距离+1,因此也定义空节点的距离为-1;
    另外,一个N节点的左偏树的根节点的距离最大为log(N+1)-1。

四.左偏树的操作

1.merge

有两个小顶堆,其中x和y分别为两个小顶堆的堆顶,x.val <= y.val
由于合并后要形成的也是小顶堆,索引合并好的堆顶还是x,因此递归合并 x的子节点(随便一边都行) 和 y
合并完后如果破坏了x的左偏性质,就要交换x的左右子节点
交换完后再计算x的距离(右子节点的距离+1)

2.push和pop

push就是新建一个新的节点和要push的堆合并一下就行
pop是只能pop堆顶节点,即用两个左右子节点合并就行,释放堆顶节点就行

五.参考代码

public class LeftistHeap<T extends Comparable<T>> {

    private LeftistNode<T> mRoot;    // 根结点

    private class LeftistNode<T extends Comparable<T>> {
        T key;                    // 关键字(键值)
        int npl;                // 零路径长度(Null Path Length)
        LeftistNode<T> left;    // 左孩子
        LeftistNode<T> right;    // 右孩子

        public LeftistNode(T key, LeftistNode<T> left, LeftistNode<T> right) {
            this.key = key;
            this.npl = 0;
            this.left = left;
            this.right = right;
        }

        public String toString() {
            return "key:"+key;
        }
    }

    public LeftistHeap() {
        mRoot = null;
    }

    /*
     * 前序遍历"左倾堆"
     */
    private void preOrder(LeftistNode<T> heap) {
        if(heap != null) {
            System.out.print(heap.key+" ");
            preOrder(heap.left);
            preOrder(heap.right);
        }
    }

    public void preOrder() {
        preOrder(mRoot);
    }

    /*
     * 中序遍历"左倾堆"
     */
    private void inOrder(LeftistNode<T> heap) {
        if(heap != null) {
            inOrder(heap.left);
            System.out.print(heap.key+" ");
            inOrder(heap.right);
        }
    }

    public void inOrder() {
        inOrder(mRoot);
    }

    /*
     * 后序遍历"左倾堆"
     */
    private void postOrder(LeftistNode<T> heap) {
        if(heap != null)
        {
            postOrder(heap.left);
            postOrder(heap.right);
            System.out.print(heap.key+" ");
        }
    }

    public void postOrder() {
        postOrder(mRoot);
    }

    /*
     * 合并"左倾堆x"和"左倾堆y"
     */
    private LeftistNode<T> merge(LeftistNode<T> x, LeftistNode<T> y) {
        //返回的是合并后的新堆的堆顶节点
        if(x == null) return y;
        if(y == null) return x;

        // 合并x和y时,将x作为合并后的树的根;
        // 这里的操作是保证: x的key < y的key
        //这一步很关键,结合下面和右节点merge,如果右节点的值大于y的值,那此时两个节点就交换了,此时x的右节点就是y了
        //而y就变成了x的右节点了!!!所以一定一定要对照例子看懂这一步,单看下面是不行的
        if(x.key.compareTo(y.key) > 0) {
            LeftistNode<T> tmp = x;
            x = y;
            y = tmp;
        }

        // 将x的右孩子和y合并,"合并后的树的根"是x的右孩子。
        x.right = merge(x.right, y);

        // 如果"x的左孩子为空" 或者 "x的左孩子的npl<右孩子的npl"
        // 则,交换x和y
        if (x.left == null || x.left.npl < x.right.npl) {
            LeftistNode<T> tmp = x.left;
            x.left = x.right;
            x.right = tmp;
        }
        if (x.right == null || x.left == null)
            x.npl = 0;
        else
            x.npl = (x.left.npl > x.right.npl) ? (x.right.npl + 1) : (x.left.npl + 1);

        return x;
    }

    public void merge(LeftistHeap<T> other) {
        this.mRoot = merge(this.mRoot, other.mRoot);
    }

    /*
     * 新建结点(key),并将其插入到左倾堆中
     */
    public void insert(T key) {
        LeftistNode<T> node = new LeftistNode<T>(key,null,null);

        // 如果新建结点失败,则返回。
        if (node != null)
            this.mRoot = merge(this.mRoot, node);
    }

    /*
     * 删除根结点,返回被删除的节点的键值
     */
    public T remove() {
        if (this.mRoot == null)
            return null;

        T key = this.mRoot.key;
        LeftistNode<T> l = this.mRoot.left;
        LeftistNode<T> r = this.mRoot.right;

        this.mRoot = null;          // 删除根节点
        this.mRoot = merge(l, r);   // 合并左右子树

        return key;
    }

    /*
     * 销毁左倾堆
     */
    private void destroy(LeftistNode<T> heap) {
        if (heap==null)
            return ;

        if (heap.left != null)
            destroy(heap.left);
        if (heap.right != null)
            destroy(heap.right);

        heap=null;
    }

    public void clear() {
        destroy(mRoot);
        mRoot = null;
    }

    /*
     * 打印"左倾堆"
     *
     * key        -- 节点的键值
     * direction  --  0,表示该节点是根节点;
     *               -1,表示该节点是它的父结点的左孩子;
     *                1,表示该节点是它的父结点的右孩子。
     */
    private void print(LeftistNode<T> heap, T key, int direction) {

        if(heap != null) {

            if(direction==0)    // heap是根节点
                System.out.printf("%2d(%d) is root\n", heap.key, heap.npl);
            else                // heap是分支节点
                System.out.printf("%2d(%d) is %2d's %6s child\n", heap.key, heap.npl, key, direction==1?"right" : "left");

            print(heap.left, heap.key, -1);
            print(heap.right,heap.key,  1);
        }
    }

    public void print() {
        if (mRoot != null)
            print(mRoot, mRoot.key, 0);
    }
}
public class test {

    public static void main(String[] args) {
        int a[]= {10,40,24,30,36,20,12,16};
        int b[]= {17,13,11,15,19,21,23};
        LeftistHeap<Integer> ha=new LeftistHeap<Integer>();
        LeftistHeap<Integer> hb=new LeftistHeap<Integer>();

        System.out.printf("== 左倾堆(ha)中依次添加: ");
        for(int i=0; i<a.length; i++) {
            System.out.printf("%d ", a[i]);
            ha.insert(a[i]);
        }
        System.out.printf("\n== 左倾堆(ha)的详细信息: \n");
        ha.print();


        System.out.printf("\n== 左倾堆(hb)中依次添加: ");
        for(int i=0; i<b.length; i++) {
            System.out.printf("%d ", b[i]);
            hb.insert(b[i]);
        }
        System.out.printf("\n== 左倾堆(hb)的详细信息: \n");
        hb.print();

        // 将"左倾堆hb"合并到"左倾堆ha"中。
        ha.merge(hb);
        System.out.printf("\n== 合并ha和hb后的详细信息: \n");
        ha.print();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夜以冀北

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值