java实现平衡二叉树

本文参考海纳的两篇文章,需要补平衡二叉树知识的请看这里

参照的文章是这篇文章

可以直接去看这两篇文章,再回头看我这篇文章,所以我就去繁就简。

代码

package com.yubotao;

/** * @Auther: yubt * @Description: * @Date: Created in 9:23 2018/6/8 * @Modified By: */
public class AVLNode {
    public int data;
    public int depth;
    public int balance;
    public AVLNode parent;
    public AVLNode left;
    public AVLNode right;

    public AVLNode(int data){
        this.data = data;
        depth = 1;
        balance = 0;
        left = null;
        right = null;
    }

    public void insert(AVLNode root, int data){
        if (data < root.data){
            if (root.left != null){
                insert(root.left, data);
            }else {
                root.left = new AVLNode(data);
                root.left.parent = root;
            }
        }else {
            if (root.right != null){
                insert(root.right, data);
            }else {
                root.right = new AVLNode(data);
                root.right.parent = root;
            }
        }

        // 从插入的过程回溯回来的时候,计算平衡因子
        root.balance = calcBalance(root);

        // 左子树高,应该右旋
        if (root.balance >= 2){
            // 右孙高,先左旋
            if (root.left.balance == -1){
                left_rotate(root.left);
            }
            right_rotate(root);
        }

        // 右子树高,左旋
        if (root.balance <= -2){
            // 左孙高,先右旋
            if (root.right.balance == 1){
                right_rotate(root.right);
            }
            left_rotate(root);
        }

        root.balance = calcBalance(root);
        root.depth = calcDepth(root);
    }

    // 右旋
    private void right_rotate(AVLNode p){
        // 一次旋转涉及到的结点包括祖父,父亲,右儿子
        AVLNode pParent = p.parent;
        AVLNode pLeftSon = p.left;
        AVLNode pRightGrandSon = pLeftSon.right;

        // 左子变父
        pLeftSon.parent = pParent;
        if (pParent != null){
            if (p == pParent.left){
                pParent.left = pLeftSon;
            }else if (p == pParent.right){
                pParent.right = pLeftSon;
            }
        }

        pLeftSon.right = p;
        p.parent = pLeftSon;

        // 右孙变左孙
        p.left = pRightGrandSon;
        if (pRightGrandSon != null){
            pRightGrandSon.parent = p;
        }

        p.depth = calcDepth(p);
        p.balance = calcBalance(p);

        pLeftSon.depth = calcDepth(pLeftSon);
        pLeftSon.balance = calcBalance(pLeftSon);
    }

    private void left_rotate(AVLNode p){
        // 一次选择涉及到的结点包括祖父,父亲,左儿子
        AVLNode pParent = p.parent;
        AVLNode pRightSon = p.right;
        AVLNode pLeftGrandSon = pRightSon.left;

        // 右子变父
        pRightSon.parent = pParent;
        if (pParent != null){
            if (p == pParent.right){
                pParent.right = pRightSon;
            }else if (p == pParent.left){
                pParent.left = pRightSon;
            }
        }

        pRightSon.left = p;
        p.parent = pRightSon;

        // 左孙变右孙
        p.right = pLeftGrandSon;
        if (pLeftGrandSon != null){
            pLeftGrandSon.parent = p;
        }

        p.depth = calcDepth(p);
        p.balance = calcBalance(p);

        pRightSon.depth = calcDepth(pRightSon);
        pRightSon.balance = calcBalance(pRightSon);
    }

    public int calcBalance(AVLNode p){
        int left_depth;
        int right_depth;

        if (p.left != null){
            left_depth = p.left.depth;
        }else {
            left_depth = 0;
        }

        if (p.right != null){
            right_depth = p.right.depth;
        }else {
            right_depth = 0;
        }

        return left_depth - right_depth;
    }

    public int calcDepth(AVLNode p){
        int depth = 0;
        if (p.left != null){
            depth = p.left.depth;
        }

        if (p.right != null && depth < p.right.depth){
            depth = p.right.depth;
        }

        depth++;
        return depth;
    }

}

这里阐述几个概念:

平衡因子:该结点左右子树的深度(高度)差。

深度(高度):树的层数。

如图
《java实现平衡二叉树》
四棵树的深度依次为:4,3,4,3.

这两个概念理解好才能更加快速的理解上面的代码。

插入原理讲解

我们先看第一种情况:
《java实现平衡二叉树》

c节点也就是黄色节点是我们新插入的节点,此时树不平衡,需要进行操作。根据各节点平衡因子判断(可参照代码),我们需要先右旋后左旋。这里已经把每个节点都做了相应的标记了,相关步骤就是代码的执行顺序。

我只做一次代码解读,后面的自己看图对应一下。

首先,我们进入insert()方法,此时应该是新建一个节点c。执行到方法体中的开头部分

if (data < root.data){
            if (root.left != null){
                insert(root.left, data);
            }else {
                root.left = new AVLNode(data);
                root.left.parent = root;
            }
        }

此时走else分支,新建了一个结点,并指定c的parent结点。
然后我们这时需要执行到这步:

// 从插入的过程回溯回来的时候,计算平衡因子
        root.balance = calcBalance(root);

计算b结点的balance,为1。后续两个if判断跳过,并计算了b结点的balance为1,depth为2.

此时怎么样了呢?

我们从a结点的insert()方法中的开头部分

else {
            if (root.right != null){
                insert(root.right, data);
            }else {
                root.right = new AVLNode(data);
                root.right.parent = root;
            }
        }

if分支跳出(因为我们插入的时候,a有右子树b,所以当时进入了b结点的insert()方法)。
接下来计算a结点的balance,为-2,进入第二个if分支

 // 右子树高,左旋
        if (root.balance <= -2){
            // 左孙高,先右旋
            if (root.right.balance == 1){
                right_rotate(root.right);
            }
            left_rotate(root);
        }

并且b结点的balance为1,符合条件,开始进行右旋操作。进入方法right_rotate()中。
这时p为b结点,所以就如上图标识,按照代码顺序执行此次右旋操作。

// 右旋
    private void right_rotate(AVLNode p){
        // 一次旋转涉及到的结点包括祖父,父亲,右儿子
        AVLNode pParent = p.parent;
        AVLNode pLeftSon = p.left;
        AVLNode pRightGrandSon = pLeftSon.right;

        // 左子变父
        pLeftSon.parent = pParent; // 图中左1
        if (pParent != null){
            if (p == pParent.left){
                pParent.left = pLeftSon;  // 图中左2
            }else if (p == pParent.right){
                pParent.right = pLeftSon; 
            }
        }

        pLeftSon.right = p; // 图中左3
        p.parent = pLeftSon;  // 图中左4

        // 右孙变左孙
        p.left = pRightGrandSon;  // 图中左5
        if (pRightGrandSon != null){
            pRightGrandSon.parent = p;
        }

        p.depth = calcDepth(p);
        p.balance = calcBalance(p);

        pLeftSon.depth = calcDepth(pLeftSon);
        pLeftSon.balance = calcBalance(pLeftSon);
    }

接下来进入左旋方法left_rotate(),注意此时的p结点为a结点。
如上图右边标识,再看一下代码执行顺序:

private void left_rotate(AVLNode p){
        // 一次选择涉及到的结点包括祖父,父亲,左儿子
        AVLNode pParent = p.parent;
        AVLNode pRightSon = p.right;
        AVLNode pLeftGrandSon = pRightSon.left;

        // 右子变父
        pRightSon.parent = pParent; // 图中右1
        if (pParent != null){
            if (p == pParent.right){
                pParent.right = pRightSon;
            }else if (p == pParent.left){
                pParent.left = pRightSon;
            }
        }

        pRightSon.left = p;  // 图中右2
        p.parent = pRightSon;  // 图中右3

        // 左孙变右孙
        p.right = pLeftGrandSon;  // 图中右4
        if (pLeftGrandSon != null){
            pLeftGrandSon.parent = p;
        }

        p.depth = calcDepth(p);
        p.balance = calcBalance(p);

        pRightSon.depth = calcDepth(pRightSon);
        pRightSon.balance = calcBalance(pRightSon);
    }

此时,完成整个插入过程。

接下来我们再看两种情况。
《java实现平衡二叉树》

可以看到圈出的两部分就是上面两种情况。所以就不一一赘述。

最后再看更复杂的两种情况,只贴图,可以自己跟着代码顺序捋一捋,熟悉一下这个过程,我就不一一分析了。

《java实现平衡二叉树》

《java实现平衡二叉树》

删除方法

这部分待续,我还没完全想好,抽个时间再来更新吧。
主要删除的时候需要考虑,如何处理待删结点有子节点的情况。
如果待删结点是叶子结点,那么很容易删除,然后向上回溯,看一下各结点的平衡因子,并执行相关的左右旋方法即可。
但是如果待删结点有左右子树时,这个时候就要考虑如何操作了,怎样将该结点删除又不影响其子节点。
我的一个想法是:将待删结点变为叶子结点,这样就好操作了,不过这个代码怎么写,如果同时有左右结点,和哪个结点互换,是不是都行呢?
有个想法是如果待删结点有左右子树,将该结点与中序遍历的前一个结点交换,如果其还有左右子树,再交换,直到结点无左右子树为止(递归),然后删除,在向上回溯,判断其平衡因子。

先思考到这里,有兴趣的朋友可以留言,一起讨论。

终于抽时间把删除的代码写完了,调试了很久,想的时候思路挺清晰的,但是一旦写的时候就能看出之前考虑的很多地方都不是很周全。

大概耗时2个多小时。先贴一下代码

public void delete(AVLNode root, int data){
        if (data < root.data)
            delete(root.left, data);
        else if (data > root.data)
            delete(root.right, data);
        else{
            if (root.left != null)
                exchange(root, root.left);
            else if (root.right != null)
                exchange(root, root.right);

            // 当data节点为子节点时
            if (data == root.data && root.left == null && root.right == null){
                if (root.parent.left.data == root.data){
                    root.parent.left = null;
                    root.parent = null;
                }else if (root.parent.right.data == root.data){
                    root.parent.right = null;
                    root.parent = null;
                }
            }

        }

        // 回溯,计算平衡因子
        root.balance = calcBalance(root);

        // 左子树高,应该右旋
        if (root.balance >= 2){
            // 右孙高,先左旋
            if (root.left.balance == -1){
                left_rotate(root.left);
            }
            right_rotate(root);
        }

        // 右子树高,左旋
        if (root.balance <= -2){
            // 左孙高,先右旋
            if (root.right.balance == 1){
                right_rotate(root.right);
            }
            left_rotate(root);
        }

        root.balance = calcBalance(root);
        root.depth = calcDepth(root);
    }
private void exchange(AVLNode p, AVLNode pSon){
        int temp;

        temp = p.data;
        p.data = pSon.data;
        pSon.data = temp;

        // 当data节点不为子节点时
        if (pSon.left == null && pSon.right == null){
            if (pSon.parent.left.data == pSon.data){
                pSon.parent.left = null;
                pSon.parent = null;
            }else if (pSon.parent.right.data == pSon.data){
                pSon.parent.right = null;
                pSon.parent = null;
            }
            // 保证删除子节点后的p的平衡因子和深度正确,这样才能保证整体的正确
            p.depth = calcDepth(p);
            p.balance = calcBalance(p);
            return;
        }

        if (pSon.left != null){
            exchange(pSon, pSon.left);

            // 回溯时需计算平衡因子及深度,否则会出现错误
            p.depth = calcDepth(p);
            p.balance = calcBalance(p);
            // 保证递归回溯的时候不进行其他分支操作
            return;
        }
        if (pSon.right != null){
            exchange(pSon, pSon.right);

            p.depth = calcDepth(p);
            p.balance = calcBalance(p);
            return;
        }

    }

基本的坑位我都给标记出来了。

测试代码:

public class TestAVLNode {
    public static void main(String[] args) {
        AVLNode one = new AVLNode(5);
        AVLNode two = new AVLNode(3);
        AVLNode three = new AVLNode(7);
        AVLNode four = new AVLNode(2);
        AVLNode five = new AVLNode(4);
        AVLNode six = new AVLNode(6);
        AVLNode seven = new AVLNode(1);

        one.left = two;
        one.right = three;
        two.parent = one;
        three.parent = one;

        two.left = four;
        two.right = five;
        four.parent = two;
        five.parent = two;

        three.left = six;
        six.parent = three;

        four.left = seven;
        seven.parent = four;

        one.balance = 1;
        one.depth = 4;

        two.balance = 1;
        two.depth = 3;

        three.balance = 1;
        three.depth = 2;

        four.balance = 1;
        four.depth = 2;

        five.balance = 0;
        five.depth = 1;

        six.balance = 0;
        six.depth = 1;

        seven.balance = 0;
        seven.depth = 1;

        System.out.println(one);

        one.delete(one, 7);

        System.out.println(two);
    }
}

测试树长这样
《java实现平衡二叉树》

说一下整个过程碰到的几个问题。

首先是交换值方法。因为被删节点如果不是子节点,就需要把它交换到子节点位置,然后再删除。所以为了写这个交换方法,我先写了一个单链表的交换,比较简单:

package com.yubotao;

/** * @Auther: yubt * @Description: * @Date: Created in 18:50 2018/6/21 * @Modified By: */
public class TestExchange {
    private static class One{
        private One next;
        private int data;

        public One getNext() {
            return next;
        }

        public void setNext(One next) {
            this.next = next;
        }

        public int getData() {
            return data;
        }

        public void setData(int data) {
            this.data = data;
        }

        @Override
        public String toString() {
            return "One{" +
                    "data=" + data +
                    '}';
        }
    }

    public static void main(String[] args) {
        One one = new One();
        One two = new One();
        One three = new One();
        One four = new One();

        one.setData(1);
        one.setNext(two);
        two.setData(2);
        two.setNext(three);
        three.setData(3);
        three.setNext(four);
        four.setData(4);

        System.out.println("one: " + one + "," + "two: " + two + "," + "three: " + three + "," + "four: " + four + ".");

        exchange(one, two);

        System.out.println("one: " + one + "," + "two: " + two + "," + "three: " + three + "," + "four: " + four + ".");
    }

    // 死循环
    static void exchange(One root, One next){

        int temp;
        temp = root.data;
        root.data = next.data;
        next.data = temp;

        while (next.next == null){
            return;
        }
        exchange(next,next.next);

    }
}

可以看到我标注了死循环,这就是踩得第一个坑,没有很好的找到递归退出的条件,后来就用了最简单的条件

while (next.next == null){
            return;
        }

开始时想的退出条件有一些问题,当时写的是:

static void exchange(One root, One next){
        while (next.next != null){
            exchange(next,next.next);
        }
        int temp;
        temp = root.data;
        root.data = next.data;
        next.data = temp;
    }

对,你没有看错! 这个没有退出条件,刚才我试了一下,即使加上退出条件还是有问题的。

static void exchange(One root, One next){
        while (next.next != null){
            exchange(next,next.next);
        }
        int temp;
        temp = root.data;
        root.data = next.data;
        next.data = temp;

        return;
    }

主要问题就出在while关键字上,另外数据交换部位也应该在条件判断的前面,否则结果也会不对。
修改成这样

static void exchange(One root, One next){

        int temp;
        temp = root.data;
        root.data = next.data;
        next.data = temp;

        if (next.next != null){
            exchange(next,next.next);
        }

        return;
    }

可以看出已经具备我们想要的方法的雏形了。

既然单链表完成了,应用到二叉树其实就是一个分支判断的问题,所以这块其实比较简单,没什么说的。

接下来的问题是,删除节点应该在哪个位置
最开始我是放到delete方法中的,写的是这样的

else{
            if (root.left != null)
                exchange(root, root.left);
            else if (root.right != null)
                exchange(root, root.right);

            // 当data节点为子节点时
            if (root.left == null && root.right == null){
                if (root.parent.left.data == root.data){
                    root.parent.left = null;
                    root.parent = null;
                }else if (root.parent.right.data == root.data){
                    root.parent.right = null;
                    root.parent = null;
                }
            }

        }

这时我们看到exchange中没有相关删除方法,就出现了一个问题,如果待删节点不是子节点,而是交换的节点,那么我们只是把值交换了,并没有删除节点。这里可能一下子有点看不明白,我建议你画下这个exchange的递归图。**注意出口位置!**你就会明白我说的意思了。

打个比方
《java实现平衡二叉树》
你在图中的3号节点进入exchange方法,最后3号换到了1号位置。这时退出了exchange方法,**注意!**这时的delete方法中的root节点现在值为2,但还是原3号节点!故不会删除原值为1,现值为3那个子节点。

那么我觉得删除节点应该放到exchange中来做,这样不就ok了嘛。
但是!如果data值为子节点,是不会执行exchange方法的!所以最后得出结论,无论是exchange还是delete方法,都需要删除节点的操作,但是需要区分一下情况。
delete方法中的是删除data值就为子节点的。

            // 当data节点为子节点时
            if (data == root.data && root.left == null && root.right == null){
                if (root.parent.left.data == root.data){
                    root.parent.left = null;
                    root.parent = null;
                }else if (root.parent.right.data == root.data){
                    root.parent.right = null;
                    root.parent = null;
                }
            }

注意!data == root.data这个条件不可或缺,这里我不点出来,你可以测试一下,没有这个值的时候,有什么问题。
提醒,方法的执行顺序。

最后的问题就是平衡因子和深度总是出问题,以及递归回溯的过程中出现的一些怪相。
比如这段代码就把这块的问题概括了。

if (pSon.left != null){
     exchange(pSon, pSon.left);

     // 回溯时需计算平衡因子及深度,否则会出现错误
     p.depth = calcDepth(p);
     p.balance = calcBalance(p);
     // 保证递归回溯的时候不进行其他分支操作
     return;
}

首先是return;,它很关键,开始的时候我没有写这个,就会出现这样的情况,递归回溯的时候,继续走下一个条件判断,导致本来从左子树回溯上来的,又和右子树做了一次exchange。所以必须加上return;

之后是

p.depth = calcDepth(p);
p.balance = calcBalance(p);

这两句计算需要在每次回溯的时候重新计算。
一开始的时候,我认为只要最下面一个子树的平衡因子和深度正确,那么在delete方法回溯的时候,其他的也会计算正确。但是我忘记了exchange方法回溯的时候也需要重新计算的,否则只要有一个节点有问题,就会影响其上的祖先节点出现问题!

大概到这里也就总结的差不多了。可能我写的方法不够优雅,简洁,效率可能也很有问题,但是毕竟是自己思考以及一点一点调试写出来的,我觉得我应该鼓励一下自己。

我是真滴流批!

哈哈哈~

    原文作者:平衡二叉树
    原文地址: https://blog.csdn.net/Ybt_c_index/article/details/80623974
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞