数据结构--AVL树

发布于:2025-05-07 ⋅ 阅读:(14) ⋅ 点赞:(0)

目录

前言

AVL树的特点

AVL树的插入

节点的定义

情况分析

AVL树的旋转

右单旋

左单旋

左右双旋

右左双旋

​编辑总结 

验证AVL树


前言

二叉搜索树可以帮助我们以极高的效率查找(理想情况下是logn),但是当在极端情况下,比如当树中的节点值是有序的时,二叉搜索树会变成一个单枝树,相当于一个链表,于是乎为了让树更接近与一个完全二叉树,想着当向二叉搜索树中插入新节点后,让每个节点的左右子树高度差的绝对值不超过1就可以降低树的高度从而提高效率,于是就有了AVL树。

AVL树的特点

  • 每个节点的左右子树都是AVL树
  • 左右子树高度差(平衡因子)的绝对值不超过1

AVL树的插入

节点的定义

static class TreeNode {
        public int val;
        public TreeNode left;
        public TreeNode right;
        public int bf;//平衡因子
        public TreeNode parent;

        public TreeNode(int val) {
            this.val = val;
        }
    }

 //平衡因子=右子树高度-左子树高度

情况分析

因为AVL树也是二叉搜索树,所以先按照二叉搜索树的方式插入一个节点cur,但是当cur被插入后,其父节点parent的平衡因子必定会发生变化(左右子树高度差必定发生变化),所以每次插入节点后都需要更新平衡因子,并且检查AVL树的结构是否被破坏。情况可以分为以下几种

cur插入之前parent的平衡因子会有三种可能'-1' ,'0' ,'1'

  • 当cur插入到parent的左边时parent的平衡因子-1
  • 当cur插入到parent的右边时parent的平衡因子+1 

cur插入之后parent的平衡因子可能会有五种可能‘-1’,‘+1’,‘0’,‘-2’,‘+2’

  •  如果cur插入后parent的平衡因子是0,说明插入之前它的平衡因子是正负1,所以插入后才可能被调整为0,此时cur成功插入
  • 如果cur插入后parent的平衡因子是正负1,说明插入之前它的平衡因子是0,所以插入后才可能被调整为正负1,此时以parent为根的树高度增加了,上面节点的平衡因子可能会被打破所以需要继续向上更新
  • 如果cur插入后parent的平衡因子是正负2,则说明以parent为根的树不符合AVL树的性质,需要对其进行调整,既旋转

插入节点

TreeNode node = new TreeNode(val);
        //如果根节点为空直接插入
        if (root == null) {
            root = node;
            return true;
        }
        TreeNode parent = null;
        TreeNode cur = root;
        while (cur != null) {
            if (val > cur.val) {
                parent = cur;
                cur = cur.right;
            } else if (val < cur.val) {
                parent = cur;
                cur = cur.left;
            } else {
                return false;
            }
        }
        //cur == null
        if (val > parent.val) {
            parent.right = node;
        } else if (val < parent.val) {
            parent.left = node;
        }

调整parent的平衡因子

//调整
        cur = node;
        node.parent = parent;
        while (parent != null) {
            //如果cur是父节点的右孩子,父节点的平衡因子就加一,否则减一
            if (cur == parent.right) {
                parent.bf++;
            } else {
                parent.bf--;
            }
            //判断平衡因子
            if (parent.bf == 0) {
                //已经平衡
                break;
            } else if (parent.bf == 1 || parent.bf == -1) {
                //继续向上调整
                cur = parent;
                parent = cur.parent;
            } else {
                if (parent.bf == 2) {//右树高,需要降低右树高度(说明cur插到了parent的右边)
                    if (cur.bf == 1) {
                        //左单旋
                      
                    } else if (cur.bf == -1) {
                        //右左双旋
                        
                    }
                    //左树高,需要降低左树高度
                } else {//parent.bf == -2(说明cur插到了parent的左边)
                    if (cur.bf == -1) {
                        //右单旋
                        
                    } else if (cur.bf == 1) {
                        //左右双旋
                       
                    }
                }
                break;
            }
        }

AVL树的旋转

右单旋

当新节点插入较高左子树的左侧

上图是一个简单的右旋,当新节点10插入到30的左子树时(这里是左子树),破坏了avl树的平衡节点50的平衡因子变成-2,表示左子树比右子树高2,所以我们需要降低左子树,提高右子树。既将50节点向右旋转到30节点的左边(因为50比三十大,只能在30的右边),此时这颗AVL树就平衡了(别忘了修改平衡因子bf)

但是实际插入中的AVL树的结构可能不会和上图这样简单比如:

  • 30的右边可能已经有节点了

0是新插入的节点插到了30的左子树的左侧,破坏了AVL树的平衡,使50节点的平衡因子变成-2,所以和刚刚一样我们需要降低左树,抬高右树,我们让50节点向右旋转,但是此时30节点的右边已经有了一个节点(也可能是30的右子树的根),不过这个节点一定是大于30小于50的,所以我们只需要将这个节点放到50节点的左边,然后再将50节点放到30节点的右边即可,最后修改平衡因子。

  •  50可能是其他节点的左右子树或者根节点

 如果50是根节点那么当旋转完成后要更新根节点,如果50是其父节点的左孩子,那么要让30节点变成50原父节点的左孩子,右孩子则相反

代码

public void rotateRight(TreeNode parent) {

        TreeNode pParetn = parent.parent;//记录parent的父节点
        TreeNode subL = parent.left;//parent的左孩子
        TreeNode subLR = subL.right;//parent的左孩子的右孩子

        parent.left = subLR;
        if (subLR != null) {
            subLR.parent = parent;
        }
        subL.right = parent;
        parent.parent = subL;

        //检查当前parent是不是根节点
        //是根节点的话直接让subL为根节点
        if (parent == root) {
            root = subL;
            root.parent = null;
        } else {//不是根节点,就判断parent是其父节点的左孩子还是右孩子
            if (parent == pParetn.left) {//parent是左孩子,就让subL也是左孩子
                pParetn.left = subL;
            } else {
                pParetn.right = subL;
            }
            subL.parent = pParetn;
        }
        subL.bf = 0;
        parent.bf = 0;
    }

首先先标记右旋所需要的节点,然后开始右旋,过程和前文一样

  1. 将subLR作为parent的左孩子(就算subLR为空也无所谓)
  2. 判断subL是否有右孩子(subLR) ,有的话就令subLR的父节点位parent
  3. 让subL的右节点为parent
  4. 让parent的父节点为subLR
  5. 旋转完成后判断parent是否是根节点是的话,更新根节点,有父节点的话则让subL为parent父节点的孩子
  6. 最后修改平衡因子

右旋代码既图解 

左单旋

当新节点插入较高右子树的右侧

左单旋的方法和右单旋一样,只是方向不一样,具体过程参考右单旋

代码及图解

private void rotateLeft(TreeNode parent) {

        TreeNode subR = parent.right;
        TreeNode subRL = subR.left;

        parent.right = subRL;
        subR.left = parent;
        if (subRL != null) {
            subRL.parent = parent;
        }
        TreeNode pParent = parent.parent;
        parent.parent = subR;
        //检查当前parent是不是root
        if (parent == root) {
            root = subR;
            root.parent = null;
        } else {//不是root
            if (pParent.right == parent) {
                pParent.right = subR;
            } else {
                pParent.left = subR;
            }
            subR.parent = pParent;
        }
        parent.bf = 0;
        subR.bf = 0;
    }

 左旋

  1. 首先先标记左旋所需要的节点,然后开始左旋
  2. 将subRL作为parent的右孩子
  3. 判断subR的左孩子是否为空,如果不为空则令subR的左孩子(subRL)的父节点为parent
  4. 令subR的左孩子为parent
  5. 令parent的parent为subR
  6. 旋转完成后判断parent是否是根节点是的话,更新根节点,有父节点的话则让subR为parent父节点的孩子
  7. 最后修改平衡因子

左右双旋

新节点插入较高左子树右侧

这种情况无法通过直接左旋或者右旋使树平衡,需要先左旋再右旋,通过左旋把树变成左子树较高的情况然后再右旋

可以看到只是右旋达不到平衡的结果

先左旋再右旋

private void rotateLR(TreeNode parent) {
        TreeNode subL = parent.left;
        TreeNode subLR = subL.right;
        int bf = subLR.bf;

        rotateLeft(parent.left);
        rotateRight(parent);

        if (bf == 1) {
            subL.bf = 0;
            subLR.bf = 0;
            parent.bf = 1;
        } else if (bf == -1) {
            subL.bf = -1;
            subLR.bf = 0;
            parent.bf = 0;
        }
    }

 以上面的例子说明当bf==1时说明新节点插到45左边,bf==-1时说明新节点插到45右边

bf==-1时旋转结果

右左双旋

新节点插到较高右子树左侧

private void rotateRL(TreeNode parent) {
        TreeNode subR = parent.right;
        TreeNode subRL = subR.left;
        int bf = subRL.bf;

        rotateRight(parent.right);
        rotateLeft(parent);

        if (bf == 1) {
            parent.bf = -1;
            subR.bf = 0;
            subRL.bf = 0;
        } else if (bf == -1) {
            parent.bf = 0;
            subR.bf = 1;
            subRL.bf = 0;
        }
    }

总结 

当新节点插入后如果树不平衡,既parent的平衡因子为-2或者2

1.parent的平衡因子为2,说明右树高

  • 如果SubR(右子树)的平衡因子为1,说明新节点插入较高右子树的右侧,执行左单旋
  • 如果SubR的平衡因子为-1,说明新节点插入到较高右子树的左侧,执行右左双旋

2.parent的平衡因子为-2,说明左树高

  • 如果SubL的平衡因子为-1,说明新节点插入到了较高左子树的左边,执行有右选
  • 如果SubL的平衡因子为1,说明新节点插入到了较高左子树的右边,执行左右双旋 

验证AVL树

验证AVL树只需要判断每个节点的左右子树高度差的绝对值都小于一就行

public int height(TreeNode node) {
        if (node == null) {
            return 0;
        }
        int left = height(node.left);
        int right = height(node.right);
        return left > right ? left + 1 : right + 1;
    }

    public Boolean isbalanced(TreeNode root) {
        if (root == null) {
            return true;
        }
        //求左右子树高度
        int left = height(root.left);
        int right = height(root.right);

        if (root.bf != right - left) {
            return false;
        }
        return Math.abs(left - right) <= 1
                && isbalanced(root.left)
                && isbalanced(root.right);
    }
  public static void main(String[] args) {
        int[] array = {17, 4, 8, 6, 17, 25, 17, 19, 10};
        AVLTree avlTree = new AVLTree();
        for (int i=0;i<array.length;++i){
            avlTree.insert(array[i]);
        }
        boolean ret = avlTree.isbalanced(avlTree.root);
        System.out.println(ret);
    }

综上,AVL树其实就是一个相对平衡的二叉搜索树,每个节点的左右子树高度差的绝对值都小于一,这样可以使其查找的时间复杂度稳定为O(logN),但是也因为其在进行修改操作时比如插入删除,需要对树进行多次旋转,使其修改变得及其麻烦。所以,如果需要 一种查询高效且有序的数据结构,而且数据的个数为静态的(修改操作较少或不修改),可以考虑AVL树,但一个结构经常修 改,就不太适合

以上就是博主对AVL树知识的分享,在之后的博客中会陆续分享有关数据结构的其他知识,如果有不懂的或者有其他见解的欢迎在下方评论或者私信博主,也希望可以多多支持博主!!🥰🥰


网站公告

今日签到

点亮在社区的每一天
去签到