【树】平衡二叉搜索树的介绍与构建

文章详细介绍了平衡二叉搜索树(AVL树)的性质,包括其平衡因子和最小节点数的计算。通过四种旋转(LL,RR,LR,RL)说明如何在插入和删除节点后恢复平衡。此外,还涉及了节点的插入和删除操作以及中序遍历。
摘要由CSDN通过智能技术生成

一、平衡二叉搜索树的概述

平衡二叉树总称应该为平衡二叉查找树,也可称AVL树(满足平衡条件的二叉查找树),也就是说平衡二叉查找树的前提是二叉搜索树(二叉搜索树汇总)。

二叉搜索树进行插入、删除、查找操作时,时间复杂度是 O(logn),但当这棵二叉搜索树为斜树时,那么时间复杂度会引来最坏的结果 O(n)。

在这里插入图片描述
当尽可能的将树俩边保持平衡时,这时复杂度会引来最好的结果。

1. 平衡二叉树的性质

平衡二叉树(Balanced Binary Tree)具有以下性质:

  1. 要么是空树要么左右两个子树的高度差的绝对值不超过1;
  2. 左右子树也都是一棵平衡二叉树;
  3. 每个节点都有一个平衡因子(Balanced Factor),任意一个节点的平衡因子的值为 -1、0、1,计算公式是 左子树高度 - 右子树高度

2. 平衡二叉树的最小节点数(公式及其原理)

设 Nh 是高度为 h 的平衡二叉树的最小结点数

==》Nh = Nh-1 + Nh-2 + 1

学的时候会发现总把它和斐波那契数列放在一起去进行理解。斐波那契数列又可以用分治和递归的思想去解决,而最小节点数用分治思想是不好理解的,递归反而容易理解些,可以理解为由上至下。

a. 树高度和深度的区别

理解最小节点数怎么来的之前得先理解树高度和深度的区别:

定义:

高度:结点到叶子节点最长简单路径的条数;
深度:根节点到该节点的最长简单路径边的条数。
注意:这里的条数规定是 根结点的深度和 叶子结点的高度 是为 0。

区别:

深度是从顶到该节点,高度是从低到该节点。

b. 原理

根据平衡二叉树的定义可知:左右子结点高度差的绝对值<=1,那么对于高度为 h 的平衡二叉树无非就三种情况:

  1. 左结点 h-1 的高度,右结点 h-2 的高度
  2. 右结点 h-1 的高度,左结点 h-2 的高度
  3. 左右结点都为 h-1 的高度。

对于左右子节点来说无非就是新的根节点、新的平衡二叉树,想得到最小节点数,那肯定左右子结点新构成的平衡二叉树的结点也应该满足最小的节点数,且情况为 一个子树为 h-1 的高度, 一个子树为 h-2 的高度。

在这里插入图片描述

注意:这里的高度是以平衡二叉树的根节点为目标结点,Ans(Nh)表示最后结果也就是最小节点数。

(递归 + 公式 更容易理解些,看得懂就行)

二、平衡二叉树的创建和调整

1. 节点

建立一个AVLTree 类,表示平衡二叉搜索树类,由于该二叉树是由节点组成的,那在该类内部有个节点内部类 AVLTreeNode类,该二叉树也是二叉搜索树,由于需要对节点值进行比较,所以也运用了泛型节点对象只用实现了Comparable接口的类对象

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

    private AVLTreeNode<T> mRoot;
    
    class AVLTreeNode<T extends Comparable<T>>{
        public int height;
        public AVLTreeNode<T> left;
        public AVLTreeNode<T> right;
        T val;

        public AVLTreeNode() {}

        public AVLTreeNode(AVLTreeNode<T> left, AVLTreeNode<T> right, T val) {
            this.left = left;
            this.right = right;
            this.val = val;
        }
    }
}

2. 旋转

四种姿态

当对 AVL 树进行插入、删除操作时,可能会使得 AVL 树失去平衡。这种失去平衡概括为四种姿态:LL(左左)LR(左右)RR(右右)RL(右左)

在这里插入图片描述

  • LL:Left Left,也称为“左左”。插入或删除一个节点后,根节点的左子树的左子树还有非空子节点,导致“根的左子树的高度”比”根的右子树的高度“大 2,平衡因子 > 1,导致AVL 树失去平衡。

  • LR:Left Right,也称为”左右“。插入或删除一个节点后,根节点的左子树的右子树还有非空子节点,导致”根的左子树的高度“比”根的右子树的高度“大2,平衡因子 > 1,导致AVL 树失去了平衡。

  • RL:Right Left,也称为”右左“。插入或删除一个节点后,根节点的右子树的左子树还有非空子节点,导致”根的右子树的高度“比”根的左子树的高度“大2,平衡因子 < -1,导致AVL 树失去了平衡。

  • RR:Right Right,也称为”右右“。插入或删除一个节点后,根节点的右子树的右子树还有非空子节点,导致”根的右子树高度“比”根的左子树高度“大2,平衡因子 < -1,导致AVL 树失去了平衡。

(上图分别对四种姿态进行了图形展示)

当 AVL 失去平衡之后,可以通过旋转使其恢复平衡。

a. LL旋转

在这里插入图片描述

左边是失去平衡的二叉树,右边是恢复后的 AVL 树。k2 是最小不平衡子树根节点,旋转过程中他扮演着主角。

LL 使得AVL 树失去平衡的 AVL 树,是向右旋转,向右旋转 k1 会去替代k2 的位置,k2会成为k1的右孩子,k1 的右孩子会成为新k2 的左孩子。这样做不仅可以 平衡二叉树的性质再次得到满足。

    /**
     * LL 旋转
     * @param k2 要旋转的最小不平衡子树的根节点
     * @return 返回替代k2的节点,也可以说返回该最小不平衡子树的根节点。
     */
    private AVLTreeNode<T> LLRotation(AVLTreeNode<T> k2){
        AVLTreeNode<T> k1 = k2.left;// 标记节点,用来暂时指向k2 的左孩子
        // 要开始旋转了哦
        k2.left = k1.right;
        k1.right = k2;

        k2.height = Math.max(height(k2.left),height(k2.right)) + 1;
        k1.height = Math.max(height(k1.left),height(k1.right)) + 1;
        return k1;
    }

注意:该方法重新返回(最小不平衡树)根节点是有用的,插入,以及LR和RL旋转都是有用的。

b. RR旋转

在这里插入图片描述
注意:和上面的LL是相反的。代码里面的k2指的是图里的k1,k1指的是图里的k2。

	/**
     * @param k2
     * @return 返回最小不平衡子树的根节点
     */
	private AVLTreeNode<T> RRRotation(AVLTreeNode<T> k2) {
        AVLTreeNode<T> k1 = k2.right;
        k2.right = k1.left;
        k1.left = k2;

        k2.height = Math.max(height(k2.left),height(k2.right)) + 1;
        k1.height = Math.max(height(k1.left),height(k1.right)) + 1;
        return k1;
    }

c. LR旋转

  • 从右往左看,先RR转,在LL转。

在这里插入图片描述

  • 第一次旋转是围绕"k1"进行的"RR旋转",第二次是围绕"k3"进行的"LL旋转"。

代码就很简单啦:

	/**
     * @param root
     * @return 返回最小不平衡子树的根节点
     */
	 private AVLTreeNode<T> LRRotation(AVLTreeNode<T> root) {
        root.left = RRRotation(root.left);
        return LLRotation(root);
    }

d. RL旋转

  • 先 LL 转,再 RR 转咯。

在这里插入图片描述

  • 第一次旋转是围绕"k3"进行的"LL旋转",第二次是围绕"k1"进行的"RR旋转"。
	/**
     * @param root
     * @return 返回最小不平衡子树的根节点
     */
    private AVLTreeNode<T> RLRotation(AVLTreeNode<T> root) {
        root.right = LLRotation(root.right);
        return RRRotation(root);
    }

2. 节点的插入

本应该插入应该在前的,因为它可以更好的测试数据。
但是插入又离不开旋转,所以还是得先解释旋转。

结点的插入呢?和二叉搜索树差不多。多了个AVL树是否还平衡的判断和结点高度的更新。

直接上代码,更清楚:

	// 供用户使用的方法
	public void insert(T key){
        if(key!=null){
            mRoot = insert(mRoot,key);
        }
    }
	// 插入细节
    private AVLTreeNode<T> insert(AVLTreeNode<T> root,T key){
        if(root==null){
            root = new AVLTreeNode<T>(null,null,key);
        }else{
            int cmp = key.compareTo(root.val);
            if(cmp<0){
                root.left = insert(root.left, key);
                // 插入节点如果AVL 树失去平衡,则进行相应的调节
                if(height(root.left) - height(root.right) == 2){
                    // 这节点是插入在左孩子的,要么就是LL 要么就是LR
                    // LL
                    if(key.compareTo(root.left.val)<0){
                        root = LLRotation(root);
                    }else{
                        root = LRRotation(root);
                    }
                }
            }else if(cmp>0){
                root.right = insert(root.right,key);
                // 和上面一样,判断是否失去平衡然后做调节
                if(height(root.right) - height(root.left) == 2){
                    if(key.compareTo(root.right.val)>0) {
                        root = RRRotation(root);
                    }else{
                        root  = RLRotation(root);
                    }
                }
            }else{
                // 这里咱自定义一个异常类 ValOfAVLNodeEqual
                try {
                    throw new ValOfAVLNodeEqual("AVL 树中不支持节点的数据相等");
                } catch (ValOfAVLNodeEqual e) {
                    e.getMessage();
                }
            }
        }
        // 递归回溯的途中需要更新节点的高度
        root.height = Math.max(height(root.left),height(root.right)) + 1;
        // 完美返回
        return root;
    }

3. 节点的删除

删除和BST 一样分三种情况:

  1. 左右孩子都存在;
  2. 单个孩子存在;
  3. 无孩子。

什么查找代码,寻找左子树最大值节点。。。很多重复代码,不想写了。

可以看看这个的删除操作:

平衡二叉树删除操作

4. 中序遍历

有了上面的噔噔噔一些操作后,来个遍历收尾。

返回一个LIst 集合,可以操作可以输出。完美~

private List<T> valList = new ArrayList<>();
    
    /**
     * 咱来个中序遍历,有序
     */
    public List<T> orderTraversal(){
        valList.clear();
        orderTraversal(mRoot);
        return valList;
    }

    public void orderTraversal(AVLTreeNode<T> node){
        if(node!=null){
            orderTraversal(node.left);
            valList.add(node.val);
            orderTraversal(node.right);
        }
    }
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

假正经的小柴

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

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

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

打赏作者

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

抵扣说明:

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

余额充值