平衡二叉树(AVL树)
看一个案例(==说明二叉排序树可能的问题==)
给你一个数列 {1,2,3,4,5,6},要求创建一颗二叉排序树(BST),并分析问题所在。

左边BST存在的问题分析:
- ==左子树全部为空==,从形式上看,更像一个==单链表==。
 - ==插入速度没有影响==。
 - ==查询速度明显降低==(因为==需要依次比较==),不能发挥BST的优势,因为==每次还需要比较左子树==,其==查询速度比单链表还慢==
 - 解决方案——平衡二叉树(AVL)
 
基本介绍
- 平衡二叉树也叫==平衡二叉搜索树==(Self-balancing binary search tree) 又被称为 ==AVL树==,可以==保证查询效率较高==
 - 具有以下特点:它是一颗==空树==或它的==左右两个子树的高度差^只看高度不能看左右节点^的绝对值不超过1并且左右两个子树都是一颗平衡二叉树==。平衡二叉树的常用实现方法有==红黑树==,==AVL^指定是算法而不是树^==,==替罪羊树==,==Treap==,==伸展树等==。
 - 举例说明,看看下面哪些是AVL树,为什么?
 
 图一 图二 图三

分析:
图一:左子树高度:2,右子树高度:1 。差值:1
图二:左子树高度:2,右子树高度:2 。差值:0
图三:左子树高度:3,右子树高度:1 。差值:2

单旋转(左旋转)
如果是左旋转就属于是单旋转
要求:给你一个数列,创建出对应的平衡二叉树,数列 {4,3,6,5,7,8}
左旋转的目的:降低右子树的高度
思路分析(示意图)
下图红线部分是调整流程。

按照规则调整完成之后,形成了下面这样一棵树

完整流程如下图所示:

插入8时,发现左右子树高度相差大于1,则进行左旋转;
- 创建一个新的节点
newNode,值等于当前 根节点 的值(以 4 创建) - 把新节点的 左子树 设置为当前节点的 左子树
 
newNode.left = left;
- 把新节点的 右子树 设置为==当前节点== ==右子树==的==左子树==
 
newNode.right = right.left;
- 把 ==当前节点== 的==值换为== ==右子节点 的值==
 
value = right.value;
- 把 ==当前节点== 的==右子树设置为== ==右子树的右子树==
 
right = right.right;
- 把 ==当前节点== 的==左子树设置为新节点==
 
left = newNode;
注:图左边是调整期,右边是调整后。注意调整期的 6 那个节点,调整之后,没有节点指向它了。也就是说,遍历的时候它是不可达的。那么将会自动的被垃圾回收掉。
树高度计算
前面说过,平衡二叉树是为了解决二叉排序树中可能出现的查找效率问题,那么基本上的代码都可以在之前的二叉排序树上进行优化。那么下面只给出当前主题相关的代码,最后放出一份完整的代码。
树的高度计算,我们需要得到 3 个高度:
- 这棵树的整体高度
 - 左子树的高度
 - 右子树的高度
 
/**
 * @author 窦凯欣
 * @version 1.0
 * @2023/7/1015:49
 * @comment
 */
@SuppressWarnings("all")
public class AVLTreeDemo {
    public static void main(String[] args) {
        AVLTree tree = new AVLTree();
        int[] arr = {4, 3, 6, 5, 7, 8};
        for (int i = 0; i < arr.length; i++)
            tree.add(new Node(arr[i]));
        tree.infixOrder();
        System.out.printf("树高度:%d\n", tree.root.height());//4
        System.out.printf("左树高度:%d\n", tree.root.leftHeight());//1
        System.out.printf("右数高度:%d\n", tree.root.rightHeight());//3
    }
}
class AVLTree {
    public Node root;
    /**
     * @return 获取root节点
     */
    public Node getRoot() {
        return root;
    }
    /**
     * 添加节点
     *
     * @param node 传入要添加的节点
     */
    public void add(Node node) {
        if (root == null)
            root = node;
        else
            root.add(node);
    }
    /**
     * 中序遍历排序二叉树
     */
    public void infixOrder() {
        if (root == null)
            return;
        else
            root.infixOrder();
    }
}
@SuppressWarnings("all")
class Node {
    //节点的值
    public int value;
    //左子节点
    public Node left;
    //右子节点
    public Node right;
    //构造器初始化节点的值
    public Node(int value) {
        this.value = value;
    }
    //返回左子树的高度
    public int leftHeight() {
        if (left == null)
            return 0;
        else
            return left.height();
    }
    //返回右子树的高度
    public int rightHeight() {
        if (right == null)
            return 0;
        else
            return right.height();
    }
    //返回当前节点的高度,以当前节点为跟节点的树的高度
    public int height() {
        /**
         * 这里使用了递归:返回了左右子树,最高的那一个数值。
         * 递归原理: 第一个开始统计的时候,一定是一个叶子节点。
         *          根据这个逻辑:叶子节点的Math.max(0,0) = 0
         *          因为当前节点算一层,所以 +1;
         *          返回到上一层的时候,至少是这样:Math.max(1,0) = 1
         *          然后把自己本身的层+1.以此类推,返回到根节点的时候,就拿到了从包含根节点的树的高度
         */
        //+1;因为本身节点也要算上
        return Math.max(left == null ? 0 : left.height(),
                right == null ? 0 : right.height()) + 1;
    }
    /**
     * 添加节点
     *
     * @param node 传入要添加的节点进行添加
     */
    public void add(Node node) {
        if (node == null)
            return;
        if (node.value < this.value)
            if (this.left == null)
                this.left = node;
            else
                this.left.add(node);
        else if (this.right == null)
            this.right = node;
        else
            this.right.add(node);
    }
    /**
     * 中序遍历排序二叉树
     */
    public void infixOrder() {
        if (this.left != null)
            this.left.infixOrder();
        System.out.println(this);
        if (this.right != null)
            this.right.infixOrder();
    }
    @Override
    public String toString() {
        return "Node{" +
                "value=" + value +
                '}';
    }
}
旋转
说下旋转的时机:也就是什么时候采取做旋转的操作
当然是:当 ==右子树高度 - 左子树高度 > 1== 时,才执行左旋转。
这里就得到一些信息:
每次添加完一个节点后,就需要检查树的高度
满足 ==右子树高度 - 左子树高度 > 1==,那么一定满足下面的条件:
左子树高度为1
右子树高度为3
也就是符合以下这张图:

也就是有如上的信息逻辑,在实现旋转的时候,只要按照思路分析写就可以了,不需要进行边界判断了。
代码实现:
/**
 * @author 窦凯欣
 * @version 1.0
 * @2023/7/1015:49
 * @comment
 */
@SuppressWarnings("all")
public class AVLTreeDemo {
    public static void main(String[] args) {
        AVLTree tree = new AVLTree();
        int[] arr = {4, 3, 6, 5, 7, 8};
        for (int i = 0; i < arr.length; i++)
            tree.add(new Node(arr[i]));
        tree.infixOrder();
        System.out.printf("树高度:%d\n", tree.root.height());//3
        System.out.printf("左树高度:%d\n", tree.root.leftHeight());//2
        System.out.printf("右数高度:%d\n", tree.root.rightHeight());//2
        System.out.println("当前Root节点为: "+tree.getRoot());//6
    }
}
class AVLTree {
    public Node root;
    /**
     * @return 获取root节点
     */
    public Node getRoot() {
        return root;
    }
    /**
     * 添加节点
     *
     * @param node 传入要添加的节点
     */
    public void add(Node node) {
        if (root == null)
            root = node;
        else
            root.add(node);
    }
    /**
     * 中序遍历排序二叉树
     */
    public void infixOrder() {
        if (root == null)
            return;
        else
            root.infixOrder();
    }
}
@SuppressWarnings("all")
class Node {
    //节点的值
    public int value;
    //左子节点
    public Node left;
    //右子节点
    public Node right;
    //构造器初始化节点的值
    public Node(int value) {
        this.value = value;
    }
    //返回左子树的高度
    public int leftHeight() {
        if (left == null)
            return 0;
        else
            return left.height();
    }
    //返回右子树的高度
    public int rightHeight() {
        if (right == null)
            return 0;
        else
            return right.height();
    }
    //返回当前节点的高度,以当前节点为跟节点的树的高度
    public int height() {
        /**
         * 这里使用了递归:返回了左右子树,最高的那一个数值。
         * 递归原理: 第一个开始统计的时候,一定是一个叶子节点。
         *          根据这个逻辑:叶子节点的Math.max(0,0) = 0
         *          因为当前节点算一层,所以 +1;
         *          返回到上一层的时候,至少是这样:Math.max(1,0) = 1
         *          然后把自己本身的层+1.以此类推,返回到根节点的时候,就拿到了从包含根节点的树的高度
         */
        //+1;因为本身节点也要算上
        return Math.max(left == null ? 0 : left.height(),
                right == null ? 0 : right.height()) + 1;
    }
    /**
     * 创建左旋转方法
     */
    public void leftRotate() {
        //1.创建一个新的节点newNode(以4这个值创建),创建一个新的节点,值等于当前根节点的值
        Node newNode = new Node(value);
        //2.把新节点的左子树设置为当前节点的左子树
        newNode.left = this.left;
        //3.把新节点的右子树设置为当前节点的右子树的左子树
        newNode.right = this.right.left;
        //4.把当前节点的值换为右子节点的值
        this.value = this.right.value;
        //5.把当前节点的右子树设置为右子树的右子树
        this.right = this.right.right;
        //6.把当前节点的左子树设置为新节点
        this.left = newNode;
    }
    /**
     * 添加节点
     *
     * @param node 传入要添加的节点进行添加
     */
    public void add(Node node) {
        if (node == null)
            return;
        if (node.value < this.value)
            if (this.left == null)
                this.left = node;
            else
                this.left.add(node);
        else if (this.right == null)
            this.right = node;
        else
            this.right.add(node);
        /**
         * 旋转的时候有以下规则
         * 没添加一个节点后:检查树的高度是否平衡
         *         如果右子树高度 - 左子树高度 > 1 ,则左旋转
         *         也就是说:每次旋转的层只涉及到4层(对照笔记上的图示理解)
         */
        if (rightHeight() - leftHeight() > 1)
            leftRotate();
    }
    /**
     * 中序遍历排序二叉树
     */
    public void infixOrder() {
        if (this.left != null)
            this.left.infixOrder();
        System.out.println(this);
        if (this.right != null)
            this.right.infixOrder();
    }
    @Override
    public String toString() {
        return "Node{" +
                "value=" + value +
                '}';
    }
}
运行结果:
Node{value=3}
Node{value=4}
Node{value=5}
Node{value=6}
Node{value=7}
Node{value=8}
树高度:3
左树高度:2
右数高度:2
当前Root节点为: Node{value=6}
看完代码之后,它的旋转其实就是,将root节点,往下沉到了 root.right 节点下面。

看着上图,是否有想过,貌似根本就可以不用前面讲解的6个步骤来旋转:
- 不用创建新节点
 - 直接将node节点下沉
 - 更改node的right节点为right.left
 - 更改right.left = node
 
其实就已经完成了旋转。但是你仔细想一想,旋转逻辑是写在node里面的,avlTree中的引用如何改变?除非把旋转逻辑移动到avlTree中去,就可以省略掉新节点的步骤来完成。
右旋转
- 左旋转:右 - 左 > 1 ,把右边的往左边旋转一层
 - 右旋转:左 - 右 > 1 ,把左边的往右边旋转一层
 
它们是反着来得思路如下:
- 创建一个新的节点newNode,值等于当前 根节点 的值(以 10 创建)
 - 把新节点的 右子树 设置为当前节点的 右子树
 
newNode.right = this.right;
- 把新节点的 左子树 设置为当前节点的 左子树的右子树
 
newNode.left = this.left.right;
- 把 当前节点 的值换为 左子节点 的值
 
this.value = this.left.value;
- 把 当前节点 的左子树设置为 左子树的左子树
 
this.left = this.left.left;
- 把 当前节点 的右子树设置为新节点
 
this.right = newNode;
上述步骤就是对应下图的描述:查看图示更清楚

代码实现:
/**
 * @author 窦凯欣
 * @version 1.0
 * @2023/7/1015:49
 * @comment
 */
@SuppressWarnings("all")
public class AVLTreeDemo {
    public static void main(String[] args) {
        AVLTree tree = new AVLTree();
//        int[] arr = {4, 3, 6, 5, 7, 8};
        int[] arr = {10, 12, 8, 9, 7, 6};
        for (int i = 0; i < arr.length; i++)
            tree.add(new Node(arr[i]));
        tree.infixOrder();
        System.out.printf("树高度:%d\n", tree.root.height());//3
        System.out.printf("左树高度:%d\n", tree.root.leftHeight());//2
        System.out.printf("右数高度:%d\n", tree.root.rightHeight());//2
        System.out.println("当前Root节点为: "+tree.getRoot());//8
    }
}
class AVLTree {
    public Node root;
    /**
     * @return 获取root节点
     */
    public Node getRoot() {
        return root;
    }
    /**
     * 添加节点
     *
     * @param node 传入要添加的节点
     */
    public void add(Node node) {
        if (root == null)
            root = node;
        else
            root.add(node);
    }
    /**
     * 中序遍历排序二叉树
     */
    public void infixOrder() {
        if (root == null)
            return;
        else
            root.infixOrder();
    }
}
@SuppressWarnings("all")
class Node {
    //节点的值
    public int value;
    //左子节点
    public Node left;
    //右子节点
    public Node right;
    //构造器初始化节点的值
    public Node(int value) {
        this.value = value;
    }
    //返回左子树的高度
    public int leftHeight() {
        if (left == null)
            return 0;
        else
            return left.height();
    }
    //返回右子树的高度
    public int rightHeight() {
        if (right == null)
            return 0;
        else
            return right.height();
    }
    //返回当前节点的高度,以当前节点为跟节点的树的高度
    public int height() {
        /**
         * 这里使用了递归:返回了左右子树,最高的那一个数值。
         * 递归原理: 第一个开始统计的时候,一定是一个叶子节点。
         *          根据这个逻辑:叶子节点的Math.max(0,0) = 0
         *          因为当前节点算一层,所以 +1;
         *          返回到上一层的时候,至少是这样:Math.max(1,0) = 1
         *          然后把自己本身的层+1.以此类推,返回到根节点的时候,就拿到了从包含根节点的树的高度
         */
        //+1;因为本身节点也要算上
        return Math.max(left == null ? 0 : left.height(),
                right == null ? 0 : right.height()) + 1;
    }
    /**
     * 创建右旋转方法
     */
    public void rightRotate() {
        //1.创建一个新的节点newNode(以10这个值创建)
        Node newNode = new Node(value);
        //2.把新节点的右子树设置为当前节点的右子树
        newNode.right = this.right;
        //3.把新节点的左子树设置为当前节点的左子树的右子树
        newNode.left = this.left.right;
        //4.把当前节点的值换为左子树的值
        this.value = this.left.value;
        //5.把当前节点的左子树设置为左子树的左子树
        this.left = this.left.left;
        //6.把当前节点的右子树设置为新节点
        this.right = newNode;
    }
    /**
     * 创建左旋转方法
     */
    public void leftRotate() {
        //1.创建一个新的节点newNode(以4这个值创建),创建一个新的节点,值等于当前根节点的值
        Node newNode = new Node(value);
        //2.把新节点的左子树设置为当前节点的左子树
        newNode.left = this.left;
        //3.把新节点的右子树设置为当前节点的右子树的左子树
        newNode.right = this.right.left;
        //4.把当前节点的值换为右子节点的值
        this.value = this.right.value;
        //5.把当前节点的右子树设置为右子树的右子树
        this.right = this.right.right;
        //6.把当前节点的左子树设置为新节点
        this.left = newNode;
    }
    /**
     * 添加节点
     *
     * @param node 传入要添加的节点进行添加
     */
    public void add(Node node) {
        if (node == null)
            return;
        if (node.value < this.value)
            if (this.left == null)
                this.left = node;
            else
                this.left.add(node);
        else if (this.right == null)
            this.right = node;
        else
            this.right.add(node);
        /**
         * 旋转的时候有以下规则
         * 没添加一个节点后:检查树的高度是否平衡
         *         如果右子树高度 - 左子树高度 > 1 ,则左旋转
         *         也就是说:每次旋转的层只涉及到4层(对照笔记上的图示理解)
         */
        if (rightHeight() - leftHeight() > 1)
            leftRotate();
        if (leftHeight() - rightHeight() > 1)
            rightRotate();
    }
    /**
     * 中序遍历排序二叉树
     */
    public void infixOrder() {
        if (this.left != null)
            this.left.infixOrder();
        System.out.println(this);
        if (this.right != null)
            this.right.infixOrder();
    }
    @Override
    public String toString() {
        return "Node{" +
                "value=" + value +
                '}';
    }
}
结果:
Node{value=6}
Node{value=7}
Node{value=8}
Node{value=9}
Node{value=10}
Node{value=12}
树高度:3
左树高度:2
右数高度:2
当前Root节点为: Node{value=8}
==单旋转==的注意事项
有些情况下,单旋转不能完成平衡二叉树的转换如比数列 {10,11,7,6,8,9} 或者 {2,1,6,5,7,3}; 运行原来的代码可以看到,并没有转成AVL树。


问题分析:
左侧这个树满足 leftHeight - rightHeight > 1,也就是满足右旋转,旋转之后,树结构变化了。但是还是一个非平衡二叉树
它的主要原因是:root 左子树的 左子树高度 小于 右子树的高度。即:节点 7 的左子树高度小于右子树的高度。
解决办法——双旋转
- 先将 7 这个节点为 root 节点,进行左旋转
 - 再将原始的root节点进行右旋转
 

其实可以参考下面两个单旋转的例子,它有这样一个特点:
- 右旋转:
- root 的 left 左子树高度 大于 右子树高度
 - 右旋转的时候,会将 left.right 旋转到 right.left 节点上个
 
 - 左旋转:
- root 的 right 右子树高度 大于 左子树高度
 - 左旋转的时候,会将 right.left 旋转到 left.right 上
 
 
如果不满足这个要求,在第二个操作的时候,就会导致 2 层的高度被旋转到 1 层的节点下面,导致不平衡了。
解决代码如下:
代码实现:
/**
 * @author 窦凯欣
 * @version 1.0
 * @2023/7/1015:49
 * @comment
 */
@SuppressWarnings("all")
public class AVLTreeDemo {
    public static void main(String[] args) {
        AVLTree tree = new AVLTree();
//        int[] arr = {4, 3, 6, 5, 7, 8};
//        int[] arr = {10, 12, 8, 9, 7, 6};
       //下面两个数组是导致单旋转失败的例子
//        int[] arr = {10, 11, 7, 6, 8, 9};
        int[] arr = {2, 1, 6, 5, 7, 3};
        for (int i = 0; i < arr.length; i++)
            tree.add(new Node(arr[i]));
        tree.infixOrder();
        System.out.printf("树高度:%d\n", tree.root.height());//3
        System.out.printf("左树高度:%d\n", tree.root.leftHeight());//2
        System.out.printf("右数高度:%d\n", tree.root.rightHeight());//2
        System.out.println("当前Root节点为: " + tree.getRoot());//5
    }
}
class AVLTree {
    public Node root;
    /**
     * @return 获取root节点
     */
    public Node getRoot() {
        return root;
    }
    /**
     * 添加节点
     *
     * @param node 传入要添加的节点
     */
    public void add(Node node) {
        if (root == null)
            root = node;
        else
            root.add(node);
    }
    /**
     * 中序遍历排序二叉树
     */
    public void infixOrder() {
        if (root == null)
            return;
        else
            root.infixOrder();
    }
}
@SuppressWarnings("all")
class Node {
    //节点的值
    public int value;
    //左子节点
    public Node left;
    //右子节点
    public Node right;
    //构造器初始化节点的值
    public Node(int value) {
        this.value = value;
    }
    //返回左子树的高度
    public int leftHeight() {
        if (left == null)
            return 0;
        else
            return left.height();
    }
    //返回右子树的高度
    public int rightHeight() {
        if (right == null)
            return 0;
        else
            return right.height();
    }
    //返回当前节点的高度,以当前节点为跟节点的树的高度
    public int height() {
        /**
         * 这里使用了递归:返回了左右子树,最高的那一个数值。
         * 递归原理: 第一个开始统计的时候,一定是一个叶子节点。
         *          根据这个逻辑:叶子节点的Math.max(0,0) = 0
         *          因为当前节点算一层,所以 +1;
         *          返回到上一层的时候,至少是这样:Math.max(1,0) = 1
         *          然后把自己本身的层+1.以此类推,返回到根节点的时候,就拿到了从包含根节点的树的高度
         */
        //+1;因为本身节点也要算上
        return Math.max(left == null ? 0 : left.height(),
                right == null ? 0 : right.height()) + 1;
    }
    /**
     * 创建有旋转方法
     */
    public void rightRotate() {
        //1.创建一个新的节点newNode(以10这个值创建)
        Node newNode = new Node(value);
        //2.把新节点的右子树设置为当前节点的右子树
        newNode.right = this.right;
        //3.把新节点的左子树设置为当前节点的左子树的右子树
        newNode.left = this.left.right;
        //4.把当前节点的值换为左子树的值
        this.value = this.left.value;
        //5.把当前节点的左子树设置为左子树的左子树
        this.left = this.left.left;
        //6.把当前节点的右子树设置为新节点
        this.right = newNode;
    }
    /**
     * 创建左旋转方法
     */
    public void leftRotate() {
        //1.创建一个新的节点newNode(以4这个值创建),创建一个新的节点,值等于当前根节点的值
        Node newNode = new Node(value);
        //2.把新节点的左子树设置为当前节点的左子树
        newNode.left = this.left;
        //3.把新节点的右子树设置为当前节点的右子树的左子树
        newNode.right = this.right.left;
        //4.把当前节点的值换为右子节点的值
        this.value = this.right.value;
        //5.把当前节点的右子树设置为右子树的右子树
        this.right = this.right.right;
        //6.把当前节点的左子树设置为新节点
        this.left = newNode;
    }
    /**
     * 添加节点
     *
     * @param node 传入要添加的节点进行添加
     */
    public void add(Node node) {
        if (node == null)
            return;
        if (node.value < this.value)
            if (this.left == null)
                this.left = node;
            else
                this.left.add(node);
        else if (this.right == null)
            this.right = node;
        else
            this.right.add(node);
        /**
         * 旋转的时候有以下规则
         * 没添加一个节点后:检查树的高度是否平衡
         *         如果右子树高度 - 左子树高度 > 1 ,则左旋转
         *         也就是说:每次旋转的层只涉及到4层(对照笔记上的图示理解)
         */
        if (rightHeight() - leftHeight() > 1) {
            //如果它的右子树的左子树的高度大于它的右子树的右子树的高度
            if (right != null && right.leftHeight() > right.rightHeight()) {
                //先对右子节点进行右旋转
                right.rightRotate();
                //然后再对当前节点进行左旋转
                leftRotate();
            } else
                //直接进行左旋转
                leftRotate();
            //旋转好后阻止程序的向后执行否则就会产生错误的操作
            return;
        }
        if (leftHeight() - rightHeight() > 1)
            //如果它的左子树的右子树高度大于它的左子树的高度
            if (left != null && left.rightHeight() > left.leftHeight()) {
                //先对当前节点的左节点(左子树) -> 左旋转
                left.leftRotate();
                //再对当前节点进行右旋转
                rightRotate();
            } else
                //直接进行右旋转
                rightRotate();
    }
    /**
     * 中序遍历排序二叉树
     */
    public void infixOrder() {
        if (this.left != null)
            this.left.infixOrder();
        System.out.println(this);
        if (this.right != null)
            this.right.infixOrder();
    }
    @Override
    public String toString() {
        return "Node{" +
                "value=" + value +
                '}';
    }
}
结果:
Node{value=1}
Node{value=2}
Node{value=3}
Node{value=5}
Node{value=6}
Node{value=7}
树高度:3
左树高度:2
右数高度:2
当前Root节点为: Node{value=5}
文档信息
- 本文作者:Dkx
 - 本文链接:https://pigpigletsgo.github.io/dou_note.github.io/2023/12/29/pinghengerchashu/
 - 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)