平衡二叉树及其旋转实现原理(Java实现)

今天我们来聊一聊

平衡二叉树

的基本实现原理。
我们都知道,平衡二叉树是二叉搜索树的优化版本,进一步提高了二叉搜索树的查找效率。
如:当我们用{1,2,3,4,5,6,7}这样的顺序来创建一棵二叉搜索树并添加相应的节点时,根据二叉搜索树的对应添加方式,最终就会出现这样的情况:
在这里插入图片描述
当我们用这样的二叉搜索树查找时,单论时间复杂度,与普通的链表是一样的。(比如我们需要查找“7”这个节点,就需要从头开始比对,直到比对成功为止。)而二叉搜索树存在的目的,就是为了提高查找效率,所以我们就需要思考,到底什么样子的二叉搜索树,查找的效率才会最高呢?

我们不难发现,二叉搜索树的基本思想其实与查找算法中的“二分法”极其类似,都是通过比对大小,进一步缩小查找范围,从而减少查找次数的。对于二分法来说,最终折半拆分的次数就代表了查找的次数,再来看二叉查找树,如果我们把每个节点的左右分支当成一次拆分的话,

那么是不是只要拆分的次数越少,我们比对的次数也就越少,相应着查找效率也就越高了呢?

在这里插入图片描述

再来观察这棵二叉搜索树,如果我们依旧需要查找“7”所对应的节点,实际上只用对比两次(三次)就能找到了。

对比上面的两棵树,我们可以总结出一条规律:二叉搜索树的层数越少,其相对搜索效率也就越高。

那么我们如何保证一棵二叉搜索树的层数最少呢?

这就要引出我们今天的主题:平衡二叉树了。

平衡二叉树的定义:

平衡二叉树(Balanced Binary Tree)具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

我们针对这条定义思索一下,平衡二叉树要求左右子树的高度差绝对值不能大于1,这不就相当于规定了不能出现第一张图的那种“一边倒”的情况吗。同样可以经过思考和举例得出,平衡二叉树就是在二叉搜索树基础上的最小层数版本。

关于平衡二叉树的旋转问题:

如果我们想把一棵普通的二叉搜索树优化成一棵平衡二叉树,就需要对其中的节点进行“旋转”,
如,有这样一棵二叉搜索树:
在这里插入图片描述
我们把目光放在“1”这个节点上,1的左子树高度为0,但是右子树高度为2,其差值为2大于1,所以这不是一棵平衡二叉树。想要把这颗树变为平衡二叉树,我们就需要进行“左旋转”。即把左上角的节点旋转下去。
得到了:
在这里插入图片描述
这时候再来检查一次,无论是1、2还是3,他们的左右子树高度差都不大于1,所以这样就成为了一棵平衡二叉树。
相应的,如果是整体向左斜的二叉搜索树(左斜树),我们就用“右旋转”的方法,把其在右上角的节点旋转下去就可以了,这里就不画图来举例了。

这种只需要旋转一次就可以成为一棵平衡二叉树的旋转我们称之为“单旋转”,对应的“双旋转”我们后面会探讨。

那么这时候又产生了一个新的问题,除了上面说的两种情况外,还有什么情况是需要单旋转的呢?

经过一些思考以及网上资料的翻阅,我发现经过递归之后单旋转的情况只有四种(实际上只有两种):
左旋转除了上述的右斜树以外,还有:
在这里插入图片描述
看最上面的节点(根节点),其左子树高度为1,但右子树高度为3,差值大于一,所以需要进行左旋转。(由于二叉搜索树的特殊性,其旋转的实现方式比较特殊,我们稍后会说到。)

右旋转除了上述的左斜树以外,还有:
在这里插入图片描述

现在我们来说这两种情况下旋转的实现方式:

以右旋转来举例,我们为这棵树的节点附上一些值。
在这里插入图片描述
可以看到现在虽然是一棵二叉搜索树,但是其查找效率并不是很高,值为8的左子树高度比右子树高度大了2,所以现在我们来把它优化为平衡二叉树。
第一步:我们先创建一个新节点,其值也为8。(姑且称这个节点为新节点,原来的8称为原节点。)
第二步:把原节点的右节点(9),赋给新节点的右节点。就变成了这样子。

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

第三步:把原节点左节点的右节点(7)赋给新节点的左节点。
在这里插入图片描述在这里插入图片描述
第四步:删掉原节点(原来的8),并把新节点赋值给原节点的左节点(6),作为右节点。
最终就成了这样:

在这里插入图片描述
可以看到,此时的二叉树已经成为了平衡二叉树,搜索效率比起之前来说大大提升了。

这里只演示了右旋转的情况,左旋转用同样的方式只是反过来而已。

其实演示到这里,细心的朋友应该已经发现我之前留下的伏笔,在前面我曾经说过:实际上无论是左单旋转还是右单旋转,都只有一种情况。那种左斜树或者右斜树的情况,实际上就是刚刚所演示的这种情况的简化版,仅仅只是把一些节点看成空节点就可以了。

下面我们再说双旋转的例子。

在有些情况下,仅仅靠单旋转是不能把一棵普通的二叉搜索树变成平衡二叉树的,我们就需要旋转两次,这就是双旋转名字的由来。比如下面这种情况。
在这里插入图片描述
很明显,根节点的左子树高度为3,但是右子树高度为1,所以这不是一棵平衡二叉树,但是如果我们对它进行一次右旋转:
在这里插入图片描述
发现竟然得到了它的对称图形。。。。。。
那么仅仅靠一次单旋转就不能达到我们预期的效果了。这时候我们再观察这棵树:
在这里插入图片描述
虽然根节点的左子树本身就是一棵平衡二叉树,但是实际上问题就出在那里,因为左子树是右边比左边多,所以用单旋转的实现步骤去实现的时候,左子树的右子树会原封不动的赋值给新节点作为左子树,这就造成了上面出现的情况,最终的结果来了一个“镜面翻转”。

可是换一种思路,如果我们对这棵左子树先进行一次左旋转,那就会得到这样的结果:
在这里插入图片描述
神奇的事情出现了!它刚刚好就变成了我们之前举例子的那种情况,接下来的操作就不用我多说了,对他进行一次右旋转就可以了。

经过上面的描述,我们可以发现:双旋转的本质就是进行两次单旋转,第一次单旋转看似没用,实际上它把二叉树变成了我们熟悉的、只进行一次单旋转就可以完成操作的形态。然后再进行第二次单旋转,就自然变成了一棵平衡二叉树。

经过上面的叙述,大家应该已经了解了平衡二叉树的实现步骤,那么在进入代码阶段之前,还要说明一点,如果我们想在添加节点的同时就做好判断,同步的把线索二叉树优化为平衡二叉树,那么这个步骤就应该放在添加节点的方法中去,在每次添加节点时进行判断并旋转。这样才能达到创建平衡二叉树的目的。

老规矩,先向大家说明各个类的名称以及作用:

这里的树类我还是用的之前的二叉搜索树的类,只是在其中进行修改以及优化,使其拥有平衡二叉树的特征。
BinarySortTree //树类
Node //节点类
Test //测试类

再简要说明一下其中方法的名称:

MidShow(); //中序遍历方法
Add(Node node); //为树添加节点的方法
Search(int value); //根据值查找节点并返回这个节点
SearchParent(int value); //根据值查找值对应节点的父节点并返回
Delete(int value); //删除值对应的节点
LeftRotate(); //左旋转方法
RightRotate(); //右旋转方法

这里只列出了一些主要方法,其中还有一些次要的、其衔接作用的方法,在下面也标出来了,可以对应代码查看。

下面正式进入代码阶段。

首先是BinarySortTree类

public class BinarySortTree {
    private Node root;

    public Node getRoot() {
        return root;
    }

    public void setRoot(Node root) {
        this.root = root;
    }
    //中序遍历
    public void midShow(){
        if(root==null){
            return;
        }else{
            root.midShow();
        }
    }

    //为二叉排序树添加节点
    public void add(Node node){
        //如果根节点为空,直接添加为根节点
        if(root==null){
            root = node;
        }else{
            //否则调用根节点的add方法
            root.add(node);
        }

    }
    //查询二叉排序树并返回查询到的节点
    public Node search(int value){
        if(value==root.getValue()){
            return root;
        }else{
            return root.search(value);
        }
    }
    //查询并返回节点的父节点
    public Node searchParent(int value){
        //如果所查的节点为根节点,直接返回null
        if(root.getValue()==value){
            return null;
        }
        //定义一个节点用来临时存储节点
        Node temp = root;
        while(temp!=null){
            if((temp.getLeftNode()!=null&&temp.getLeftNode().getValue()==value)||(temp.getRightNode()!=null&&temp.getRightNode().getValue()==value)){
                return temp;
            }else if(value<temp.getValue()){
                temp = temp.getLeftNode();
            }else if(value>temp.getValue()){
                temp = temp.getRightNode();
            }
        }
        return temp;
    }

    //删除方法
    public void delete(int value){
        //先找到要删除的节点,如果找不到,不玩了
        Node target = search(value);
        if(target==null){
            return;
        }
        //找到要删除节点的父节点
        Node parent = searchParent(value);
        //如果要删除的节点为叶子节点,那就直接删除
        if(target.getLeftNode()==null&&target.getRightNode()==null){
            if(parent.getLeftNode()==target){
                parent.setLeftNode(null);
            }else{
                parent.setRightNode(null);
            }
        }else if((target.getLeftNode()!=null&&target.getRightNode()==null)){
            //如果要删除的节点有一个左子节点,那就把这个子节点赋值给要删除节点的父节点
            if(parent.getLeftNode()==target){
                parent.setLeftNode(target.getLeftNode());
            }else{
                parent.setRightNode(target.getLeftNode());
            }
        }else if(target.getRightNode()!=null&&target.getLeftNode()==null){
            //如果被删除节点有一个右子节点,那就把这个子节点赋值给要删除节点的父节点
            if(parent.getLeftNode()==target){
                parent.setLeftNode(target.getRightNode());
            }else{
                parent.setRightNode(target.getRightNode());
            }
        }else{
            //如果要删除的节点有两个子节点,那就找到并删除要删除节点的后继节点,把后继节点的值赋值给要删除节点
            //调用方法,找到右子树中最小的节点的权值
            int i = target.getRightNode().searchMin();
            //删除这个节点
            delete(i);
            //把后继节点的权值赋值给target
            target.setValue(i);
        }

    }


}

其次是Node类:

public class Node {
    private int value;
    private Node leftNode;
    private Node rightNode;

    @Override
    public String toString() {
        return "Node{" +
                "value=" + value +
                '}';
    }

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }

    public Node getLeftNode() {
        return leftNode;
    }

    public void setLeftNode(Node leftNode) {
        this.leftNode = leftNode;
    }

    public Node getRightNode() {
        return rightNode;
    }

    public void setRightNode(Node rightNode) {
        this.rightNode = rightNode;
    }

    public Node(int value) {

        this.value = value;
    }
    //获取当前节点高度的方法
    public int height(){
        return Math.max(leftNode==null?0:leftNode.height(),rightNode==null?0:rightNode.height())+1;
    }
    //获取左子树高度
    public int leftHeight(){
        if (this.leftNode==null){
            return 0;
        }else{
            return leftNode.height();
        }
    }
    //获取右子树高度
    public int rightHeight(){
        if (rightNode==null){
            return 0;
        }else{
            return rightNode.height();
        }
    }


    //add方法
    public void add(Node node) {
        //判断要添加的节点是否为空
        if(node==null){
            return;
        }
        //判断目标节点的值比当前节点大还是小
        if(node.value<this.value){
            if(this.leftNode==null){
                this.leftNode = node;
            }else{
                leftNode.add(node);
            }
        }else{
            if(this.rightNode==null){
                this.rightNode = node;
            }else{
                rightNode.add(node);
            }
        }
        //判断,如果当前节点的左子树高度与右子树高数差的绝对值大于1,则需要进行旋转
        if((this.leftHeight()-this.rightHeight())>1){
           //如果左子树高度减去右子树高度大于1,需要进行右旋转
            //但如果左节点的右子树高度大于左子树高度,需要先进行左旋转,然后再右旋转(双旋转)
            if(leftNode!=null&&leftNode.leftHeight()<leftNode.rightHeight()){
                leftNode.leftRotate();
            }
            rightRotate();
        }else if((this.rightHeight()-this.leftHeight())>1){
            //如果右子树的高度减去左子树高度大于1,需要进行左旋转
             //但如果右节点的左子树高度大于右子树高度,需要先进行右旋转,然后再左旋转(双旋转)
            if(rightNode!=null&&rightNode.rightHeight()<rightNode.leftHeight()){
                rightNode.rightRotate();
            }
            leftRotate();
        }
    }

    //左旋转方法
    private void leftRotate() {
        //先创建一个和当前节点权值一样的节点
        Node newLeft = new Node(this.getValue());
        //再把当前节点的左节点赋值给新节点的左节点
        newLeft.leftNode = this.leftNode;
        //再把当前节点右节点的左节点赋值给新节点的右节点
        newLeft.rightNode = this.rightNode.leftNode;
        //再把当前节点右节点的值赋值给当前节点
        this.value = this.rightNode.value;
        //把当前节点的右节点设置为右节点的右节点
        this.rightNode = rightNode.rightNode;
        //把新节点设置为当前节点的左节点
        this.leftNode = newLeft;
    }

    //右旋转方法
    private void rightRotate() {
        //先创建一个和当前节点权值一样的节点
        Node newRight = new Node(this.value);
        //再把当前节点的右节点赋值给新节点的右节点
        newRight.rightNode = this.rightNode;
        //再把当前节点左节点的右节点赋值给新节点的左节点
        newRight.leftNode = this.leftNode.rightNode;
        //再把当前节点的左节点的值赋值给当前节点
        this.setValue(this.leftNode.getValue());
        //把当前节点的左节点设置为左节点的左节点
        this.leftNode = leftNode.leftNode;
        //把当前节点的右节点设置为新节点
        this.rightNode = newRight;
    }

    //中序遍历
    public void midShow() {
        //处理左边
        if(leftNode!=null){
            leftNode.midShow();
        }
        //处理自己
        System.out.println(value);
        //处理右边
        if(rightNode!=null){
            rightNode.midShow();
        }
    }
    //查询方法
    public Node search(int value) {
        if(value<this.value&&leftNode!=null){
            if(value==leftNode.value){
                return leftNode;
            }else{
                return leftNode.search(value);
            }
        }else if(value>this.value&&rightNode!=null){
            if(value==rightNode.value){
                return rightNode;
            }else{
                return rightNode.search(value);
            }
        }else{
            return null;
        }
    }

    //找到权值最小节点的方法
    public int searchMin(){
        //定义一个临时存储节点的变量
        Node temp = this;
        if(temp.getLeftNode()!=null){
            temp = temp.getLeftNode();
        }
        return temp.getValue();
    }
}

最后是Test类及其打印结果:

public class Test {
    public static void main(String[] args) {
        //创建一棵空树
        BinarySortTree tree = new BinarySortTree();
        //创建一个数组
        int[] arr = new int[]{8,9,5,4,6,7};
        //循环添加节点
        for (int i = 0; i < arr.length; i++) {
            tree.add(new Node(arr[i]));
        }
        tree.midShow();  //4,5,6,7,8
        System.out.println("==========");
        System.out.println(tree.getRoot()); //6
        tree.midShow();  //4,5,6,7,8

    }
}

在最后的测试结果我们可以发现,在添加完所有的节点以后,打印根节点的值并不是8,而是6,说明在添加的同时已经完成了平衡二叉树的转换,添加完成最终得到的树就是一棵平衡二叉树。
其中方法具体的步骤在代码中都有注解,还是不懂的朋友可以debug走一下,如果我写的还有不对的地方或者可以优化的地方还请大佬指出来,我一定尽快改正。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值