平衡二叉树详解(Java实现)

一、概念

平衡二叉树是一种特殊的二叉搜索树,关于二叉搜索树,请查看上一篇博客二叉搜索树的java实现,那它有什么特别的地方呢,了解二叉搜索树的基本都清楚,在按顺序向插入二叉搜索树中插入值,最后会形成一个类似链表形式的树,而我们设计二叉搜索树的初衷,显然是看中了它的查找速度与它的高度成正比,如果每一颗二叉树都像链表一样,那就没什么意思了,所以就设计出来了平衡二叉树,相对于二叉搜索树,平衡二叉树的一个特点就是,在该树中,任意一个节点,它的左右子树的差的绝对值一定小于2。关于它的演变什么的,请自行网上搜索答案。在本文中,为了方便,也是采用了int型值插入。

二、平衡二叉树的构建

它的类相对于二叉搜索树也并没有什么特殊之处,不做过多讲解,直接上代码

static class Node{
        Node parent;
        Node leftChild;
        Node rightChild;
        int val;
        public Node(Node parent, Node leftChild, Node rightChild,int val) {
            super();
            this.parent = parent;
            this.leftChild = leftChild;
            this.rightChild = rightChild;
            this.val = val;
        }
        
        public Node(int val){
            this(null,null,null,val);
        }
        
        public Node(Node node,int val){
            this(node,null,null,val);
        }

    }

三、增加

在谈及平衡二叉树的增加,先来考虑什么样的情况会打破这个平衡,假如A树已经是一颗平衡二叉树,但现在要往里面插入一个元素,有这两种结果:(1)平衡未打破,这种肯定皆大欢喜;(2)平衡被打破了。

那一般要考虑三个问题:

1、平衡被打破之前是什么状态?

2、被打破之后又是一个什么样的状态?

3、平衡被打破了,该怎么调整,使它又重新成为一个平衡二叉树呢?

这里截取打破平衡后左子树的高度比右子树高度高2的所有可能情况(若右子树高,情况一样,这里只选取一种分析),下面的图,只是代表着那个被打破平衡的点的子树(被打破平衡的点就是这个节点的左右子树高度差的绝对值大于或等于2,当然,这里只能等于2),不代表整棵树。

在这里插入图片描述
这是第一种情况,其中A节点和B节点只是平衡二叉树的某一个子集合,要想打破这个平衡,那么插入的节点C必然在B的子节点上,即左右子节点,调整后面一起说。
在这里插入图片描述

这是第二种情况,其中A、B、C、D四个节点也是该平衡树的某个子集合,同样要打破这个平衡,那么,插入的节点F必然在D节点上。
在这里插入图片描述

第三种情况,其中A、B、C、D、E五个节点也是该平衡树的某个子集合,同样要打破这个平衡,那么,插入的节点F必然在D节点和E节点上。

(我个人所想到的所有可能情况,若还有其他情况,请在评论中指出,谢谢!)

或许细心的人已经发现,第二种和第三种情况就是由第一种情况变化而来的,如分别在A节点的右孩子和B节点的右孩子上添加子节点,就变化成了第二种和第三种情况(这里并不是说第一种情况直接加上这些节点就变成了第二或第三种情况)

这里只详细分析第一种情况

在这里插入图片描述
要使A节点的左右子树差的绝对值小于2,此时只需将B节点来替换A节点,A节点成为B节点的右孩子。若A节点有父节点,则A的父节点的子节点要去指向B节点,而A节点的父节点要去指向B节点,先看这段代码吧。这段操作也就是右旋操作

   /**
     * 在这种情况,因为A和B节点均没有右孩子节点,
     * 所以不用考虑太多
     * @param aNode 代表A节点
     * @return
     */
    public Node rightRotation(Node aNode){
        if(aNode != null){
            Node bNode = aNode.leftChild;// 先用一个变量来存储B节点
            bNode.parent = aNode.parent;// 重新分配A节点的父节点指向
            //判断A节点的父节点是否存在
            if(aNode.parent != null){// A节点不是根节点
                /**
                 * 分两种情况
                 *   1、A节点位于其父节点左边,则B节点也要位于左边
                 *   2、A节点位于其父节点右边,则B节点也要位于右边
                 */
                if(aNode.parent.leftChild == aNode){
                    aNode.parent.leftChild = bNode;
                }else{
                    aNode.parent.rightChild = bNode;
                }
            }else{// 说明A节点是根节点,直接将B节点置为根节点
                this.root = bNode;
            }
            bNode.rightChild = aNode;// 将B节点的右孩子置为A节点
            aNode.parent = bNode;// 将A节点的父节点置为B节点
            return bNode;// 返回旋转的节点
        }
        return null;
    }

而对于第一种情况的这个图
在这里插入图片描述
涉及的情况又不一样,假如按照上面那种情况一样去右旋,那么得到的图是或许是这样的
在这里插入图片描述
这好像又不平衡,似乎和原图是一个对称的。不太行的通。如果将C节点替换B节点位置,而B节点成为C节点的左节点,这样就成为了上一段代码的那种情况。这段B节点替换成为C节点的代码如下,这里操作也就是,先左旋后右旋

   /**
     * 
     * @param bNode 代表B节点
     * @return
     */
    public Node leftRotation(Node bNode){
        if(bNode != null){
            Node cNode = bNode.rightChild;// 用临时变量存储C节点
            cNode.parent = bNode.parent;
            // 这里因为bNode节点父节点存在,所以不需要判断。加判断也行,
            if(bNode.parent.rightChild == bNode){
                bNode.parent.rightChild = cNode;
            }else{
                bNode.parent.leftChild = cNode;
            }
            cNode.leftChild = bNode;
            bNode.parent = cNode;
            return cNode;
        }
        return null;
    }

代码逻辑和上一段代码一样。变换过来后,再按照上面的右旋再操作一次,就变成了平衡树了。

对于第二种和第三种情况的分析和第一种类似,再把代码修改一下,适合三种情况,即可。完整代码如下。

public Node leftRotation(Node node){
        if(node != null){
            Node leftChild = node.leftChild;// 用变量存储node节点的左子节点
            node.leftChild = leftChild.rightChild;// 将leftChild节点的右子节点赋值给node节点的左节点
            if(leftChild.rightChild != null){// 如果leftChild的右节点存在,则需将该右节点的父节点指给node节点
                leftChild.rightChild.parent = node;
            }
            leftChild.parent = node.parent;
            if(node.parent == null){// 即表明node节点为根节点
                this.root = leftChild;
            }else if(node.parent.rightChild == node){// 即node节点在它原父节点的右子树中
                node.parent.rightChild = leftChild;
            }else if(node.parent.leftChild == node){
                node.parent.leftChild = leftChild;
            }
            leftChild.rightChild = node;
            node.parent = leftChild;
            return leftChild;
        }
        return null;
    }

以下是右旋代码。逻辑参考以上分析

public Node rightRotation(Node node){
        if(node != null){
            Node rightChild = node.rightChild;
            node.rightChild = rightChild.leftChild;
            if(rightChild.leftChild != null){
                rightChild.leftChild.parent = node;
            }
            rightChild.parent = node.parent;
            if(node.parent == null){
                this.root = rightChild;
            }else if(node.parent.rightChild == node){
                node.parent.rightChild = rightChild;
            }else if(node.parent.leftChild == node){
                node.parent.leftChild = rightChild;
            }
            rightChild.leftChild = node;
            node.parent = rightChild;
            
        }
        return null;
    }

至此,打破平衡后,经过一系列操作达到平衡,由以上可知,大致有以下四种操作情况

一、只需要经过一次右旋即可达到平衡

二、只需要经过一次左旋即可达到平衡

三、需先经过左旋,再经过右旋也可达到平衡

四、需先经过右旋,再经过左旋也可达到平衡

那问题就来了,怎么判断被打破的平衡要经历哪种操作才能达到平衡呢?

经过了解,这四种情况,还可大致分为两大类,如下(以下的A节点就是被打破平衡的那个节点)

第一大类,A节点的左子树高度比右子树高度高2,最终需要经过右旋操作(可能需要先左后右)

第二大类,A节点的左子树高度比右子树高度低2,最终需要经过左旋操作(可能需要先右后左)

所以很容易想到,在插入节点后,判断插入的节点是在A节点的左子树还是右子树(因为插入之前已经是平衡二叉树)再决定采用哪个大类操作,在大类操作里再去细分要不要经历两步操作。

插入元素代码如下

public boolean put(int val){
        return putVal(root,val);
    }
    private boolean putVal(Node node,int val){
        if(node == null){// 初始化根节点
            node = new Node(val);
            root = node;
            size++;
            return true;
        }
        Node temp = node;
        Node p;
        int t;
        /**
         * 通过do while循环迭代获取最佳节点,
         */
        do{ 
            p = temp;
            t = temp.val-val;
            if(t > 0){
                temp = temp.leftChild;
            }else if(t < 0){
                temp = temp.rightChild;
            }else{
                temp.val = val;
                return false;
            }
        }while(temp != null);
        Node newNode = new Node(p, val);
        if(t > 0){
            p.leftChild = newNode;
        }else if(t < 0){
            p.rightChild = newNode;
        }
        rebuild(p);// 使二叉树平衡的方法
        size++;
        return true;
    }

这部分代码,详细分析可看上一篇博客,二叉搜索树的java实现。继续看rebuild方法的代码,这段代码采用了从插入节点父节点进行向上回溯去查找失去平衡的节点

private void rebuild(Node p){
        while(p != null){
            if(calcNodeBalanceValue(p) == 2){// 说明左子树高,需要右旋或者 先左旋后右旋
                fixAfterInsertion(p,LEFT);// 调整操作
            }else if(calcNodeBalanceValue(p) == -2){
                fixAfterInsertion(p,RIGHT);
            }
            p = p.parent;
        }
    }

那个calcNodeBalanceValue方法就是计算该参数的左右子树高度之差的方法。fixAfterInsertion方法是根据不同类型进行不同调整的方法,代码如下

private int calcNodeBalanceValue(Node node){
            if(node != null){
                return getHeightByNode(node);
            }
            return 0;
    }
    // 计算node节点的高度
    public int getChildDepth(Node node){
        if(node == null){
            return 0;
        }
        return 1+Math.max(getChildDepth(node.leftChild),getChildDepth(node.rightChild));
    }
    public int getHeightByNode(Node node){
        if(node == null){
            return 0;
        }
        return getChildDepth(node.leftChild)-getChildDepth(node.rightChild);
    }
/**
     * 调整树结构
     * @param p
     * @param type
     */
    private void fixAfterInsertion(Node p, int type) {
        // TODO Auto-generated method stub
        if(type == LEFT){
            final Node leftChild = p.leftChild;
            if(leftChild.leftChild != null){//右旋
                rightRotation(p);
            }else if(leftChild.rightChild != null){// 先左旋后右旋
                leftRotation(leftChild);
                rightRotation(p);
            }
        }else{
            final Node rightChild = p.rightChild;
            if(rightChild.rightChild != null){// 左旋
                leftRotation(p);
            }else if(rightChild.leftChild != null){// 先右旋,后左旋
                rightRotation(p);
                leftRotation(rightChild);
            }
        }
    }

在对每个大类再具体分析,我这里采用了 左右子树是否为空的判断来决定它是单旋还是双旋,我思考的原因:如果代码执行到了这个方法,那么肯定平衡被打破了,就暂且拿第一个大类来说 ,A的左子树高度要比右子树高2,意味平衡被打破了,再去结合上面分析的第一种情况,当插入元素后树结构是以下结构,那肯定是单旋
在这里插入图片描述

如果是以下结构,那肯定是这种结构,由上面分析,这种结构必须的双旋。
在这里插入图片描述
除了这两种情况,并没有其他旋转情况了。所以,我这里是根据插入的节点是位于B节点的左右方来决定是单旋还是双旋,(在这里,不保证结论完全正确,若有错误,还望大家指正)。

以上就是平衡二叉树的插入操作,以及后续的调整操作代码

四、删除

先来上一段二叉树的删除代码,关于具体的删除逻辑,请查看上一篇博客,这里只讨论重调整操作

p);
            }else if(rightChild.leftChild != null){// 先右旋,后左旋
                rightRotation(p);
                leftRotation(rightChild);
            }
        }
    }
    private int calcNodeBalanceValue(Node node){
            if(node != null){
                return getHeightByNode(node);
            }
            return 0;
    }
    public void print(){
        print(this.root);
    }
    public Node getNode(int val){
        Node temp = root;
        int t;
        do{
            t = temp.val-val;
            if(t > 0){
                temp = temp.leftChild;
            }else if(t < 0){
                temp = temp.rightChild;
            }else{
                return temp;
            }
        }while(temp != null);
        return null;
    }
    public boolean delete(int val){
        Node node = getNode(val);
        if(node == null){
            return false;
        }
        boolean flag = false;
        Node p = null;
        Node parent = node.parent;
        Node leftChild = node.leftChild;
        Node rightChild = node.rightChild;
        //以下所有父节点为空的情况,则表明删除的节点是根节点
        if(leftChild == null && rightChild == null){//没有子节点
            if(parent != null){
                if(parent.leftChild == node){
                    parent.leftChild = null;
                }else if(parent.rightChild == node){
                    parent.rightChild = null;
                }
            }else{//不存在父节点,则表明删除节点为根节点
                root = null;
            }
            p = parent;
            node = null;
            flag =  true;
        }else if(leftChild == null && rightChild != null){// 只有右节点
            if(parent != null && parent.val > val){// 存在父节点,且node位置为父节点的左边
                parent.leftChild = rightChild;
            }else if(parent != null && parent.val < val){// 存在父节点,且node位置为父节点的右边
                parent.rightChild = rightChild;
            }else{
                root = rightChild;
            }
            p = parent;
            node = null;
            flag =  true;
        }else if(leftChild != null && rightChild == null){// 只有左节点
            if(parent != null && parent.val > val){// 存在父节点,且node位置为父节点的左边
                parent.leftChild = leftChild;
            }else if(parent != null && parent.val < val){// 存在父节点,且node位置为父节点的右边
                parent.rightChild = leftChild;
            }else{
                root = leftChild;
            }
            p = parent;
            flag =  true;
        }else if(leftChild != null && rightChild != null){// 两个子节点都存在
            Node successor = getSuccessor(node);// 这种情况,一定存在后继节点
            int temp = successor.val;
            boolean delete = delete(temp);
            if(delete){
                node.val = temp;
            }
            p = successor;
            successor = null;
            flag =  true;
        }
        if(flag){
            rebuild(p);
        }
        return flag;
    }
    
    /**
     * 找到node节点的后继节点
     * 1、先判断该节点有没有右子树,如果有,则从右节点的左子树中寻找后继节点,没有则进行下一步
     * 2、查找该节点的父节点,若该父节点的右节点等于该节点,则继续寻找父节点,
     *   直至父节点为Null或找到不等于该节点的右节点。
     * 理由,后继节点一定比该节点大,若存在右子树,则后继节点一定存在右子树中,这是第一步的理由
     *      若不存在右子树,则也可能存在该节点的某个祖父节点(即该节点的父节点,或更上层父节点)的右子树中,
     *      对其迭代查找,若有,则返回该节点,没有则返回null
     * @param node
     * @return
     */
    private Node getSuccessor(Node node){
        if(node.rightChild != null){
            Node rightChild = node.rightChild;
            while(rightChild.leftChild != null){
                rightChild = rightChild.leftChild;
            }
            return rightChild;
        }
        Node parent = node.parent;
        while(parent != null && (node == parent.rightChild)){
            node = parent;
            parent = parent.parent;
        }
        return parent;
    }

这里也采用了插入操作的调整平衡代码。不做过多分析

五、遍历

这里采用了两种遍历,一种是中序遍历打印数据,第二种是层次遍历,以便查看调整后的数据是否正确

中序遍历

public void print(){
        print(this.root);
    }
    private void print(Node node){
        if(node != null){
            print(node.leftChild);
            System.out.println(node.val+",");
            print(node.rightChild);
        }
    }

层次遍历

/**
     * 层次遍历
     */
    public void printLeft(){
        if(this.root == null){
            return;
        }
        Queue<Node> queue = new LinkedList<>();
        Node temp = null;
        queue.add(root);
        while(!queue.isEmpty()){
            temp = queue.poll();
            System.out.print("节点值:"+temp.val+",平衡值:"+calcNodeBalanceValue(temp)+"\n");
            if(temp.leftChild != null){
                queue.add(temp.leftChild);
            }
            if(temp.rightChild != null){
                queue.add(temp.rightChild);
            }
        }
    }

六、测试

测试代码如下

@Test
    public void test_balanceTree(){
        BalanceBinaryTree bbt = new BalanceBinaryTree();
        bbt.put(10);
        bbt.put(9);
        bbt.put(11);
        bbt.put(7);
        bbt.put(12);
        bbt.put(8);
        bbt.put(38);
        bbt.put(24);
        bbt.put(17);
        bbt.put(4);
        bbt.put(3);
        System.out.println("----删除前的层次遍历-----");
        bbt.printLeft();
        System.out.println("------中序遍历---------");
        bbt.print();
        System.out.println();
        bbt.delete(9);
        System.out.println("----删除后的层次遍历-----");
        bbt.printLeft();
        System.out.println("------中序遍历---------");
        bbt.print();
    }

运行结果
在这里插入图片描述

在这里插入图片描述

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值