JAVA实现平衡二叉树(AVL)

前言

  • 本文主要探讨平衡二叉树的实现过程,对于原理还请自行翻阅其它资料进行学习

1.平衡二叉树简介

1.1什么是平衡二叉树

了解平衡二叉树之前我们首先需要知道什么是树结构

  • . 树结构

树是一种数据结构,它是由n(n>=1)个有限节点组成一个具有层次关系的集合。


树的定义:
把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点:

  1. 树由若干个节点组成
  2. 如果一颗树不为空,那么至少拥有一个根节点,且根节点没有父节点
  3. 每个节点可以拥有若干个子节点
  4. 除了根节点以外,每个子节点可以分为多个不相交的子树
  • 二叉树

二叉树(Binary tree)是树形结构的一个重要类型。许多实际问题抽象出来的数据结构往往是二叉树形式,即使是一般的树也能简单地转换为二叉树,而且二叉树的存储结构及其算法都较为简单,因此二叉树显得特别重要。二叉树特点是每个结点最多只能有两棵子树,且有左右之分 [1] 。
二叉树是n个有限元素的集合,该集合或者为空、或者由一个称为根(root)的元素及两个不相交的、被分别称为左子树和右子树的二叉树组成,是有序树。当集合为空时,称该二叉树为空二叉树。在二叉树中,一个元素也称作一个结点 [1] 。

二叉树定义:

  1. 二叉树由若干个节点组成
  2. 如果一颗二叉树不为空,那么至少拥有一个根节点,且根节点没有父节点
  3. 每个节点可以拥有最多两个子节点,分别为左子节点和右子节点
  4. 若任意节点的左子树不空,则左子树上所有的节点值均小于该节点的值
  5. 若任意节点的右子树不空,则右子树上所有节点的值均大于该节点的值
  6. 每个子节点也必须符合2-5的规范

二叉树有以下5中基本形态:


二叉树的缺点:

虽然说二叉树能提高查找的效率 O(logn),但当插入的数据是一个有序的数列时,二叉树看起来像一个链表一样,搜索效率降低为O(n)

比如我插入的数据为{1,2,3,4,5,6}



为了避免这种情况存在,在 1962 年,一个姓 AV 的大佬(G. M. Adelson-Velsky) 和一个姓 L 的大佬( Evgenii Landis)提出「平衡二叉树」(AVL) 。
插入 {1,2,3,4,5,6} 这种数据结果演变为下图:


  • 平衡二叉树

在计算机科学中,AVL树是最早被发明的自平衡二叉查找树。在AVL树中,任一节点对应的两棵子树的最大高度差为1,因此它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下的时间复杂度都是O(logn)。增加和删除元素的操作则可能需要借由一次或多次树旋转,以实现树的重新平衡。AVL 树得名于它的发明者 G. M. Adelson-Velsky 和 Evgenii Landis,他们在1962年的论文《An algorithm for the organization of information》中公开了这一数据结构。

平衡二叉树定义:

  1. 平衡二叉树由若干个节点组成
  2. 如果一颗二叉树不为空,那么至少拥有一个根节点,且根节点没有父节点
  3. 每个节点可以拥有最多两个子节点,分别为左子节点和右子节点
  4. 若任意节点的左子树不空,则左子树上所有的节点值均小于该节点的值
  5. 若任意节点的右子树不空,则右子树上所有节点的值均大于该节点的值
  6. 没有键值相等的节点
  7. 左树和右树的高度差的绝对值不能超过1
  8. 每个子节点也必须符合3-7的规范

以下是一颗平衡二叉树(图1)

  • 我们以根节点4为参照系,可以看到这颗二叉树不为空,有且仅有1个根节点,满足定义2
  • 每个节点最多有两个子节点,满足定义3
  • 根节点4的左子树2不为空,2的子节点为{1,3},则根节点4的左子树节点集合为[2,1,3],该集合的任意值小于根节点4的值,满足定义4
  • 根节点4的右子树7不为空,7的子节点为{6,9},6的子节点为{5,null},9的子节点为{8,10},则根节点4的右子树节点集合为[7,6,9,5,8,10],该集合的任意值大于根节点4的值,满足定义5
  • 该树没有值相等的节点,满足定义6
  • 4的左子树深度为2,右子树深度为3,则左右高度差=左子树深度-右子树深度=2-3=-1,可以看到根节点头上有一个红色的-1,表示左右子树高度差,-1的绝对值是1,满足定义7
  • 根节点4的每个子节点也符合以上规范,满足定义8

1.2平衡二叉树的作用

二叉树支持动态的插入和查找,保证操作在O(height)时间,这就是完成了哈希表不便完成的工作,动态性。但是二叉树有可能出现worst-case,如果输入序列已经排序,则时间复杂度为O(N)
平衡二叉树/红黑树就是为了将查找的时间复杂度保证在O(logN)范围内。
所以如果输入结合确定,所需要的就是查询,则可以考虑使用哈希表,如果输入集合不确定,则考虑使用平衡二叉树/红黑树,保证达到最大效率
平衡二叉树主要优点集中在快速查找。

2.关键词概述

  • 根节点(root_node)

    父节点为空的节点,一颗不为空的平衡二叉树有且只有一个根节点

  • 父节点(parent_node)

    除了根节点,每个不为空的节点都有且只有一个父节点

  • 左子树(left_tree)

    任意节点的左分支节点树(表示的是左分支下的节点集合)

  • 右子树(right_tree)

    任意节点的右分支节点树(表示的是右分支下的节点集合)

  • 左子节点(left_child)

    任意节点左子树中的第一个节点(表示的是单个的节点)

  • 右子节点(right_child)

    任意节点右子树中的第一个节点(表示的是单个的节点)

  • 叶子节点(leaf_node)

    除根节点以外,若任意节点的左右子节点都为空,则称这个节点为叶子节点,表示没有后继分支

  • 平衡因子(BalanceFactor)

    表示任意节点的左右子树高度差
    计算公式为:bf = 左子树高度 - 右子树高度
    如果bf的绝对值>1代表此树失衡

  • 最小不平衡子树(MinUnBalanceTree)

    最小的失衡节点

3.代码实现思路

3.1节点类(TreeNode)

根据上文平衡二叉树的定义可知,节点类需要拥有下列字段

  • data (节点值)
  • bf (平衡因子)
  • parent_node (父节点)
  • left_child (左子节点)
  • right_child (右子节点)
  • count (节点数据被重复插入的次数)

新建一个节点类(TreeNode)

    /**
     * 存放数据的节点类
     */
    static class TreeNode {
        private int bf = 0;//BalanceFactor(平衡因子)
        private int data;//该节点存放的数据
        private TreeNode parent_node;//该节点的父节点
        private TreeNode left_child, right_child;//该节点的左右子节点
        private int count = 0;//记录该条数据被重复插入的次数

        public TreeNode(int data) {
            this.data = data;

        }
        
         /**
         * 此处注意不能直接打印左右子节点和父节点的全部节点信息
         * 因为节点之间有互相引用关系,如果全部打印会导致堆栈溢出
         * 故此处值打印父节点和左右子节点的值
         *
         * @return
         */
        @Override
        public String toString() {
            String p_data = null, l_data = null, r_data = null;
            if (parent_node != null) {
                p_data = String.valueOf(parent_node.data);
            }
            if (left_child != null) {
                l_data = String.valueOf(left_child.data);
            }
            if (right_child != null) {
                r_data = String.valueOf(right_child.data);
            }
            return "TreeNode{" +
                    " data=" + data +
                    ", bf=" + bf +
                    ", count=" + count +
                    ", parent_node_data=" + p_data +
                    ", left_child_data=" + l_data +
                    ", right_child_data=" + r_data +
                    '}';
        }
         /**
         * 因为平衡二叉树的节点值唯一,所以此处只需要比较值相等
         * @param o
         * @return
         */
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            TreeNode node = (TreeNode) o;
            return data == node.data;
        }

        @Override
        public int hashCode() {
            return Objects.hash(bf, data, parent_node, left_child, right_child, count);
        }

    }
    

3.2 树类(AVLTree)

树类需要满足的条件有:

  • 任意节点左子树上的节点值小于该节点的值,右子树上的节点值大于该节点的值,该节点的子节点也适用这个规律
  • 每个节点左右子树的深度差绝对值不大于1

新建一个树类(AVLTree)

    /**
     * 平衡二叉树
     * 条件:
     * 1.任意节点左子树上的节点值小于该节点的值,右子树上的节点值大于该节点的值,该节点的子节点也适用这个规律
     * 2.每个节点左右子树的深度差绝对值不大于1
     */
    static class AVLTree {
        
    }
    

树类中需要一个队列来存放我们的节点值

    //用于存放节点的队列
    private Queue tree = new LinkedList();

现在我们来插入第一个节点 ,写一个插入节点的方法

    /**
     * 插入节点
     *
     * @param data 节点值
     */
    public void insertNode(int data) {
            TestAVL_Tree.TreeNode new_node = new TestAVL_Tree.TreeNode(data);
            tree.add(new_node);
        }
        

写一个测试类,插入数据7来查看调用结果

    AVLTree avlTree = new AVLTree();
    avlTree.insertNode(7);
    System.out.println("最终结果:" + avlTree.tree.toString());

最终结果:
[TreeNode{ data=7, bf=0, count=0, parent_node_data=null, left_child_data=null, right_child_data=null}]

此时的AVL树状态为:


  • 由于根节点是必定没有父节点的,此处就不在标出
  • 因为左右子树都是NULL,此时平衡因子为0,

接着来插入第二个节点6

    avlTree.insertNode(6);

因为6的值比根节点7小,所以放在根节点的左子节点中,如图

  • 根节点7的的左树中新增节点6
  • 因为左树中插入了新节点,需要重新计算平衡因子。此时根节点7的左树深度为1,右树深度为0,bf=1,AVL树保持平衡状态

如果根节点7在插入之前已经有子节点呢?
此时会有4种可能

  • 插入的新节点小于根节点的左子节点,此时插入的方式称为左左(left_left),即在节点左子树下的左子节点下插入

    插入流程:
    1.由上文AVL树的定义可知,任意节点左子树上所有的节点值均小于该节点的值
    2.根节点的值为7,新节点的值为4;由于新节点的值 < 根节点的值,传递给根节点的左子节点,继续进行比较
    3.左子节点的值为5,新节点的值为4;由于新节点的值 < 左子节点的值,且左子节点没有后继的子节点可以继续进行传递,则将新节点插入到左子节点的左子树下。
  • 插入的新节点大于根节点的左子节点,此时插入的方式称为左右(left_right),即在节点左子树下的右子节点下插入

    插入流程:*
    1.由上文AVL树的定义可知,任意节点左子树上所有的节点值均小于该节点的值,右子树上所有节点的值均大于该节点的值
    2.根节点的值为7,新节点的值为6;由于新节点的值 < 根节点的值,传递给根节点的左子节点,继续进行比较
    3.左子节点的值为5,新节点的值为6;由于新节点的值 > 左子节点的值,且左子节点没有后继的子节点可以继续进行传递,则将新节点插入到左子节点的右子树下。

  • 插入的新节点大于根节点的右子节点此时插入的方式称为右右(right_right),即在节点右子树下的右子节点下插入

    插入流程:
    1.由上文AVL树的定义可知,任意节点右子树上所有节点的值均大于该节点的值
    2.根节点的值为7,新节点的值为15;由于新节点的值 > 根节点的值,传递给根节点的右子节点,继续进行比较
    3.右子节点的值为10,新节点的值为15;由于新节点的值 > 右子节点的值,且右子节点没有后继的子节点可以继续进行传递,则将新节点插入到右子节点的右子树下。

  • 插入的新节点小于根节点的右子节点此时插入的方式称为右左(right_left),即在节点右子树下的左子节点下插入


    插入流程:
    1.由上文AVL树的定义可知,任意节点左子树上所有的节点值均小于该节点的值,右子树上所有节点的值均大于该节点的值
    2.根节点的值为7,新节点的值为8;由于新节点的值 > 根节点的值,传递给根节点的右子节点,继续进行比较
    3.右子节点的值为10,新节点的值为8;由于
    新节点的值 < 右子节点的值*,且右子节点没有后继的子节点可以继续进行传递,则将新节点插入到右子节点的左子树下。

       /**
         * 计算新插入节点的位置
         *
         * @param old_node 旧节点
         * @param new_node 新节点
         */
        private void indexOf(TreeNode old_node, TreeNode new_node) {
            if (new_node.data == old_node.data) {//新节点的值等于旧节点的值
                old_node.count++;//不计算新节点的下表,直接在旧节点的count+1;
            } else if (new_node.data < old_node.data) {//新节点的值小于旧节点的值
                if (old_node.left_child == null) {//如果左子树为空,就放入旧节点的左子树
                    old_node.left_child = new_node;
                    new_node.parent_node = old_node;//把当前旧节点设置为新节点的父节点
                } else {
                    indexOf(old_node.left_child, new_node);//如果左子树不为空,则递归计算位置
                }
            } else if (new_node.data > old_node.data) {//新节点的值大于等于旧节点的值
                if (old_node.right_child == null) {//如果右子树为空,就放入旧节点的右子树
                    old_node.right_child = new_node;
                    new_node.parent_node = old_node;//把当前旧节点设置为新节点的父节点
                } else {
                    indexOf(old_node.right_child, new_node);//如果右子树不为空,则递归计算位置
                }
            }
        }
        

可以看到以上4种插入情况都会导致根节点bf的绝对值 >1,此时二叉树失衡,需要通过旋转来恢复平衡状态
针对4种插入情况(左左、左右、右右、右左),有4种不同的旋转策略,两种旋转的方法,分别为左旋右旋

==左旋和右旋不同于逆时针旋和顺时针旋转,下面两个动图便于理解:==
左旋

左旋就是将节点的右支往左拉,右子节点变成父节点,并把晋升之后多余的左子节点出让给降级节点的右子节点


右旋

右旋就是反过来,将节点的左支往右拉,左子节点变成了父节点,并把晋升之后多余的右子节点出让给降级节点的左子节点



即左旋就是往左变换,右旋就是往右变换。不管是左旋还是右旋,旋转的目的都是将节点多的一支出让节点给另一个节点少的一支

左左

  • 针对LL型(left_left)的旋转策略为:右旋

此情况下最小不平衡子树为根节点7,由下图可知根节点的左子树深度为2,右子树深度为0
则需要对节点5执行右旋


  • 旧根节点(节点 7)为新根节点(节点 5)的右子树
  • 新根节点(节点 5)的 右子树(如果存在)为旧根节点的左子树

旋转之前首先要找到最小不平横子树

寻找过程为:
1.从新插入的节点开始,向上寻找父节点,判断父节点的bf绝对值是否大于1
2.如果 > 1,则此父节点就是最小不平衡子树
3.如果 < 或 = 1,则继续往上追朔父节点的父节点,直到找到bf>1的祖父节点

       /**
         * 计算最小不平衡子树
         * 可能存在的情况:
         * 1.该节点是根节点(没有父节点)
         * 2.该节点的父节点平衡因子>1,代表此节点是最小不平衡子树
         * 3.该节点的父节点平衡因子<1,递归继续向上查找
         *
         * @param node 新插入的节点
         * @return 最小不平衡子树
         */
        private TreeNode countMinUnBalanceNode(TreeNode node) {
            TreeNode from_node = node.parent_node;//获得新插入节点的父节点
            if (from_node != null) {//此节点不是根节点
                int bf_abs = Math.abs(from_node.bf);
                if (bf_abs > 1) {//平衡因子绝对值>1代表此节点失衡,并且是最小不平衡子数
                    return from_node;
                } else if (bf_abs <= 1) {
                    return countMinUnBalanceNode(from_node);//否则递归调用向上查找
                }
            } else {//此节点是根节点 只需要判断根节点的不平衡因子,不需要往上递归
                int bf_abs = Math.abs(node.bf);
                if (bf_abs > 1) {//平衡因子绝对值>1代表此节点失衡,并且是最小不平衡子数
                    return node;
                }
            }
            return null;
        }
        

节点右旋的代码

       /**
         * 节点右旋
         *
         * @param node 需要右旋的节点
         */
        private void rightRotate(TreeNode node) {
            TreeNode left_child = node.left_child;//当前节点的左子树
            TreeNode parent_node = node.parent_node;//当前节点的父节点
            if (parent_node != null) {//如果当前节点不是根节点
                //判断当前节点是父节点的左子树还是右子树
                if (node.equals(parent_node.left_child)) {
                    node.parent_node.left_child = left_child;//用左子树替换掉当前节点在父节点的位置
                } else if (node.equals(parent_node.right_child)) {
                    node.parent_node.right_child = left_child;//用左子树替换掉当前节点在父节点的位置
                }
            }
            left_child.parent_node = parent_node;//原左子树(现父节点)的父节点改为原父节点(现右子树)的父节点

            TreeNode left_child_right = left_child.right_child;//获取原左子树的右节点
            //如果不为空则把左子树的右节点设为原父节点的左子树
            if (left_child_right != null) {
                left_child_right.parent_node = node;
            }
            node.left_child = left_child_right;//原左子树右节点的父节点改为现右子树(原父节点)的左节点

            left_child.right_child = node;//现父节点(原左子节点)的右子树改为原父节点
            node.parent_node = left_child;//现右子树(原父节点)的父节点改为原左子树(现父节点)
            reCountNodeBF();//右旋完成重新计算节点的平衡因子
            System.out.println("右旋后: " + tree.toString());
        }
        

右右

  • 针对RR型(right_right)的旋转策略为:左旋

此情况下最小不平衡子树为根节点7,由下图可知根节点的左子树深度为0,右子树深度为2
则需要对节点10执行左旋


  • 旧根节点(节点7)为新根节点(节点 10)的左子树
  • 新根节点(节点10)的左子树(如果存在)为旧根节点的右子树

节点左旋的代码

/**
         * 节点左旋
         *
         * @param node 需要左旋的节点
         */
        private void leftRotate(TreeNode node) {
            TreeNode right_child = node.right_child;//当前节点的右子树
            TreeNode parent_node = node.parent_node;//当前节点的父节点
            if (parent_node != null) {//如果当前节点不是根节点
                //判断当前节点是父节点的左子树还是右子树
                if (node.equals(parent_node.left_child)) {
                    parent_node.left_child = right_child;//用左子树替换掉当前节点在父节点的位置
                } else if (node.equals(parent_node.right_child)) {
                    parent_node.right_child = right_child;//用左子树替换掉当前节点在父节点的位置
                }
            }
            right_child.parent_node = parent_node;//原右子树(现父节点)的父节点改为原父节点(现左子树)的父节点

            TreeNode right_child_left = right_child.left_child;//获取原右子树的左节点
            //如果不为空则把右子树左节点的父节点设为原父节点(现左子树的)的右节点
            if (right_child_left != null) {
                right_child_left.parent_node = node;
            }
            node.right_child = right_child_left;//原右子树左节点的父节点改为现左子树(原父节点)的右节点

            right_child.left_child = node;//现父节点(原右子树)的左子树改为原父节点
            node.parent_node = right_child;//现左子树(原父节点)的父节点改为原右子树(现父节点)
            reCountNodeBF();//左旋完成重新计算节点的平衡因子
            System.out.println("左旋后: " + tree.toString());
        }
        

左右

  • 针对LR型(left_right)的旋转策略为:先左旋转成左左,再右旋

此情况下最小不平衡子树为根节点7,由下图可知根节点的左子树深度为2,右子树深度为0,且新节点(6)小于根节点的左子节点(5)
则需要先对节点6执行左旋再右旋


右左

  • 针对RL型(right_left)的旋转策略为:先右旋转成右右,再左旋

此情况下最小不平衡子树为根节点7,由下图可知根节点的左子树深度为0,右子树深度为2,且新节点(6)小于根节点的右子节点(5)
则需要先对节点6执行右旋再左旋


       /**
         * 执行旋转
         * 可能出现4中情况:
         * 1.左左 (直接右旋)
         * 2.左右(先左旋变为左左再右旋)
         * 3.右右(直接左旋)
         * 4.右左(先右旋变为右右再左旋)
         *
         * @param new_node         新插入的节点
         * @param minUnBalanceTree 最小不平衡子树
         */
        private void executeRotation(TreeNode new_node, TreeNode minUnBalanceTree) {
            System.out.println("旋转前: " + tree.toString());
            TreeNode old_node = new_node.parent_node;
            if (minUnBalanceTree.bf > 1) {//左边多,需要右旋
                if (new_node != old_node.left_child) {//左右(当前节点不是旧节点的左子树)
                    leftRotate(old_node);//先左旋变为左左
                    minUnBalanceTree = countMinUnBalanceNode(new_node);//计算最小不平衡子树
                }
                rightRotate(minUnBalanceTree);//右旋
            } else if (minUnBalanceTree.bf < -1) {//右边多,需要左旋
                if (new_node != old_node.right_child) {//右左(当前节点不是旧节点的右子树)
                    rightRotate(old_node);//先右旋变为右右
                    minUnBalanceTree = countMinUnBalanceNode(new_node);//计算最小不平衡子树
                }
                leftRotate(minUnBalanceTree);//左旋
            }
        }
        

插入节点的方法修改后为

       /**
         * 插入节点
         * 执行步骤:
         * 1.计算插入节点的坐标
         * 2.添加到队列
         * 3.重新计算每个节点的平衡因子
         * 4.计算最小不平衡子树
         * 5.如果有最小不平衡子树,则执行旋转
         *
         * @param data 节点值
         */
        public void insertNode(int data) {
            TreeNode new_node = new TreeNode(data);
            int size = tree.size();
            if (size == 0) {//如果树里没有节点,是第一次添加
                tree.add(new_node);
            } else {
                indexOf(getRootNode(), new_node);//计算插入的位置
                if (new_node.parent_node != null) {
                    tree.add(new_node);//添加到队列
                }
                reCountNodeBF();//重新计算每个节点的平衡因子
                TreeNode minUnBalanceTree = countMinUnBalanceNode(new_node);//计算最小不平衡子树
                if (minUnBalanceTree != null) {//代表失衡
                    System.out.println("最小不平衡子树为: " + minUnBalanceTree.toString());
                    executeRotation(new_node, minUnBalanceTree);//执行旋转
                }
            }
        }
        

同理我们可以很轻松的写出一个插入节点值数组的方法

       /**
         * 插入数组
         *
         * @param arr 节点值数组
         */
        public void insertArr(int[] arr) {
            for (int data : arr) {
                insertNode(data);
            }
        }
        

查找结点

  • 原理
    先来看一张图


    查找节点的思路类似于二分法

    比如我们想查找的节点为3
    根据平衡二叉树的定义可知节点左子树上的值小于节点本身,右子树上的值大于节点本身
    先从根节点开始判断,根节点的值为4大于我们的查找值3,则我们的目标节点只可能存在于根节点的左子树
    根节点的左子节点的值为2小于我们查找值3,则我们的目标节点只可能存在于此节点的右子树
    节点2的右子节点值为3等于我们的查找值,则此节点为我们的目标节点

    同理,如果我们查找一个不存在的节点7

    先从根节点开始判断,根节点的值为4小于我们的查找值7,则我们的目标节点只可能存在于根节点的** 右子树**
    根节点的右子节点的值为5小于我们查找值7,则我们的目标节点只可能存在于此节点的右子树
    节点6的右子节点为空,并且节点值小于我们的查找值7,此时代表目标节点不存在

  • 代码实现

       /**
         * 查找节点
         * 类似于二分法
         *
         * @param data 节点值
         * @return Node
         */
        public TreeNode searchNode(int data) {
            TreeNode rootNode = getRootNode();//得到根节点
            return dichotomousSearch(data, rootNode);//从根节点开始往下查找(使用二分法)
        }
        
       /**
         * 递归二分查找
         * 有以下几种情况:
         * 1.目标值等于比较节点的值,代表已找到
         * 2.目标值小于比较节点的值,,则从比较节点的左子树开始递归查找
         * 3.目标值大于比较节点的值,则从比较节点的右子树开始递归查找
         *
         * @param target_data  目标值
         * @param compare_node 比较目标值的节点
         * @return Node
         */
        private TreeNode dichotomousSearch(int target_data, TreeNode compare_node) {
            if (target_data == compare_node.data) {//如果目标值等于比较节点的值,则返回比较节点
                return compare_node;
            } else if (target_data < compare_node.data && compare_node.left_child != null) {//如果目标值大于比较节点的值,则从比较节点的左子树开始递归查找
                return dichotomousSearch(target_data, compare_node.left_child);
            } else if (target_data > compare_node.data && compare_node.right_child != null) {//如果目标值大于比较节点的值,则从比较节点的右子树开始递归查找
                return dichotomousSearch(target_data, compare_node.right_child);
            }
            return null;
        }
       

总结一下,插入节点的流程为:

  1. 遍历二叉树,判断新插入的节点是否已经存在,若存在,则对该节点count++,不重复插入
  2. 计算新节点的坐标(它的父节点是谁,它是父节点的左子节点还是右子节点)
  3. 把节点添加到二叉树
  4. 遍历二叉树,重新计算所有节点的平衡因子,看是否有节点失衡
  5. 如果有失衡则从新插入的节点开始,找出最小不平衡子树
  6. 根据失衡的类型(左左,左右,右左,右右),执行不同的旋转策略

删除结点
删除节点的情况比较复杂,因为删除节点和添加节点一样,有可能会导致二叉树失衡


删除过程可分为三种情况

  1. 被删除的节点为叶子节点(比如上图中的节点1、3、6),即没有左右子节点
  2. 被删除的节点只有左子树或只有右子树(比如上图中的节点5)
  3. 被删除的节点既有左子树又有右子树(比如上图中的节点4,2)

我们需要知道这么一点,左子树上节点的删除相当于我们在右子树上插入了一个新节点,右子树上节点的删除相当于在左子树上插入了一个新节点,根据这一点,我们进行判断并采取对应的平衡调整操作。

分析一下这三种情况:

  1. 被删除的节点为叶子节点
    例1:


    比如上图中的节点{8,14,25,31,40}都是叶子节点,我们来删除节点14

    删除后的树结构为:

删除一个几点我们需要重被删除节点的父节点开始,往上寻找是否有失衡节点

  • 节点14的父节点值为10,节点10的左子树深度为1,右子树深度为0,则bf = 1;未失衡,继续往上寻找

  • 节点10的父节点值为20,节点20的左子树深度为2,右子树深度为3,则bf = -1;未失衡,有于此节点已经是根节点,停止寻找

  • 此时二叉树处于平衡状态

    例2:


    上图中的节点{7,21,40}都是叶子节点,我们来删除节点7

    删除后的树结构为:


    依照例1的规律,检索节点的步骤为

  • 节点7的父节点值为8,节点8没有子树,则bf = 0;未失衡,继续往上寻找
  • 节点8的父节点值为20,节点20的左子树深度为1,右子树深度为3,则bf = -2,此节点失衡,则此节点为最小不平衡子树
  • 此时二叉树处于失衡状态

那么该如何调整呢?

上文提过,在左子树上删除节点其实就相当于在右子树上插入节点。

我们找到节点20的右子树上的子节点30,发现节点30的左子树高度比右子树高,这就相当于在节点20的右子树节点30的左子树下插入了一个新的节点。这就需要进行RL型(右左)调整。

==注意:这里的RL型(右左)调整不同于上面插入节点的调整步骤!!!==

先对最小不平衡子树(节点20)的右子节点(30)进行右旋调整,调整后为:


接下来对最小不平衡子树(节点20)进行左旋调整,调整后为:

通过检索可得,此时的二叉树保持平衡状态

例3:


我们来删除节点8,删除后的树结构为:

通过检索可以得出,根节点(25)为最小不平衡子树,bf = -2,此树处于失衡状态,需要进行调整。
上文说过,在左子树上删除节点其实就相当于在右子树上插入节点。但要如何调整还取决于该失衡节点的右子树的左右子树的高度差。

调整的步骤为:

  1. 找到最小不平衡子树(根节点25)的右子节点(30)
  2. 节点30的左右子树高度一样,故只需要RR型(右右)调整

调整后的二叉树为:

总结一下,删除叶子节点的几个步骤为:

  1. 将该结点直接从树中删除
  2. 从被删除的节点的父节点开始向上检索,判断是否节点左右子树高度差超过1;如果有,则此树失衡,且该节点是最小不平衡子树;如果没有,则继续向上检索,直至根节点。
  3. 如果检索到了失衡,则根据失衡的类型(LL、LR、RL、RR)执行不同的旋转策略(==此时的策略和添加节点的旋转策略有所不同==)

代码实现:

       /**
         * 删除一个叶子节点(没有左右子树的节点)
         *
         * @param node 被删除节点
         * @return Result
         */
        private Result deleteNodeByLeafNode(TreeNode node) {
            if (node.parent_node == null) {//该节点是根节点(此情况下二叉树只有一个节点)
                tree.remove(node);//从队列中移除此节点
                return new Result(true, node);
            } else {//该节点是叶子节点
                if (node == node.parent_node.left_child) {//该节点是父节点的左子节点
                    node.parent_node.left_child = null;//节点置空
                } else if (node == node.parent_node.right_child) {//该节点是父节点的右子节点
                    node.parent_node.right_child = null;//节点置空
                }
            }
            tree.remove(node);//从队列中移除此节点
            return new Result(false, node);
        }
        

删除结果回调类:

 /**
     * 删除结果回调类
     */
    static class Result {
        private boolean isSkipBuild;//是否跳过检索
        private TreeNode target_node;//标记的节点

        public Result(boolean isSkipBuild, TreeNode target_node) {
            this.isSkipBuild = isSkipBuild;
            this.target_node = target_node;
        }
    }

判断如何执行旋转的方法:

       /**
         * 整理节点树
         * 判断是否需要执行旋转
         *
         * @param target_node
         */
        private void rebuild(TreeNode target_node) {
            TreeNode minUnBalanceTree = countMinUnBalanceNode(target_node);//查找最小不平衡子树,从被删除节点的父节点开始层级查找
            if (minUnBalanceTree == null) return;
            System.out.println("最小不平衡子树为: " + minUnBalanceTree.toString());
            if (minUnBalanceTree.bf > 1) {//被删除节点属于最小不平衡子树的右分支
                if (minUnBalanceTree.left_child.bf >= 0) {//左树和右树相等或左树比右树高
                    System.out.println("旋转前: " + tree.toString());
                    rightRotate(minUnBalanceTree);
                } else if (minUnBalanceTree.left_child.bf < 0) {//右树比左树高
                    leftRotate(minUnBalanceTree.left_child);
                    rightRotate(minUnBalanceTree);
                }
            } else if (minUnBalanceTree.bf < -1) {//被删除节点属于最小不平衡子树的左分支
                if (minUnBalanceTree.right_child.bf > 0) {//左树比右树高
                    rightRotate(minUnBalanceTree.right_child);
                    leftRotate(minUnBalanceTree);
                } else if (minUnBalanceTree.right_child.bf <= 0) {//右树和左树等高或右树比左树高
                    System.out.println("旋转前: " + tree.toString());
                    leftRotate(minUnBalanceTree);
                }
            }
            //递归向上检索
            rebuild(minUnBalanceTree.parent_node);
        }
      
  1. 被删除的节点只有一颗子树(左子树或右子树)
**例1:**
![在这里插入图片描述](https://img-blog.csdnimg.cn/e11dbc4ff2db48af80451c4930c7d057.png#pic_center)
该树中,只有一颗子树的节点为{29,40},我们来删除节点29

**删除后的树结构为:**

![在这里插入图片描述](https://img-blog.csdnimg.cn/525100bb144947b9a3f98b225d87801e.png#pic_center)
可以看到此时二叉树处于平衡状态

如果我们将**例1**中删除的节点改为**40**,则需要用节点**40**的右子树或者左子树替换掉节点40在其父节点中的位置
因为节点**40**只有右子树,此时我们用右子节点**50**进行替换

**删除后的树结构为:**

![在这里插入图片描述](https://img-blog.csdnimg.cn/892d705fc72b47318ff02bfa3ea336d8.png#pic_center)
通过检索可知节点30失衡,最小不平衡子树为30, **删除右子树的节点相当于在左子树上插入新的节点** ,也就是相当于在左子树的右子树上插入节点。因此我们这里要进行LR型调整。

***调整步骤:***
- 我们首先找到被删除节点40的替代节点50
 - 从节点50开始,向上检索失衡节点
 - 检索到根节点(30)失衡,bf = 2,
 - 找到最小不平衡子树(根节点30)的左子节点(25),执行左旋
 - 再对最小不平衡子树(根节点30)执行右旋

左旋后的树结构为:


右旋后的树结构为:

总结一下,删除有一个子节点的节点的步骤为:

  1. 将该结点直接从树中删除
  2. 将该节点的后继子节点(左子节点或右子节点)替换该节点原来的位置
  3. 从被删除的节点的父节点开始向上检索,判断是否节点左右子树高度差超过1;如果有,则此树失衡,且该节点是最小不平衡子树;如果没有,则继续向上检索,直至根节点。
  4. 如果检索到了失衡,则根据失衡的类型(LL、LR、RL、RR)执行不同的旋转策略(==此时的策略和添加节点的旋转策略有所不同==)

代码实现:

       /**
         * 删除一个只有左子树的节点
         *
         * @param node 被删除节点
         * @return Result
         */
        private Result deleteNodeByOnlyleftTree(TreeNode node) {
            if (node.parent_node != null && node.data < node.parent_node.data) {//如果存在父节点并且此节点的值小于父节点的值(代表此节点为父节点的左子节点)
                node.parent_node.left_child = node.left_child;//当前父节点的左子节点替换为此节点的左子节点
                node.left_child.parent_node = node.parent_node;//当前节点左子节点的父节点替换为当前节点的父节点
            } else if (node.parent_node != null && node.data > node.parent_node.data) {//如果存在父节点并且此节点的值大于于父节点的值(代表此节点为父节点的右子节点)
                node.parent_node.right_child = node.left_child;//当前父节点的右子节点替换为此节点的左子节点
                node.left_child.parent_node = node.parent_node;//当前节点左子节点的父节点替换为当前节点的父节点
            } else {//代表被删除的节点是根节点且根节点只有左子节点
                node.left_child.parent_node = null;//直接将该节点左子节点的父节点引用置空,并跳过检索
                tree.remove(node);//从队列中移除此节点
                return new Result(true, node);
            }
            tree.remove(node);//从队列中移除此节点
            return new Result(false, node);
        }
       /**
         * 删除一个只有右子树的节点
         *
         * @param node 被删除节点
         * @return Result
         */
        private Result deleteNodeByOnlyRightTree(TreeNode node) {
            if (node.parent_node != null && node.data < node.parent_node.data) {//如果存在父节点并且此节点的值小于父节点的值(代表此节点为父节点的左子节点)
                node.parent_node.left_child = node.right_child;//当前父节点的左子节点替换为此节点的右子节点
                node.right_child.parent_node = node.parent_node;//当前节点左子节点的父节点替换为当前节点的父节点
            } else if (node.parent_node != null && node.data > node.parent_node.data) {//如果存在父节点并且此节点的值大于于父节点的值(代表此节点为父节点的右子节点)
                node.parent_node.right_child = node.right_child;//当前父节点的右子节点替换为此节点的右子节点
                node.right_child.parent_node = node.parent_node;//当前节点左子节点的父节点替换为当前节点的父节点
            } else {//代表被删除的节点是根节点且根节点只有右子节点
                node.right_child.parent_node = null;//直接将该节点右子节点的父节点引用置空,并跳过检索
                tree.remove(node);//从队列中移除此节点
                return new Result(true, node);
            }
            tree.remove(node);//从队列中移除此节点
            return new Result(false, node);
        }
  • 被删除的节点既有左子树又右子树

    例1:


    满足此条件的节点有{10,20},我们来删除根节点(20)
    这里和情况下2不同,因为根节点同时拥有左右子树,所以不能简单的用左右子树去替换被删除节点的位置

    我们来分析一下情况:

 - 由根节点(20)的平衡因子为1可以知道,根节点的左树深度是比右树高的
 - 此时我们需要找到根节点左子树中的最大值节点
 - 由上图可知最大值节点为15,我们将节点15的值和节点20的值进行替换(==仅仅是值的替换,不改变两个节点的其它信息,比如说父节点引用、左右子节点引用、平衡因子、插入次数==)

替换后的树结构为:

  • 替换后再删除值为20的节点

删除后的树结构为:

此时我们发现被删除的值为20的节点的父节点(10)bf = 2,处于失衡的状态,此时最小不平衡子树为节点10。
需要对以10为根节点的子树进行调整【这个地方进行删除操作时,相当于在以10为根节点的左子树的右子树上进行插入操作】,也就是进行LR调整。先对节点10的左子节点(8)进行左旋,让其成为左左

左旋后的树结构为:


再对最小不平衡子树(节点10)进行右旋

右旋后的树结构为:


此时的二叉树保持平衡

总结一下,删除有左右子树的节点的步骤为:

  1. 判断被删除的节点bf 值 属于[0,1] 的区间还是 = -1( 属于[0,1] 说明该节点左子树比右子树深或和右子树一样深,等于-1表示右子树比左子树深)
  2. 如果是左子树深度 > = 右子树深度,则找到左子树中的最大值节点
  3. 如果是右子树深度 > 左子树深度,则找到右子树中的最小值节点
  4. 将找到的最大或最小值节点与被删除的节点进行值的交换(==仅仅只交换值,不修改其它属性==)
  5. 删除原值所在的节点
  6. 从被删除的节点的父节点开始向上检索,判断是否节点左右子树高度差超过1;如果有,则此树失衡,且该节点是最小不平衡子树;如果没有,则继续向上检索,直至根节点。
  7. 如果检索到了失衡,则根据失衡的类型(LL、LR、RL、RR)执行不同的旋转策略(==此时的策略和添加节点的旋转策略有所不同==)

PS: 关于最大值节点和最小值节点,这里我们来探究一个问题。
为什么左子树深度 > = 右子树深度,则找左子树中的最大值节点去替换,右子树深度 > 左子树深度,则找到右子树中的最小值节点去替换呢?这里能不能反过来呢?

我们先来看反过来会有什么后果,还是上面的那个例子


我们删除节点20,本来是用左子树中的最大值节点15去替换它,这回我们反过来,用左子树中的最小值节点8去替换

替换后的树结构为:

  • 替换后再删除值为20的节点

    删除后的树结构为:


    此时根节点(8)的左子节点值10,右子节点值为25。由上文二叉树的定义可知,任意节点左子树上的值均小于该节点,右子树上的值均大于该节点。很显然节点8的左子节点值大于此节点,不符合二叉树的要求。所以这里只能用左子树上的最大值节点来替换。

如何获取左子树上的最大值节点

例图:


比如我想获取节点根节点4的左子树中最大值节点3,由于任意节点的右子树值都大于该节点的值 ,获取节点3其实就是获取根节点左子树中最右边的那个节点。
那如果节点左子树中没有最右边的节点怎么办呢?
例如我想获取节点7左子树中的最大值节点,节点7的左子树为节点6,由于节点6的右子节点为空,此时节点6就是左子树中的最大值节点。

代码实现:
这里提供两种实现思路

  • 递归

       /**
         * 获取目标节点的最大值节点
         * 获取节点的最大值其实就是获取该节点的最右子节点
         *
         * @param node
         * @return
         */
        private TreeNode getMaxNode(TreeNode node) {
            if (node == null) return null;
            return node.right_child == null ? node : getMaxNode(node.right_child);
        }
        
    
  • 循环

       /**
         * 获取目标节点的最大值节点
         * 获取节点的最大值其实就是获取该节点的最右子节点
         *
         * @param node
         * @return
         */
        private TreeNode getMaxNode(TreeNode node) {
            if (node == null) return null;
             while (node.right_child != null) {
                node = node.right_child;
            }
            return node;
        }
        
    

    同理可以得出获取节点右子树中的最小值节点

    代码实现:

    • 递归
       /**
         * 获取目标节点的最小值节点
         * 获取节点的最小值其实就是获取该节点的最左子节点
         *
         * @param node
         * @return
         */
        private TreeNode getMinNode(TreeNode node) {
            if (node == null) return null;
            return node.left_child== null ? node : getMinNode(node.left_child);
        }
        
    
  • 循环

       /**
         * 获取目标节点的最小值节点
         * 获取节点的最小值其实就是获取该节点的最左子节点
         *
         * @param node
         * @return
         */
        private TreeNode getMinNode(TreeNode node) {
            if (node == null) return null;
             while (node.left_child!= null) {
                node = node.left_child;
            }
            return node;
        }
        
    

删除节点完整的代码为:

       /**
         * 删除节点
         * * 有四种情况:
         * * 1.删除的节点没有左右子节点(叶子节点或根节点)
         * * 2.删除的节点有左子节点
         * * 3.删除的节点有右子节点
         * * 4.删除的节点有左右子节点
         *
         * @param data 节点值
         * @return ture:删除成功,false:删除失败
         */
        public boolean deleteNode(int data) {
            TreeNode node = searchNode(data);//查找该节点
            if (node == null) return false;
            boolean isSkipBuild = false;//是否跳过rebuild()
            Result result = null;//删除结果回调
            /**
             * 如果删除的节点没有左右子节点
             * 1.判断该节点是否是根节点(删除没有左右子树的根节点可以跳过检索)
             * 2.判断该节点是父节点的左子节点还是右子节点,把其在父节点的引用链置空
             * 3.删除该节点
             * 4.从被删除的节点的父节点向上检索是否有节点失衡,如果有则停止检索,根据失衡的类型执行旋转
             */
            if (node.left_child == null && node.right_child == null) {
                result = deleteNodeByLeafNode(node);//删除一个叶子节点(没有左右子树的节点)
                node = result.target_node;
                isSkipBuild = result.isSkipBuild;
                System.out.println("删除的节点为: " + node.toString());
                /**
                 * 如果删除的节点只有左子树
                 * 1.判断该节点是否是根节点(删除只有左子树的根节点可以跳过检索)
                 * 2.判断该节点是父节点的左子节点还是右子节点,将其原来在父节点中的引用链替换为该节点的左子节点,将该节点的左子节点的父节点替换为该节点的父节点
                 * 3.删除节点
                 * 4.从被删除的节点的父节点向上检索是否有节点失衡,如果有则停止检索,根据失衡的类型执行旋转
                 */
            } else if (node.left_child != null && node.right_child == null) {
                result = deleteNodeByOnlyleftTree(node);//删除一个只有左子树的节点
                node = result.target_node;
                isSkipBuild = result.isSkipBuild;
                System.out.println("删除的节点为: " + node.toString());
                /**
                 * 如果删除的节点只有右子树
                 * 1.判断该节点是否是根节点(删除只有右子树的根节点可以跳过检索)
                 * 2.判断该节点是父节点的左子节点还是右子节点,将其原来在父节点中的引用链替换为该节点的右子节点,将该节点的右子节点的父节点替换为该节点的父节点
                 * 3.删除节点
                 * 4.从被删除的节点的父节点向上检索是否有节点失衡,如果有则停止检索,根据失衡的类型执行旋转
                 */
            } else if (node.right_child != null && node.left_child == null) {
                result = deleteNodeByOnlyRightTree(node);//删除一个只有右子树的节点
                node = result.target_node;
                isSkipBuild = result.isSkipBuild;
                System.out.println("删除的节点为: " + node.toString());
                /**
                 * 如果删除的节点有左右子树
                 * 1.判断该节点的平衡因子是否为0或1,如果是,则寻找到该节点左子树中的最大值节点(left_max节点),把该节点与left_max节点的值进行交换(只交换值,不替换引用链,计算因子等)
                 * 2.判断判断该节点的平衡因子是否为-1,如果是,则寻找到该节点右子树中的最小值节点(right_min节点),把该节点与right_min节点的值进行交换(只交换值,不替换引用链,计算因子等)
                 * 3.删除替换值后的left_max节点或right_min节点
                 * 4.从被删除的节点的父节点向上检索是否有节点失衡,如果有则停止检索,根据失衡的类型执行旋转
                 */
            } else if (node.left_child != null && node.right_child != null) {
                if (node.bf == 0 || node.bf == 1) {//如果该节点的平衡因子是0或1
                    TreeNode max_node = getMaxNode(node.left_child);//获取被删除节点左子树中的最大值节点(该节点只可能是一个叶子节点,或者是一个只有左子树的节点)
                    int temp_data = node.data;//节点值互换
                    node.data = max_node.data;
                    max_node.data = temp_data;
                    if (max_node.left_child != null) {//该节点是一个只有左子树的节点
                        result = deleteNodeByOnlyleftTree(max_node);//删除一个只有左子树的节点
                        node = result.target_node;
                        isSkipBuild = result.isSkipBuild;
                    } else {//该节点是一个叶子节点
                        result = deleteNodeByLeafNode(max_node);//删除一个叶子节点(没有左右子树的节点)
                        node = result.target_node;
                        isSkipBuild = result.isSkipBuild;
                    }
                    System.out.println("删除的节点为: " + max_node.toString());
                } else if (node.bf == -1) {//如果该节点的平衡因子是-1
                    TreeNode min_node = getMinNode(node.right_child);//获取被删除节点右子树中的最小值节点(该节点只可能是一个叶子节点,或者是一个只有右子树的节点)
                    int temp_data = node.data;//节点值互换
                    node.data = min_node.data;
                    min_node.data = temp_data;//节点值互换
                    if (min_node.right_child != null) {//该节点是一个只有右子树的节点
                        result = deleteNodeByOnlyRightTree(min_node);//删除一个只有右子树的节点
                        node = result.target_node;
                        isSkipBuild = result.isSkipBuild;
                    } else {//该节点是一个叶子节点
                        result = deleteNodeByLeafNode(min_node);//删除一个叶子节点(没有左右子树的节点)
                        node = result.target_node;
                        isSkipBuild = result.isSkipBuild;
                    }
                    System.out.println("删除的节点为: " + min_node.toString());
                }
            }
            reCountNodeBF();//重新计算平衡因子
            if (!isSkipBuild) {
                rebuild(node);
            }
            return true;
        }

修改节点

修改节点比较简单,这里只说一下实现思路,不画图做原理的探讨。修改一个节点的值等于删除这个节点再插入一个新节点

修改步骤:

  1. 检索二叉树,判断修改的节点是否存在
  2. 删除检索到的节点
  3. 新建一个节点,节点值为修改值

代码实现:

       /**
         * 修改节点
         * 修改一个节点的值等于删除这个节点再把期望值赋给新节点
         *
         * @param target_value   目标值
         * @param expected_value 期望值
         * @return 修改结果
         */
        public boolean modifyNode(int target_value, int expected_value) {
            TreeNode node = searchNode(target_value);
            if (node == null) return false;//查无此节点
            if (deleteNode(target_value)) {//删除目标值节点
                insertNode(expected_value);//如果删除成功则插入一个新节点,节点值为目标值
            } else {
                return false;
            }
            return true;
        }

最后贴上完整的源码资源,有兴趣的可以复制下来测试。
源码地址
==ps:代码并未做性能上的优化,有兴趣的可以深入研究==

你可能感兴趣的:(JAVA实现平衡二叉树(AVL))