二叉平衡树 AVL

二叉查找树 BST : https://blog.csdn.net/cj_286/article/details/90183298

二叉平衡树 AVL : https://blog.csdn.net/cj_286/article/details/90217072

红黑树 RBT : https://blog.csdn.net/cj_286/article/details/90245150

 

为什么需要AVL树

BST与TreeMap的效率对比

1.随机序列的存取 (他们的存取速度差不多)
2.升序或降序序列的存取 (两万的数据量TreeMap的存取(先存后取)速度是BST的四百倍左右)
 

                          BST        TreeMap
      随机序列             OK           OK
    升序或降序序列         Slow          OK

为什么BST在极端情况下存取速度会如此的慢呢,因为在极端情况下,BST会退化为链表(升序或降序),时间复杂度会由原来的O(logN)退化为O(N),所以查询速度会变慢


        4                        1                                     7
     /    \                       \                                   /
    2      6     O(logN)-->        2            O(N)-->              6       O(N)
  /  \   /  \                       \                               /
 1    3  5   7                       3                             5
                                      \                           /	
                                       4                         4
                                        \                       /
                                         5                     3
                                          \                   /
                                           6                 2
                                            \               /
                                             7             1

   BST随机存储                    BST升序存储                     BST降序存储

在升序或降序的情况下BST明显是满足不了需求的,那么有没有哪种数据结构,对于任何插入节点或者删除节点的操作都能自动的保持树的平衡,这时AVL树就诞生了,AVL树它是一种自平衡的树。

性质

以下AVL的代码是基于BST代码的,只是添加了使其平衡的代码

在计算机科学中,AVL树是最先发明的自平衡二叉查找树。在AVL树中任何节点的两个子树的高度最大差别为1,所以它也被称为高度平衡树。增加和删除可能需要通过一次或多次树旋转来重新平衡这个树。AVL树得名于它的发明者G. M. Adelson-Velsky和E. M. Landis

AVL树本质上还是一棵二叉搜索树,它的特点是:
1.本身首先是一棵二叉搜索树。
2.带有平衡条件:每个结点的左右子树的高度之差的绝对值(平衡因子Balance Factor)最多为1。
3.空树、左右子树都是AVL
也就是说,AVL树,本质上是带了平衡功能的二叉查找树(二叉排序树,二叉搜索树)。

对比AVL树和非AVL树

avl-1-1-1

由图可知,一棵AVL树不一定是完全二叉树,AVL树它的每个子节点的平衡因子的绝对值都是小于等于1的,它的每个子节点都是一个AVL树

非AVL树转为AVL树
为了简化操作,只考虑三个节点的情况
三个节点单旋转

avl-1-2-1

以3为根节点顺时针旋转,旋转之后,原来的根节点3变成了原来的左子树2的右子树,原来的左子树2变成了根节点,这时二叉树就恢复平衡变成AVL树
三个节点双旋转

avl-1-2-2

首先先处理节点1,将节点1进行左旋转,原来的右子树2变成了新的根节点,原来的1变成了2的左子树,这时的情况和上面的单旋转情况一样了,以3节点右旋就变成了一个AVL树了

JDK TreeMap右旋源码解析

avl-1-3-1

红色节点是相对位置发生了改变,l原本是左子树,右旋过后,l代替了p,p变成了l的右子树,原本l.right是l的右孩子,右旋之后,l.right变成了p的左孩子。

private void rotateRight(Entry<K,V> p) {
        if (p != null) {
            Entry<K,V> l = p.left; //取得p的左孩子l
            p.left = l.right; //l的右孩子l.right变成p的左孩子
            if (l.right != null) l.right.parent = p; //l.right的父节点设置为p
            l.parent = p.parent; //l的父节点设置为p的父节点p.parent
            if (p.parent == null)
                root = l;
            else if (p.parent.right == p) 
                p.parent.right = l; //p.parent的左孩子或者右孩子设置为l
            else p.parent.left = l;
            l.right = p; //l的右子树设置为p
            p.parent = l;//p的父节点设置为l
        }
    }

JDK TreeMap左旋源码解析

avl-1-3-2

红色节点是相对位置发生了改变,r原本是右子树,左旋过后,r替代了p,p变成了r的左孩子,原本的r.left是r的左孩子,左旋之后,r.left变成了p的右孩子
左旋和右旋完全对称

private void rotateLeft(Entry<K,V> p) {
        if (p != null) {
            Entry<K,V> r = p.right;
            p.right = r.left;
            if (r.left != null)
                r.left.parent = p;
            r.parent = p.parent;
            if (p.parent == null)
                root = r;
            else if (p.parent.left == p)
                p.parent.left = r;
            else
                p.parent.right = r;
            r.left = p;
            p.parent = r;
        }
    }

什么时候需要旋转
1,插入关键字key后,结点p的平衡因子由原来 的1或者-1,变成了2或者-2,则需要旋转:值考虑插入key到左子树left的情况,即平衡因子是2
    情况1:key < left.key,即插入到left的左子树,需要进行单旋转,将结点p右旋 (图:avl-1-4-1)
    情况2:key > left.key,即插入到left的右子树,需要进行双旋转,先将left左旋,再将p右旋 (图:avl-1-4-2 ,avl-1-4-3)
2,插入到右子树right、平衡因子为-2,完全对称
平衡因子是2的情况示图如下
情况1示图

avl-1-4-1

情况2示图(1)

avl-1-4-2

情况2示图(2)

avl-1-4-3

平衡因子是-2的情况和2的情况正好相反

插入

AVL的插入与BST完全相同,都是自顶向下的
检测是否平衡并旋转的调整过程
    1.AVL性质2决定了在检测结点p是否平衡之前,必须先保证 左右子树已经平衡
    2.子问题必须成立 推导出 总问题是否成立,则说明是自底向上(这个很重要,自底向上,在递归或者循环去实现左旋或者右旋都是自底向上的去计算高度(height),这样计算高度才不会出错,所以在代码中计算高度只右+1而没有-1,先增的叶子节点的高度都是固定的1)
    3.有parent指针,直接向上回溯
    4.无parent指针,后续遍历框架,递归
    5.无parent指针,栈实现非递归

实现 (以下AVL的代码是基于BST代码的,只是添加了使其平衡的代码)
1.AVLEntry增加height属性,表示树的高度,平衡因子可以实时计算
2.单旋转:右旋rotateRight、左旋rotateLeft
3.双旋转:先左后右firstLeftThenRight、先右后左firstRightThenLeft
4.实现非递归,需要辅助栈Stack,将插入时候所经过的路径压栈
5.插入调整函数fixAfterInsertion
6.辅助函数checkBalance,断言AVL树的平衡性,检测算法的正确性 

AVLMap中添加height属性,表示树的高度,添加获取节点高度的方法

/**
     * 返回一个结点的高度
     * @param p
     * @return
     */
    public int getHeight(AVLEntry<K,V> p){
        return p == null ? 0 : p.height;
    }

旋转调整

单旋转,右旋代码实现

avl-1-2-1
/**
     * 右旋(单旋转)
     * 该方法需要右返回值,因为AVLEntry中没有parent指针(JDK中是有parent指针的,所以不需要有返回值),旋转之后它的新的根节点需要返回
     * @param p
     * @return
     */
    private AVLEntry<K,V> rotateRight(AVLEntry<K,V> p) {
        AVLEntry<K,V> left = p.left;
        p.left = left.right;
        left.right = p;
        p.height = Math.max(getHeight(p.left),getHeight(p.right)) + 1;
        left.height = Math.max(getHeight(left.left),p.height) + 1;
        return left;//新的根节点
    }

单旋转,左旋代码实现(和右旋完全对称)
和图:avl-1-2-1完全对称

/**
     * 左旋(单旋转)
     * @param p
     * @return
     */
    private AVLEntry<K, V> rotateLeft(AVLEntry<K, V> p) {
        AVLEntry<K,V> right = p.right;
        p.right = right.left;
        right.left = p;
        p.height = Math.max(getHeight(p.left),getHeight(p.right)) + 1;
        right.height = Math.max(p.height,getHeight(right.right)) + 1;
        return right;//新的根节点
    }

双旋转,先左旋再右旋代码实现

avl-1-2-2
 /**
     * 先左旋再右旋
     * 先将p.left进行左旋,再将p进行右旋
     * @param p
     * @return
     */
    private AVLEntry<K,V> firstLeftThenRight(AVLEntry<K,V> p) {
        p.left = rotateLeft(p.left);
        p = rotateRight(p);
        return p;
    }

双旋转,先右旋再左旋代码实现
和图:avl-1-2-2完全对称

/**
     * 先右旋再左旋
     * 先将p.right进行右旋,再将p进行左旋
     * @param p
     * @return
     */
    private AVLEntry<K, V> firstRightThenLeft(AVLEntry<K, V> p) {
        p.right = rotateRight(p.right);
        p = rotateLeft(p);
        return p;
    }

旋转代码写完,下面实现插入平衡的代码,要实现插入调整树平衡,需要引入栈Stack,使用栈可以实现插入调整的非递归算法
private LinkedList<AVLEntry<K,V>> stack = new LinkedList<>();//用于实现插入调整的非递归算法

插入调整函数实现
插入的时候需要将其走的所有路径不断压栈

public V put(K key,V value) {
        if (root == null) {
            root = new AVLEntry<K,V>(key,value);
            stack.push(root);//需要将put走的路径全部压栈,为了fixAfterInsertion实现平衡
            size ++;
        }else{
            AVLEntry<K,V> p = root;
            while (p != null) {
                stack.push(p);//需要将put走的路径全部压栈,为了fixAfterInsertion实现平衡
                int cmp = compare(key,p.key);
                if (cmp < 0) {
                    if (p.left == null) {
                        p.left = new AVLEntry<K,V>(key,value);
                        stack.push(p.left);//需要将put走的路径全部压栈,为了fixAfterInsertion实现平衡
                        size ++;
                        break;
                    }else{
                        p = p.left;//再次循环比较
                    }
                } else if (cmp > 0) {
                    if (p.right == null) {
                        p.right = new AVLEntry<K,V>(key,value);
                        stack.push(p.right);//需要将put走的路径全部压栈,为了fixAfterInsertion实现平衡
                        size ++;
                        break;
                    }else{
                        p = p.right;
                    }
                }else{
                    p.setValue(value);//替换旧值
                    break;
                }
            }
        }
        fixAfterInsertion(key);
        //不管是插入的是新值还是重复值,都返回插入的值,这个和JDK TreeMap不一样
        return value;
    }
/**
     * 插入调整,使其二叉搜索树达到平衡
     * @param key
     */
    private void fixAfterInsertion(K key){
        AVLEntry<K,V> p = root;
        while (!stack.isEmpty()) {
            p = stack.pop();//插入所走的路径不断弹栈
            p.height = Math.max(getHeight(p.left),getHeight(p.right)) + 1;
            int d = getHeight(p.left) - getHeight(p.right);//计算平衡因子
            if (Math.abs(d) <= 1) { //改树平衡无需调整(旋转)
                continue;
            }else{
                if (d == 2) {
                    if (compare(key, p.left.key) < 0) { //插入到了左子树的左子树
                        p = rotateRight(p);//单旋转:右旋rotateRight
                    }else{//插入到了左子树的右子树
                        p = firstLeftThenRight(p); //双旋转:先左后右firstLeftThenRight
                    }
                }else{ //d == -2
                    if (compare(key, p.right.key) > 0) { //插入到了右子树的右子树
                        p = rotateLeft(p);//单旋转:左旋rotateLeft
                    }else{//插入到了右子树的左子树
                        p = firstRightThenLeft(p);//双旋转:先右后左firstRightThenLeft
                    }
                }
                //旋转过后,需要判断走的是左子树还是右子树,也就是检测爷爷结点,也就是p.parent要设置左子树还是右子树
                if (!stack.isEmpty()) {
                    if (compare(key, stack.peek().key) < 0) { //表明插入到了左子树
                        stack.peek().left = p;
                    }else{
                        stack.peek().right = p;
                    }
                }
            }
        }
        root = p;//重新设置根节点
     }

插入调整插入调整优化

/**
     * 插入调整,使其二叉搜索树达到平衡
     * @param key
     */
    private void fixAfterInsertion(K key){
        AVLEntry<K,V> p = root;
        while (!stack.isEmpty()) {
            p = stack.pop();//插入所走的路径不断弹栈
            //优化
            //**************************************************************
            int newHeight = Math.max(getHeight(p.left),getHeight(p.right)) + 1;
            if (p.height > 1 /*保证p不是叶子节点*/ && newHeight == p.height/*高度没有改变*/) {
                stack.clear();
                return;
            }
            //**************************************************************
            p.height = newHeight;//Math.max(getHeight(p.left),getHeight(p.right)) + 1;
            int d = getHeight(p.left) - getHeight(p.right);//计算平衡因子
            if (Math.abs(d) <= 1) { //改树平衡无需调整(旋转)
                continue;
            }else{
                if (d == 2) {
                    if (compare(key, p.left.key) < 0) { //插入到了左子树的左子树
                        p = rotateRight(p);//单旋转:右旋rotateRight
                    }else{//插入到了左子树的右子树
                        p = firstLeftThenRight(p); //双旋转:先左后右firstLeftThenRight
                    }
                }else{ //d == -2
                    if (compare(key, p.right.key) > 0) { //插入到了右子树的右子树
                        p = rotateLeft(p);//单旋转:左旋rotateLeft
                    }else{//插入到了右子树的左子树
                        p = firstRightThenLeft(p);//双旋转:先右后左firstRightThenLeft
                    }
                }
                //旋转过后,需要判断走的是左子树还是右子树,也就是检测爷爷结点,也就是p.parent要设置左子树还是右子树
                if (!stack.isEmpty()) {
                    if (compare(key, stack.peek().key) < 0) { //表明插入到了左子树
                        stack.peek().left = p;
                    }else{
                        stack.peek().right = p;
                    }
                }
            }
        }
        root = p;//重新设置根节点
     }

AVL插入平衡算法改进与时间复杂度分析
1,弹栈的时候,一旦发现某个节点的高度未发生改变,则立即停止回溯
2,指针回溯次数,最坏情况O(logN),最好情况O(1),平均任然是O(logN)
3,旋转次数,无旋转O(0),单旋转O(1),双旋转O(2),不会超过两次,平均O(1) (AVL树插入旋转不会超过两次)
4,时间复杂度:BST的插入O(logN) + 指针回溯O(logN) + 旋转O(1) = O(logN)
5,空间复杂度:有parent为O(1),无parent为O(logN)

插入平衡练习
将给定的排序数组转化为平衡二叉树,左右子树高度差的绝对值不超过1
实现方式1:使用AVLMap中的put实现方式
时间复杂度O(NlogN),空间复杂度O(N)

/**
 * 108(https://leetcode.com/problems/convert-sorted-array-to-binary-search-tree/)
 * 给定排序数组,将它转化为平衡二叉树
 * 要求左右子树高度差的绝对值不超过1(性质2)
 *
 * 实现方式1
 * AVLMap的put实现方式
 *
 */
public class ConvertSortedArrayToBinarySearchTree {

    class LeetCodeAVL{
        private int size;
        private TreeNode root;
        private LinkedList<TreeNode> stack = new LinkedList<>();

        public LeetCodeAVL() {
        }

        public int size() {
            return this.size;
        }

        public boolean isEmpty() {
            return this.size == 0;
        }

        public void put(int key) {
            if (root == null) {
                root = new TreeNode(key);
                stack.push(root);//需要将put走的路径全部压栈,为了fixAfterInsertion实现平衡
                size ++;
            }else{
                TreeNode p = root;
                while (p != null) {
                    stack.push(p);//需要将put走的路径全部压栈,为了fixAfterInsertion实现平衡
                    int cmp = key - p.val;
                    if (cmp < 0) {
                        if (p.left == null) {
                            p.left = new TreeNode(key);
                            size ++;
                            stack.push(p.left);//需要将put走的路径全部压栈,为了fixAfterInsertion实现平衡
                            break;
                        }else{
                            p = p.left;//再次循环比较
                        }
                    } else if (cmp > 0) {
                        if (p.right == null) {
                            p.right = new TreeNode(key);
                            size ++;
                            stack.push(p.right);//需要将put走的路径全部压栈,为了fixAfterInsertion实现平衡
                            break;
                        }else{
                            p = p.right;
                        }
                    }else{
                        break;
                    }
                }
            }
            fixAfterInsertion(key);
        }

        private HashMap<TreeNode,Integer> heightMap = new HashMap<>();

        /**
         * 返回一个结点的高度
         */
        public int getHeight(TreeNode p) {
            return heightMap.containsKey(p) ? heightMap.get(p):0;
        }


        /**
         * 右旋(单旋转)
         * 该方法需要右返回值,因为AVLEntry中没有parent指针(JDK中是有parent指针的,所以不需要有返回值),旋转之后它的新的根节点需要返回
         * @param p
         * @return
         */
        private TreeNode rotateRight(TreeNode p) {
            TreeNode left = p.left;
            p.left = left.right;
            left.right = p;
            heightMap.put(p,Math.max(getHeight(p.left),getHeight(p.right)) + 1);
            heightMap.put(left,Math.max(getHeight(left.left),getHeight(p)) + 1);
            return left;//新的根节点
        }

        /**
         * 左旋(单旋转)
         * @param p
         * @return
         */
        private TreeNode rotateLeft(TreeNode p) {
            TreeNode right = p.right;
            p.right = right.left;
            right.left = p;
            heightMap.put(p,Math.max(getHeight(p.left),getHeight(p.right)) + 1);
            heightMap.put(right,Math.max(getHeight(p),getHeight(right.right)) + 1);
            return right;//新的根节点
        }

        /**
         * 先左旋再右旋
         * 先将p.left进行左旋,再将p进行右旋
         * @param p
         * @return
         */
        private TreeNode firstLeftThenRight(TreeNode p) {
            p.left = rotateLeft(p.left);
            p = rotateRight(p);
            return p;
        }

        /**
         * 先右旋再左旋
         * 先将p.right进行右旋,再将p进行左旋
         * @param p
         * @return
         */
        private TreeNode firstRightThenLeft(TreeNode p) {
            p.right = rotateRight(p.right);
            p = rotateLeft(p);
            return p;
        }

        /**
         * 插入调整,使其二叉搜索树达到平衡
         * @param key
         */
        private void fixAfterInsertion(int key){
            TreeNode p = root;
            while (!stack.isEmpty()) {
                p = stack.pop();//插入所走的路径不断弹栈
                int newHeight = Math.max(getHeight(p.left),getHeight(p.right)) + 1;
                if (heightMap.containsKey(p) && getHeight(p) > 1 /*保证p不是叶子节点*/ && newHeight == getHeight(p)/*高度没有改变*/) {
                    stack.clear();
                    return;
                }
                heightMap.put(p,newHeight);//Math.max(getHeight(p.left),getHeight(p.right)) + 1;
                int d = getHeight(p.left) - getHeight(p.right);//计算平衡因子
                if (Math.abs(d) <= 1) { //改树平衡无需调整(旋转)
                    continue;
                }else{
                    if (d == 2) {
                        if (key - p.left.val < 0) { //插入到了左子树的左子树
                            p = rotateRight(p);//单旋转:右旋rotateRight
                        }else{//插入到了左子树的右子树
                            p = firstLeftThenRight(p); //双旋转:先左后右firstLeftThenRight
                        }
                    }else{ //d == -2
                        if (key - p.right.val > 0) { //插入到了右子树的右子树
                            p = rotateLeft(p);//单旋转:左旋rotateLeft
                        }else{//插入到了右子树的左子树
                            p = firstRightThenLeft(p);//双旋转:先右后左firstRightThenLeft
                        }
                    }
                    //旋转过后,需要判断走的是左子树还是右子树,也就是检测爷爷结点,也就是p.parent要设置左子树还是右子树
                    if (!stack.isEmpty()) {
                        if (key - stack.peek().val < 0) { //表明插入到了左子树
                            stack.peek().left = p;
                        }else{
                            stack.peek().right = p;
                        }
                    }
                }
            }
            root = p;//重新设置根节点
        }
    }

    public TreeNode sortedArrayToBST(int[] nums){
        if (nums == null || nums.length == 0) { //边界检测
            return null;
        }
        LeetCodeAVL avl = new LeetCodeAVL();
        for (int num : nums) {
            avl.put(num);
        }
        return avl.root;
    }
}

实现方式2:递归构建AVL + BST
参考TreeMap中的buildFromSorted
时间复杂度O(N),空间复杂度O(logN)
二分快排归并的递归算法实现方式二

avl-1-5-1
public class ConvertSortedArrayToBinarySearchTree {
   /**
     * 模仿TreeMap中的buildFromSorted
     * 时间复杂度O(N)
     * 空间复杂度O(logN)
     * @param nums
     * @return
     */
    public TreeNode sortedArrayToBST(int[] nums){
        if (nums == null || nums.length == 0) {
            return null;
        }
        return buildFromSorted(0,nums.length - 1,nums);
    }

    private TreeNode buildFromSorted(int lo, int hi, int[] nums) {
        if (hi < lo) {
            return null;
        }
        int mid = (lo + hi) / 2;
        TreeNode left = null;
        if (lo < mid) {
            left = buildFromSorted(lo, mid - 1, nums);
        }
        TreeNode middle = new TreeNode(nums[mid]);
        if (left != null) {
            middle.left = left;
        }
        if (mid < hi) {
            TreeNode right = buildFromSorted(mid + 1, hi, nums);
            middle.right = right;
        }
        return middle;
    }
}

计算完整二叉树的高度
JDK TreeMap源码中的通过节点个数计算树的层数,实现原理使用的是二分法
时间复杂度O(logN)

private static int computeRedLevel(int sz) {
        int level = 0;
        for (int m = sz - 1; m >= 0; m = m / 2 - 1)
            level++;
        return level;
    }

删除

AVL的删除
AVL的删除只需在BST的删除基础上加上删除平衡即可
1,类似插入,假设删除了p右子树的某个结点,引起了p的平衡因子d[p]=2,分析p的左子树left,三种情况如下:
    情况1:left的平衡因子d[left]=1,将p右旋  (图:avl-1-6-1)

avl-1-6-1


    情况2:left的平衡因子d[left]=0,将p右旋  (图:avl-1-6-2)

avl-1-6-2


    情况3:left的平衡因子d[left]=-1,先左旋left,再右旋p  (图:avl-1-6-3)

avl-1-6-3


2,删除左子树,即d[p]=-2的情况,与d[p]=2对称 

代码实现
删除节点后,调整该节点,使其整棵树保持平衡

/**
     * 删除调整
     * 1,类似插入,假设删除了p右子树的某个结点,引起了p的平衡因子d[p]=2,分析p的左子树left,三种情况如下:
     *     情况1:left的平衡因子d[left]=1,将p右旋
     *     情况2:left的平衡因子d[left]=0,将p右旋
     *     情况3:left的平衡因子d[left]=-1,先左旋left,再右旋p
     * 2,删除左子树,即d[p]=-2的情况,与d[p]=2对称
     *
     * 删除算法是递归的,所以该方法是在递归中调用的
     * @param p
     * @return
     */
    private AVLEntry<K, V> fixAfterDeletion(AVLEntry<K, V> p) {
        if (p == null) return null;
        else{
            p.height = Math.max(getHeight(p.left),getHeight(p.right)) + 1;
            int d = getHeight(p.left) - getHeight(p.right);
            if (d == 2) { //说明p.left一定不为null
                if (getHeight(p.left.left) - getHeight(p.left.right) >= 0) {
                    p = rotateRight(p);
                }else{
                    p = firstLeftThenRight(p);
                }
            } else if (d == -2) {//说明p.right一定不为null
                if (getHeight(p.right.right) - getHeight(p.right.left) >= 0) {
                    p = rotateLeft(p);
                }else{
                    p = firstRightThenLeft(p);
                }
            }
            return p;
        }
    }

源码:
https://github.com/xiaojinwei/java-learning/blob/master/src/com/cj/learn/tree/avl/AVLMap.java

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值