【算法笔记】有序表——AVL树

目录

《【算法笔记】有序表------AVL树》
《【算法笔记】有序表------SB树》
《【算法笔记】有序表------跳表》
《【算法笔记】有序表------相关题目》


1、算法概述

  • AVL树:AVL树是最早出现的自平衡二叉搜索树,它的核心特性在于维护一种严格的平衡条件:对于树中的任意节点,其左子树和右子树的高度差(平衡因子)绝对值不超过1。

  • 二叉搜索树(BST)的特点:

    • 1、不存在重复元素
    • 2、对于任意一个父节点P的元素值,小于的值放到其左孩子,大鱼的放到右孩子
    • 3、二叉搜索树中序遍历是从小到大排列好的
  • 特点:

    • 1、严格平衡:通过平衡因子(通常定义为右子树高度 - 左子树高度或左子树高度 - 右子树高度)实时监控每个节点的平衡状态,确保其值仅为-1、0或1。
    • 2、性能稳定:得益于平衡性,查找、插入和删除操作的时间复杂度在最坏情况下也能稳定在 O(log n),有效避免了普通二叉搜索树在数据有序插入时退化成链表(操作复杂度升至O(n))的风险。
    • 3、应用广泛:在对查询效率有严格要求或需要频繁动态更新的场景中作用关键,例如数据库索引(如MySQL的InnoDB引擎早期版本)、文件系统管理目录和元数据、内存管理以及网络路由表等。
  • 二叉树失衡的类型:

  • 二叉树失衡指的是二叉树在某个节点违法了其左子树和右子树的高度差(平衡因子)绝对值不超过1这个限定条件,其分为以下四种情况。

  • 1、LL型:左子树的左侧节点过长导致节点失衡。

  • 2、LR型:左子树的右侧节点过长导致节点失衡。

  • 3、RL型:右子树的左侧节点过长导致节点失衡。

  • 4、RR型:右子树的右侧节点过长导致节点失衡。

  • 平衡维护的旋转操作:比如当前节点记为C,左子节点为L,右子节点为R,

    • 1、左旋:在当前节点的位置将整棵树向左旋转一个位置。具体步骤如下:
    • 1.1、将R提升为新的子树的根节点
    • 1.2、将C作为R的左孩子
    • 1.3、将R原来的左子树作为C的新的右子树
    • 2、右旋:在当前节点的位置将整棵树向右旋转一个位置。具体步骤如下:
    • 2.1、将L提升为新的子树的根节点
    • 2.2、将C作为L的右子树
    • 2.3、将L原来的右子树作为C的新左子树。
  • 不同失衡情况的的旋转方式:

    • 1、单LL型:对当前节点C右旋一次
    • 2、单LR型:先对左孩子L进行一次左旋操作,然后对当前节点C做一次右旋操作。
    • 3、同时存在LL和LR型时:和单LL型一样,对当前节点C进行一次右旋操作即可。
    • 4、单RL型:先对右孩子R进行一次右旋操作,然后对当前节点C进行一次左旋操作。
    • 5、单RR型:对当前节点C左旋一次
    • 6、同时存在RL和RR型时:和单RR型一样,对当前节点C进行一次左旋即可。
  • 插入节点的操作和平衡性的维持:

    • 插入节点时,先根据BST的性质找到需要插入的位置,然后需要从插入位置到根节点root的这一条链进行回溯调整平衡。
    • 在具体的操作中,我们会实现一个add方法,从根节点开始调用add方法,在add中根据条件用其左孩子或者右孩子作为参数递归调用add方法,然后调整平衡,
    • 因为add每次都会调用维持平衡的maintain方法,根据递归调用的特定,会自动从插入节点到根节点反向调整好整个调用链。
  • 删除节点的操作和平衡性的维持:

    • 删除节点时,先找到目标节点。
    • 1)若无子节点,直接删除;
    • 2)若有一个子节点,用子节点替代;
    • 3)若有两个子节点,通常用其左子树的最大值或右子树的最小值(中序遍历前驱或后继)替代,然后递归删除那个替代节点。
    • 在具体的实现中,同样也是实现一个delete方法,从根节点开始调用,完成删除逻辑后调用maintain方法调整平衡,根据递归的原理,会自动进行回溯调整影响节点到根节点的平衡性。
    • 在插入和删除节点的操作中,都是需要将从影响的节点依次回溯到根节点,一个一个进行平衡性的调整的,通常实现是利用递归的性质来实现这种调整,
    • 因为递归性质实现比较简单,而且因为递归调用是logN的操作,不会有很深的深度,所以复杂性也不会太高。
  • 时间复杂度

    • 1、查找:O(log n)
    • 2、插入:O(log n)(包括查找位置和最多一次旋转调整)
    • 3、删除:O(log n)(可能需要从删除点回溯到根节点并进行最多O(log n)次旋转)
  • 优缺点比较

    • 1、优点:
    • 提供严格平衡,查询效率极高,尤其适合查询操作远多于插入/删除操作或对查询性能有严格要求的场景。
    • 2、缺点:
    • 2.1、插入和删除操作,特别是删除,可能需要执行多次旋转,开销相对较大。
    • 2.2、需要额外空间存储平衡因子或高度信息。
    • 2.3、与红黑树等"近似平衡"的二叉搜索树相比,在插入删除频繁的场景中,AVL树为维持严格平衡所付出的代价可能更高。

2、利用AVL树实现的自定义map

java 复制代码
     /**
     * 利用AVL树实现的自定义map
     */
    public static class AvlTreeMap<K extends Comparable<K>, V> {

        // 根节点
        private Node<K, V> root;
        private int size;

        public AvlTreeMap() {
            this.root = null;
            this.size = 0;
        }

        /**
         * 获取节点个数
         */
        public int size() {
            return this.size;
        }

        /**
         * 判断是否包含某个key的节点
         */
        public boolean containsKey(K key) {
            if (key == null) {
                return false;
            }
            Node<K, V> node = findNode(key);
            return node != null;
        }

        /**
         * 增加或者更新值
         * 如果key存在,就更新value,不存在就增加
         */
        public void put(K key, V value) {
            if (key == null) {
                return;
            }
            // 根据key查询
            Node<K, V> node = findNode(key);
            if (node != null) {
                // 存在,更新值即可
                node.value = value;
                return;
            }
            // 不存在就调用add方法更新
            this.root = add(this.root, key, value);
            this.size++;
        }

        /**
         * 移除某个节点
         */
        public void remove(K key) {
            if (key == null) {
                return;
            }
            // 存在的情况调用delete方法删除
            if (containsKey(key)) {
                size--;
                root = delete(root, key);
            }
        }

        /**
         * 根据key获取值,如果不存在,返回null
         */
        public V get(K key) {
            if (key == null) {
                return null;
            }
            Node<K, V> node = findNode(key);
            if (node == null) {
                return null;
            }
            return node.value;
        }

        /**
         * 获得最小的 key
         */
        public K firstKey() {
            if (root == null) {
                return null;
            }
            Node<K, V> cur = root;
            while (cur.left != null) {
                cur = cur.left;
            }
            return cur.key;
        }

        /**
         * 获得最大的 key
         */
        public K lastKey() {
            if (root == null) {
                return null;
            }
            Node<K, V> cur = root;
            while (cur.right != null) {
                cur = cur.right;
            }
            return cur.key;
        }

        /**
         * 获得小于等于 key的最大的数
         */
        public K floorKey(K key) {
            if (key == null) {
                return null;
            }
            Node<K, V> lastNoBigNode = findLastNoBig(key);
            return lastNoBigNode == null ? null : lastNoBigNode.key;
        }

        /**
         * 获得大于等于 key的最小的数
         */
        public K ceilingKey(K key) {
            if (key == null) {
                return null;
            }
            Node<K, V> lastNoSmallNode = findLastNoSmall(key);
            return lastNoSmallNode == null ? null : lastNoSmallNode.key;
        }

        /**
         * 对指定节点cur进行左旋,返回新的子树根节点
         * 左旋:在当前节点的位置将整棵树向左旋转一个位置。具体步骤如下:
         * 1、将R提升为新的子树的根节点
         * 2、将C作为R的左孩子
         * 3、将R原来的左子树作为C的新的右子树
         */
        private Node<K, V> leftRotate(Node<K, V> cur) {
            if (cur == null || cur.right == null) {
                return cur;
            }
            Node<K, V> right = cur.right;
            // 先将 right的左孩子给cur的右孩子
            cur.right = right.left;
            // 将 cur作为right的左孩子
            right.left = cur;
            // 更新高度,因为此时right已经是新的根节点了,所以先更新cur的高度,再更新right的高度
            cur.height = Math.max((cur.left != null ? cur.left.height : 0), (cur.right != null ? cur.right.height : 0)) + 1;
            right.height = Math.max(right.left.height, (right.right != null ? right.right.height : 0)) + 1;
            // 返回新的子树的根节点
            return right;
        }

        /**
         * 对指定节点cur进行右旋,返回新的子树根节点
         * 右旋:在当前节点的位置将整棵树向右旋转一个位置。具体步骤如下:
         * 1、将L提升为新的子树的根节点
         * 2、将C作为L的右子树
         * 3、将L原来的右子树作为C的新左子树
         */
        private Node<K, V> rightRotate(Node<K, V> cur) {
            if (cur == null || cur.left == null) {
                return cur;
            }
            Node<K, V> left = cur.left;
            // 先将 left的右节点给cur的左孩子
            cur.left = left.right;
            // 将 cur作为left的右孩子
            left.right = cur;
            // 更新高度,因为此时left为根节点,所以先更新cur节点,再更新left节点
            cur.height = Math.max((cur.left != null ? cur.left.height : 0), (cur.right != null ? cur.right.height : 0)) + 1;
            left.height = Math.max((left.left != null ? left.left.height : 0), left.right.height) + 1;
            // 返回新的子树的根节点
            return left;
        }

        /**
         * 调整指定节点 cur的平衡性,并返回新的节点引用
         * 因为调整完以后 cur位置的节点有可能被别的代替,这种情况下需要返回新的节点的引用
         */
        private Node<K, V> maintain(Node<K, V> cur) {
            if (cur == null) {
                return null;
            }
            int leftHeight = cur.left != null ? cur.left.height : 0;
            int rightHeight = cur.right != null ? cur.right.height : 0;
            // 如果平衡,直接返回
            if (Math.abs(leftHeight - rightHeight) <= 1) {
                return cur;
            }
            // 根据不平衡的类型来进行调整
            if (leftHeight > rightHeight) {
                // 左子树高度比较大的情况,需要区分是 LL型还是 LR型
                int leftLeftHeight = cur.left != null && cur.left.left != null ? cur.left.left.height : 0;
                int leftRightHeight = cur.left != null && cur.left.right != null ? cur.left.right.height : 0;
                // LL型、LL和LR同时存在的时候,根据LL型判断,右旋即可
                if (leftLeftHeight >= leftRightHeight) {
                    return rightRotate(cur);
                }
                // LR单独存在,先将左子树左旋,再将当前节点右旋
                cur.left = leftRotate(cur.left);
                return rightRotate(cur);
            }
            // 到这里,说明是右子树高度较高,区分RL和RR型
            int rightLeftHeight = cur.right != null && cur.right.left != null ? cur.right.left.height : 0;
            int rightRightHeight = cur.right != null && cur.right.right != null ? cur.right.right.height : 0;
            // RR、RL和RR都存在的时候,按照RR处理,左旋即可
            if (rightRightHeight >= rightLeftHeight) {
                return leftRotate(cur);
            }
            // 如果是RL型,先对右节点进行一次右旋,然后对cur进行一次左旋
            cur.right = rightRotate(cur.right);
            return leftRotate(cur);
        }

        /**
         * 根据指定的key查找节点
         * 如果存在就返回那个节点,不存在就返回null
         */
        private Node<K, V> findNode(K key) {
            if (key == null) {
                return null;
            }
            Node<K, V> cur = this.root;
            while (cur != null) {
                int cmt = key.compareTo(cur.key);
                if (cmt == 0) {
                    // 值相等,直接返回
                    return cur;
                }
                if (cmt < 0) {
                    // key比当前的值小,需要在左侧进行查找
                    cur = cur.left;
                } else {
                    // key比当前值大,需要在右侧进行查找
                    cur = cur.right;
                }
            }
            // 到这里,说明挑出了while循环,此时cur == null,说明没找到
            return cur;
        }

        /**
         * 用递归的方式给指定子树cur添加元素
         * 这里指定的cur是整个子树的头结点,需要在add函数中判断添加到具体的哪个位置
         * 这里用这种递归的实现方式,完成了从添加位置到根节点的回溯调整平衡的功能。
         */
        private Node<K, V> add(Node<K, V> cur, K key, V value) {
            if (cur == null) {
                // 要添加的子树为null,说明当前值就是添加到这个位置,返回一个新节点
                return new Node<>(key, value);
            }
            // 当前节点不为空,需要跟觉BST的规则继续递归往下面的子树去添加
            int cmt = key.compareTo(cur.key);
            // 调用add方法的函数已经判断过已经存在的情况,所以这种情况就不需要再判断一次
            if (cmt == 0) {
                // key相同,直接更新
                cur.value = value;
                return cur;
            }
            if (cmt < 0) {
                // key的值比当前值小,需要往左子树添加
                cur.left = add(cur.left, key, value);
            } else {
                // key的值比当前值大,需要往右子树添加
                cur.right = add(cur.right, key, value);
            }
            // 调整完重新调整高度和平衡
            cur.height = Math.max(cur.left != null ? cur.left.height : 0, cur.right != null ? cur.right.height : 0) + 1;
            return maintain(cur);
        }

        /**
         * 用递归的方式在指定子树cur删除节点
         * 这里是在cur为头的子树上删除节点,并不是删除cur节点,需要在delete函数中具体判断删除哪个节点。
         * 这里用这种递归的方式,就能完成影响节点的反向回溯调整平衡的操作。
         * 找到节点后删除该节点的逻辑:
         * 1)若无子节点,直接删除;
         * 2)若有一个子节点,用子节点替代;
         * 3)若有两个子节点,通常用其左子树的最大值或右子树的最小值(中序遍历前驱或后继)替代,然后递归删除那个替代节点。
         */
        private Node<K, V> delete(Node<K, V> cur, K key) {
            if (cur == null || key == null) {
                return cur;
            }
            int cmt = key.compareTo(cur.key);
            if (cmt < 0) {
                // key比当前值小,从左侧子树删除
                cur.left = delete(cur.left, key);
            } else if (cmt > 0) {
                // key比当前值大,从右侧子树删除
                cur.right = delete(cur.right, key);
            } else {
                // 相等,说明删除的就是当前节点,根据删除节点的逻辑删除
                if (cur.left == null && cur.right == null) {
                    // 无子节点,直接删除当前节点
                    cur = null;
                } else if (cur.left == null) {
                    // 两个节点不是同时没有,如果left没有,那只能是right有,用right代替
                    cur = cur.right;
                } else if (cur.right == null) {
                    // 两个节点不是同时没有,如果right没有,那只能是left有,用left代替
                    cur = cur.left;
                } else {
                    // 两个节点都有,用右子树中的最小值来替代,然后递归删除那个节点
                    Node<K, V> des = cur.right;
                    while (des.left != null) {
                        // 找到右子树的最左侧节点,就是右树中的最小值
                        des = des.left;
                    }
                    // 在右侧树中删除des节点,同时做了平衡性调整,然后用des替换掉cur
                    // 这样做的效果和用des替换cur一样,同时已经调整了右树,只需要从当前位置往上调整即可
                    cur.right = delete(cur.right, des.key);
                    // 用des替换cur节点,不能只用cur=des,内部的左右指针没有变,当然这里也可以直接替换value即可
                    des.left = cur.left;
                    des.right = cur.right;
                    cur = des;
                }
            }
            if (cur == null) {
                // 当前节点被删除了,不需要调整了
                return null;
            }
            // 调整高度和平衡度
            cur.height = Math.max(cur.left != null ? cur.left.height : 0, cur.right != null ? cur.right.height : 0) + 1;
            return maintain(cur);
        }

        /**
         * 查找最后一个不小于指定key的节点
         * 不小于指定key指的是离key最近的大于等于key的值的节点
         * 方法:
         * 按照BST的方式进行查找,相等的时候直接返回,
         * 往左侧查找的时候,先用一个变量将当前节点记录下来,因为当前节点比要查找的大,
         * 这样挑出循环,说明没找到相等的,变量记录的就是比key大的最近的节点
         */
        private Node<K, V> findLastNoSmall(K key) {
            if (key == null) {
                return null;
            }
            Node<K, V> ans = null;
            Node<K, V> cur = root;
            while (cur != null) {
                int cmt = key.compareTo(cur.key);
                if (cmt == 0) {
                    // 相等,直接返回当前节点
                    return cur;
                }
                if (cmt < 0) {
                    // key 比当前值小,先记录当前值,再从左侧查找
                    ans = cur;
                    cur = cur.left;
                } else {
                    cur = cur.right;
                }
            }
            // 到这里,说明没有找到相等的,返回记录的值
            return ans;
        }

        /**
         * 查找最后一个不大于指定key的节点
         * 不大于指定key的意思是小于等于key
         * 方法:
         * 按照BST的方法查找,
         * 找到相等的,直接返回
         * 没有相等时,在往右侧查找的时候,用一个变量记录当前的值,当前值小于key
         * 这样到最后,返回变量记录的节点,就是小于key最近的节点
         */
        private Node<K, V> findLastNoBig(K key) {
            if (key == null) {
                return null;
            }
            Node<K, V> ans = null;
            Node<K, V> cur = root;
            while (cur != null) {
                int cmt = key.compareTo(cur.key);
                if (cmt == 0) {
                    // 相等,直接返回当前节点
                    return cur;
                }
                if (cmt < 0) {
                    cur = cur.left;
                } else {
                    // key 比当前值大,先记录当前值,再从右侧查找
                    ans = cur;
                    cur = cur.right;
                }
            }
            // 到这里,说明没有找到相等的,返回记录的值
            return ans;
        }

        /**
         * 节点类
         */
        static class Node<K extends Comparable<K>, V> {
            private final K key;
            private V value;
            private Node<K, V> left;
            private Node<K, V> right;
            public int height;

            public Node(K key, V value) {
                this.key = key;
                this.value = value;
                this.height = 1;
            }
        }
    }

整体代码和测试:

java 复制代码
import java.util.Objects;
import java.util.TreeMap;

/**
 * AVL树:AVL树是最早出现的自平衡二叉搜索树,它的核心特性在于维护一种严格的平衡条件:对于树中的任意节点,其左子树和右子树的高度差(平衡因子)绝对值不超过1。
 * <br>
 * 二叉搜索树(BST)的特点:
 * 1、不存在重复元素
 * 2、对于任意一个父节点P的元素值,小于的值放到其左孩子,大鱼的放到右孩子
 * 3、二叉搜索树中序遍历是从小到大排列好的
 * <br>
 * 特点:
 * 1、严格平衡:通过平衡因子(通常定义为右子树高度 - 左子树高度或左子树高度 - 右子树高度)实时监控每个节点的平衡状态,确保其值仅为-1、0或1。
 * 2、性能稳定:得益于平衡性,查找、插入和删除操作的时间复杂度在最坏情况下也能稳定在 O(log n),有效避免了普通二叉搜索树在数据有序插入时退化成链表(操作复杂度升至O(n))的风险。
 * 3、应用广泛:在对查询效率有严格要求或需要频繁动态更新的场景中作用关键,例如数据库索引(如MySQL的InnoDB引擎早期版本)、文件系统管理目录和元数据、内存管理以及网络路由表等。
 * <br>
 * 二叉树失衡的类型:
 * 二叉树失衡指的是二叉树在某个节点违法了其左子树和右子树的高度差(平衡因子)绝对值不超过1这个限定条件,其分为以下四种情况。
 * 1、LL型:左子树的左侧节点过长导致节点失衡。
 * 2、LR型:左子树的右侧节点过长导致节点失衡。
 * 3、RL型:右子树的左侧节点过长导致节点失衡。
 * 4、RR型:右子树的右侧节点过长导致节点失衡。
 * <br>
 * 平衡维护的旋转操作:比如当前节点记为C,左子节点为L,右子节点为R,
 * 1、左旋:在当前节点的位置将整棵树向左旋转一个位置。具体步骤如下:
 * 1.1、将R提升为新的子树的根节点
 * 1.2、将C作为R的左孩子
 * 1.3、将R原来的左子树作为C的新的右子树
 * 2、右旋:在当前节点的位置将整棵树向右旋转一个位置。具体步骤如下:
 * 2.1、将L提升为新的子树的根节点
 * 2.2、将C作为L的右子树
 * 2.3、将L原来的右子树作为C的新左子树。
 * <br>
 * 不同失衡情况的的旋转方式:
 * 1、单LL型:对当前节点C右旋一次
 * 2、单LR型:先对左孩子L进行一次左旋操作,然后对当前节点C做一次右旋操作。
 * 3、同时存在LL和LR型时:和单LL型一样,对当前节点C进行一次右旋操作即可。
 * 4、单RL型:先对右孩子R进行一次右旋操作,然后对当前节点C进行一次左旋操作。
 * 5、单RR型:对当前节点C左旋一次
 * 6、同时存在RL和RR型时:和单RR型一样,对当前节点C进行一次左旋即可。
 * <br>
 * 插入节点的操作和平衡性的维持:
 * 插入节点时,先根据BST的性质找到需要插入的位置,然后需要从插入位置到根节点root的这一条链进行回溯调整平衡。
 * 在具体的操作中,我们会实现一个add方法,从根节点开始调用add方法,在add中根据条件用其左孩子或者右孩子作为参数递归调用add方法,然后调整平衡,
 * 因为add每次都会调用维持平衡的maintain方法,根据递归调用的特定,会自动从插入节点到根节点反向调整好整个调用链。
 * <br>
 * 删除节点的操作和平衡性的维持:
 * 删除节点时,先找到目标节点。
 * 1)若无子节点,直接删除;
 * 2)若有一个子节点,用子节点替代;
 * 3)若有两个子节点,通常用其左子树的最大值或右子树的最小值(中序遍历前驱或后继)替代,然后递归删除那个替代节点。
 * 在具体的实现中,同样也是实现一个delete方法,从根节点开始调用,完成删除逻辑后调用maintain方法调整平衡,根据递归的原理,会自动进行回溯调整影响节点到根节点的平衡性。
 * 在插入和删除节点的操作中,都是需要将从影响的节点依次回溯到根节点,一个一个进行平衡性的调整的,通常实现是利用递归的性质来实现这种调整,
 * 因为递归性质实现比较简单,而且因为递归调用是logN的操作,不会有很深的深度,所以复杂性也不会太高。
 * <br>
 * 时间复杂度
 * 1、查找:O(log n)
 * 2、插入:O(log n)(包括查找位置和最多一次旋转调整)
 * 3、删除:O(log n)(可能需要从删除点回溯到根节点并进行最多O(log n)次旋转)
 * <br>
 * 优缺点比较
 * 1、优点:
 * 提供严格平衡,查询效率极高,尤其适合查询操作远多于插入/删除操作或对查询性能有严格要求的场景。
 * 2、缺点:
 * 2.1、插入和删除操作,特别是删除,可能需要执行多次旋转,开销相对较大。
 * 2.2、需要额外空间存储平衡因子或高度信息。
 * 2.3、与红黑树等"近似平衡"的二叉搜索树相比,在插入删除频繁的场景中,AVL树为维持严格平衡所付出的代价可能更高。
 */
public class AvlTree {

    /**
     * 利用AVL树实现的自定义map
     */
    public static class AvlTreeMap<K extends Comparable<K>, V> {

        // 根节点
        private Node<K, V> root;
        private int size;

        public AvlTreeMap() {
            this.root = null;
            this.size = 0;
        }

        /**
         * 获取节点个数
         */
        public int size() {
            return this.size;
        }

        /**
         * 判断是否包含某个key的节点
         */
        public boolean containsKey(K key) {
            if (key == null) {
                return false;
            }
            Node<K, V> node = findNode(key);
            return node != null;
        }

        /**
         * 增加或者更新值
         * 如果key存在,就更新value,不存在就增加
         */
        public void put(K key, V value) {
            if (key == null) {
                return;
            }
            // 根据key查询
            Node<K, V> node = findNode(key);
            if (node != null) {
                // 存在,更新值即可
                node.value = value;
                return;
            }
            // 不存在就调用add方法更新
            this.root = add(this.root, key, value);
            this.size++;
        }

        /**
         * 移除某个节点
         */
        public void remove(K key) {
            if (key == null) {
                return;
            }
            // 存在的情况调用delete方法删除
            if (containsKey(key)) {
                size--;
                root = delete(root, key);
            }
        }

        /**
         * 根据key获取值,如果不存在,返回null
         */
        public V get(K key) {
            if (key == null) {
                return null;
            }
            Node<K, V> node = findNode(key);
            if (node == null) {
                return null;
            }
            return node.value;
        }

        /**
         * 获得最小的 key
         */
        public K firstKey() {
            if (root == null) {
                return null;
            }
            Node<K, V> cur = root;
            while (cur.left != null) {
                cur = cur.left;
            }
            return cur.key;
        }

        /**
         * 获得最大的 key
         */
        public K lastKey() {
            if (root == null) {
                return null;
            }
            Node<K, V> cur = root;
            while (cur.right != null) {
                cur = cur.right;
            }
            return cur.key;
        }

        /**
         * 获得小于等于 key的最大的数
         */
        public K floorKey(K key) {
            if (key == null) {
                return null;
            }
            Node<K, V> lastNoBigNode = findLastNoBig(key);
            return lastNoBigNode == null ? null : lastNoBigNode.key;
        }

        /**
         * 获得大于等于 key的最小的数
         */
        public K ceilingKey(K key) {
            if (key == null) {
                return null;
            }
            Node<K, V> lastNoSmallNode = findLastNoSmall(key);
            return lastNoSmallNode == null ? null : lastNoSmallNode.key;
        }

        /**
         * 对指定节点cur进行左旋,返回新的子树根节点
         * 左旋:在当前节点的位置将整棵树向左旋转一个位置。具体步骤如下:
         * 1、将R提升为新的子树的根节点
         * 2、将C作为R的左孩子
         * 3、将R原来的左子树作为C的新的右子树
         */
        private Node<K, V> leftRotate(Node<K, V> cur) {
            if (cur == null || cur.right == null) {
                return cur;
            }
            Node<K, V> right = cur.right;
            // 先将 right的左孩子给cur的右孩子
            cur.right = right.left;
            // 将 cur作为right的左孩子
            right.left = cur;
            // 更新高度,因为此时right已经是新的根节点了,所以先更新cur的高度,再更新right的高度
            cur.height = Math.max((cur.left != null ? cur.left.height : 0), (cur.right != null ? cur.right.height : 0)) + 1;
            right.height = Math.max(right.left.height, (right.right != null ? right.right.height : 0)) + 1;
            // 返回新的子树的根节点
            return right;
        }

        /**
         * 对指定节点cur进行右旋,返回新的子树根节点
         * 右旋:在当前节点的位置将整棵树向右旋转一个位置。具体步骤如下:
         * 1、将L提升为新的子树的根节点
         * 2、将C作为L的右子树
         * 3、将L原来的右子树作为C的新左子树
         */
        private Node<K, V> rightRotate(Node<K, V> cur) {
            if (cur == null || cur.left == null) {
                return cur;
            }
            Node<K, V> left = cur.left;
            // 先将 left的右节点给cur的左孩子
            cur.left = left.right;
            // 将 cur作为left的右孩子
            left.right = cur;
            // 更新高度,因为此时left为根节点,所以先更新cur节点,再更新left节点
            cur.height = Math.max((cur.left != null ? cur.left.height : 0), (cur.right != null ? cur.right.height : 0)) + 1;
            left.height = Math.max((left.left != null ? left.left.height : 0), left.right.height) + 1;
            // 返回新的子树的根节点
            return left;
        }

        /**
         * 调整指定节点 cur的平衡性,并返回新的节点引用
         * 因为调整完以后 cur位置的节点有可能被别的代替,这种情况下需要返回新的节点的引用
         */
        private Node<K, V> maintain(Node<K, V> cur) {
            if (cur == null) {
                return null;
            }
            int leftHeight = cur.left != null ? cur.left.height : 0;
            int rightHeight = cur.right != null ? cur.right.height : 0;
            // 如果平衡,直接返回
            if (Math.abs(leftHeight - rightHeight) <= 1) {
                return cur;
            }
            // 根据不平衡的类型来进行调整
            if (leftHeight > rightHeight) {
                // 左子树高度比较大的情况,需要区分是 LL型还是 LR型
                int leftLeftHeight = cur.left != null && cur.left.left != null ? cur.left.left.height : 0;
                int leftRightHeight = cur.left != null && cur.left.right != null ? cur.left.right.height : 0;
                // LL型、LL和LR同时存在的时候,根据LL型判断,右旋即可
                if (leftLeftHeight >= leftRightHeight) {
                    return rightRotate(cur);
                }
                // LR单独存在,先将左子树左旋,再将当前节点右旋
                cur.left = leftRotate(cur.left);
                return rightRotate(cur);
            }
            // 到这里,说明是右子树高度较高,区分RL和RR型
            int rightLeftHeight = cur.right != null && cur.right.left != null ? cur.right.left.height : 0;
            int rightRightHeight = cur.right != null && cur.right.right != null ? cur.right.right.height : 0;
            // RR、RL和RR都存在的时候,按照RR处理,左旋即可
            if (rightRightHeight >= rightLeftHeight) {
                return leftRotate(cur);
            }
            // 如果是RL型,先对右节点进行一次右旋,然后对cur进行一次左旋
            cur.right = rightRotate(cur.right);
            return leftRotate(cur);
        }

        /**
         * 根据指定的key查找节点
         * 如果存在就返回那个节点,不存在就返回null
         */
        private Node<K, V> findNode(K key) {
            if (key == null) {
                return null;
            }
            Node<K, V> cur = this.root;
            while (cur != null) {
                int cmt = key.compareTo(cur.key);
                if (cmt == 0) {
                    // 值相等,直接返回
                    return cur;
                }
                if (cmt < 0) {
                    // key比当前的值小,需要在左侧进行查找
                    cur = cur.left;
                } else {
                    // key比当前值大,需要在右侧进行查找
                    cur = cur.right;
                }
            }
            // 到这里,说明挑出了while循环,此时cur == null,说明没找到
            return cur;
        }

        /**
         * 用递归的方式给指定子树cur添加元素
         * 这里指定的cur是整个子树的头结点,需要在add函数中判断添加到具体的哪个位置
         * 这里用这种递归的实现方式,完成了从添加位置到根节点的回溯调整平衡的功能。
         */
        private Node<K, V> add(Node<K, V> cur, K key, V value) {
            if (cur == null) {
                // 要添加的子树为null,说明当前值就是添加到这个位置,返回一个新节点
                return new Node<>(key, value);
            }
            // 当前节点不为空,需要跟觉BST的规则继续递归往下面的子树去添加
            int cmt = key.compareTo(cur.key);
            // 调用add方法的函数已经判断过已经存在的情况,所以这种情况就不需要再判断一次
            if (cmt == 0) {
                // key相同,直接更新
                cur.value = value;
                return cur;
            }
            if (cmt < 0) {
                // key的值比当前值小,需要往左子树添加
                cur.left = add(cur.left, key, value);
            } else {
                // key的值比当前值大,需要往右子树添加
                cur.right = add(cur.right, key, value);
            }
            // 调整完重新调整高度和平衡
            cur.height = Math.max(cur.left != null ? cur.left.height : 0, cur.right != null ? cur.right.height : 0) + 1;
            return maintain(cur);
        }

        /**
         * 用递归的方式在指定子树cur删除节点
         * 这里是在cur为头的子树上删除节点,并不是删除cur节点,需要在delete函数中具体判断删除哪个节点。
         * 这里用这种递归的方式,就能完成影响节点的反向回溯调整平衡的操作。
         * 找到节点后删除该节点的逻辑:
         * 1)若无子节点,直接删除;
         * 2)若有一个子节点,用子节点替代;
         * 3)若有两个子节点,通常用其左子树的最大值或右子树的最小值(中序遍历前驱或后继)替代,然后递归删除那个替代节点。
         */
        private Node<K, V> delete(Node<K, V> cur, K key) {
            if (cur == null || key == null) {
                return cur;
            }
            int cmt = key.compareTo(cur.key);
            if (cmt < 0) {
                // key比当前值小,从左侧子树删除
                cur.left = delete(cur.left, key);
            } else if (cmt > 0) {
                // key比当前值大,从右侧子树删除
                cur.right = delete(cur.right, key);
            } else {
                // 相等,说明删除的就是当前节点,根据删除节点的逻辑删除
                if (cur.left == null && cur.right == null) {
                    // 无子节点,直接删除当前节点
                    cur = null;
                } else if (cur.left == null) {
                    // 两个节点不是同时没有,如果left没有,那只能是right有,用right代替
                    cur = cur.right;
                } else if (cur.right == null) {
                    // 两个节点不是同时没有,如果right没有,那只能是left有,用left代替
                    cur = cur.left;
                } else {
                    // 两个节点都有,用右子树中的最小值来替代,然后递归删除那个节点
                    Node<K, V> des = cur.right;
                    while (des.left != null) {
                        // 找到右子树的最左侧节点,就是右树中的最小值
                        des = des.left;
                    }
                    // 在右侧树中删除des节点,同时做了平衡性调整,然后用des替换掉cur
                    // 这样做的效果和用des替换cur一样,同时已经调整了右树,只需要从当前位置往上调整即可
                    cur.right = delete(cur.right, des.key);
                    // 用des替换cur节点,不能只用cur=des,内部的左右指针没有变,当然这里也可以直接替换value即可
                    des.left = cur.left;
                    des.right = cur.right;
                    cur = des;
                }
            }
            if (cur == null) {
                // 当前节点被删除了,不需要调整了
                return null;
            }
            // 调整高度和平衡度
            cur.height = Math.max(cur.left != null ? cur.left.height : 0, cur.right != null ? cur.right.height : 0) + 1;
            return maintain(cur);
        }

        /**
         * 查找最后一个不小于指定key的节点
         * 不小于指定key指的是离key最近的大于等于key的值的节点
         * 方法:
         * 按照BST的方式进行查找,相等的时候直接返回,
         * 往左侧查找的时候,先用一个变量将当前节点记录下来,因为当前节点比要查找的大,
         * 这样挑出循环,说明没找到相等的,变量记录的就是比key大的最近的节点
         */
        private Node<K, V> findLastNoSmall(K key) {
            if (key == null) {
                return null;
            }
            Node<K, V> ans = null;
            Node<K, V> cur = root;
            while (cur != null) {
                int cmt = key.compareTo(cur.key);
                if (cmt == 0) {
                    // 相等,直接返回当前节点
                    return cur;
                }
                if (cmt < 0) {
                    // key 比当前值小,先记录当前值,再从左侧查找
                    ans = cur;
                    cur = cur.left;
                } else {
                    cur = cur.right;
                }
            }
            // 到这里,说明没有找到相等的,返回记录的值
            return ans;
        }

        /**
         * 查找最后一个不大于指定key的节点
         * 不大于指定key的意思是小于等于key
         * 方法:
         * 按照BST的方法查找,
         * 找到相等的,直接返回
         * 没有相等时,在往右侧查找的时候,用一个变量记录当前的值,当前值小于key
         * 这样到最后,返回变量记录的节点,就是小于key最近的节点
         */
        private Node<K, V> findLastNoBig(K key) {
            if (key == null) {
                return null;
            }
            Node<K, V> ans = null;
            Node<K, V> cur = root;
            while (cur != null) {
                int cmt = key.compareTo(cur.key);
                if (cmt == 0) {
                    // 相等,直接返回当前节点
                    return cur;
                }
                if (cmt < 0) {
                    cur = cur.left;
                } else {
                    // key 比当前值大,先记录当前值,再从右侧查找
                    ans = cur;
                    cur = cur.right;
                }
            }
            // 到这里,说明没有找到相等的,返回记录的值
            return ans;
        }

        /**
         * 节点类
         */
        static class Node<K extends Comparable<K>, V> {
            private final K key;
            private V value;
            private Node<K, V> left;
            private Node<K, V> right;
            public int height;

            public Node(K key, V value) {
                this.key = key;
                this.value = value;
                this.height = 1;
            }
        }
    }


    public static void main(String[] args) {
        functionTest("avlTreeMap");
        System.out.println("======");
        performanceTest("avlTreeMap");
    }


    public static void functionTest(String prefix) {
        System.out.println(prefix + " 功能测试开始");
        TreeMap<Integer, Integer> treeMap = new TreeMap<>();
        AvlTreeMap<Integer, Integer> target = new AvlTreeMap<>();
        int maxK = 500;
        int maxV = 50000;
        int testTime = 1000000;
        boolean success = true;
        for (int i = 0; i < testTime; i++) {
            int addK = (int) (Math.random() * maxK);
            int addV = (int) (Math.random() * maxV);
            treeMap.put(addK, addV);
            target.put(addK, addV);

            int removeK = (int) (Math.random() * maxK);
            treeMap.remove(removeK);
            target.remove(removeK);

            int querryK = (int) (Math.random() * maxK);
            boolean treeMapAns = treeMap.containsKey(querryK);
            boolean ans = target.containsKey(querryK);
            if (treeMapAns != ans) {
                System.out.println("containsKey 错误");
                System.out.printf("key:%d, treeMapAns:%b, ans:%b\n", querryK, treeMapAns, ans);
                success = false;
                break;
            }

            if (treeMap.containsKey(querryK)) {
                int v1 = treeMap.get(querryK);
                int v2 = target.get(querryK);
                if (v1 != v2) {
                    System.out.println("get 错误");
                    System.out.printf("key:%d, treeMapAns:%d, ans:%d\n", querryK, v1, v2);
                    success = false;
                    break;
                }
                Integer f1 = treeMap.floorKey(querryK);
                Integer f2 = target.floorKey(querryK);
                if (!Objects.equals(f1, f2)) {
                    System.out.println("floorKey 错误");
                    System.out.printf("key:%d, treeMapAns:%d, ans:%d\n", querryK, f1, f2);
                    success = false;
                    break;
                }
                f1 = treeMap.ceilingKey(querryK);
                f2 = target.ceilingKey(querryK);
                if (!Objects.equals(f1, f2)) {
                    System.out.println("ceilingKey 错误");
                    System.out.printf("key:%d, treeMapAns:%d, ans:%d\n", querryK, f1, f2);
                    success = false;
                    break;
                }
            }

            Integer f1 = treeMap.firstKey();
            Integer f2 = target.firstKey();
            if (!Objects.equals(f1, f2)) {
                System.out.println("firstKey 错误");
                System.out.printf("key:%d, treeMapAns:%d, ans:%d\n", querryK, f1, f2);
                success = false;
                break;
            }

            f1 = treeMap.lastKey();
            f2 = target.lastKey();
            if (!Objects.equals(f1, f2)) {
                System.out.println("lastKey 错误");
                System.out.printf("key:%d, treeMapAns:%d, ans:%d\n", querryK, f1, f2);
                success = false;
                break;
            }
            int treeMapSize = treeMap.size();
            int ansSize = target.size();
            if (treeMapSize != ansSize) {
                System.out.println("size 错误");
                System.out.printf("key:%d, treeMapAns:%d, ans:%d\n", querryK, treeMapSize, ansSize);
                success = false;
                break;
            }
        }
        if (!success) {
            System.out.println("测试失败");
            return;
        }
        System.out.println(prefix + " 功能测试结束");
    }

    public static void performanceTest(String prefix) {
        System.out.println(prefix + " 性能测试开始");
        TreeMap<Integer, Integer> treeMap = new TreeMap<>();
        AvlTreeMap<Integer, Integer> target = new AvlTreeMap<>();
        long start;
        long end;
        int max = 1000000;
        System.out.println("顺序递增加入测试,数据规模 : " + max);
        start = System.currentTimeMillis();
        for (int i = 0; i < max; i++) {
            treeMap.put(i, i);
        }
        end = System.currentTimeMillis();
        System.out.println("treeMap 运行时间 : " + (end - start) + "ms");

        start = System.currentTimeMillis();
        for (int i = 0; i < max; i++) {
            target.put(i, i);
        }
        end = System.currentTimeMillis();
        System.out.println(prefix + " 运行时间 : " + (end - start) + "ms");


        System.out.println("顺序递增删除测试,数据规模 : " + max);
        start = System.currentTimeMillis();
        for (int i = 0; i < max; i++) {
            treeMap.remove(i);
        }
        end = System.currentTimeMillis();
        System.out.println("treeMap 运行时间 : " + (end - start) + "ms");

        start = System.currentTimeMillis();
        for (int i = 0; i < max; i++) {
            target.remove(i);
        }
        end = System.currentTimeMillis();
        System.out.println(prefix + " 运行时间 : " + (end - start) + "ms");

        System.out.println("顺序递减加入测试,数据规模 : " + max);
        start = System.currentTimeMillis();
        for (int i = max; i >= 0; i--) {
            treeMap.put(i, i);
        }
        end = System.currentTimeMillis();
        System.out.println("treeMap 运行时间 : " + (end - start) + "ms");

        start = System.currentTimeMillis();
        for (int i = max; i >= 0; i--) {
            target.put(i, i);
        }
        end = System.currentTimeMillis();
        System.out.println(prefix + " 运行时间 : " + (end - start) + "ms");

        System.out.println("顺序递减删除测试,数据规模 : " + max);
        start = System.currentTimeMillis();
        for (int i = max; i >= 0; i--) {
            treeMap.remove(i);
        }
        end = System.currentTimeMillis();
        System.out.println("treeMap 运行时间 : " + (end - start) + "ms");

        start = System.currentTimeMillis();
        for (int i = max; i >= 0; i--) {
            target.remove(i);
        }
        end = System.currentTimeMillis();
        System.out.println(prefix + " 运行时间 : " + (end - start) + "ms");


        System.out.println("随机加入测试,数据规模 : " + max);
        start = System.currentTimeMillis();
        for (int i = 0; i < max; i++) {
            treeMap.put((int) (Math.random() * i), i);
        }
        end = System.currentTimeMillis();
        System.out.println("treeMap 运行时间 : " + (end - start) + "ms");

        start = System.currentTimeMillis();
        for (int i = max; i >= 0; i--) {
            target.put((int) (Math.random() * i), i);
        }
        end = System.currentTimeMillis();
        System.out.println(prefix + " 运行时间 : " + (end - start) + "ms");

        System.out.println("随机删除测试,数据规模 : " + max);
        start = System.currentTimeMillis();
        for (int i = 0; i < max; i++) {
            treeMap.remove((int) (Math.random() * i));
        }
        end = System.currentTimeMillis();
        System.out.println("treeMap 运行时间 : " + (end - start) + "ms");

        start = System.currentTimeMillis();
        for (int i = max; i >= 0; i--) {
            target.remove((int) (Math.random() * i));
        }
        end = System.currentTimeMillis();
        System.out.println(prefix + " 运行时间 : " + (end - start) + "ms");

        System.out.println(prefix + " 性能测试结束");
    }
}

后记

个人学习总结笔记,不能保证非常详细,轻喷

相关推荐
巧克力味的桃子2 小时前
算法:大数除法
算法
@小码农2 小时前
2025年12月 GESP认证 图形化编程 一级真题试卷(附答案)
开发语言·数据结构·算法
巧克力味的桃子2 小时前
让程序在读取到整数0时就终止循环
c语言·算法
_OP_CHEN2 小时前
【算法基础篇】(三十九)数论之从质数判定到高效筛法:质数相关核心技能全解析
c++·算法·蓝桥杯·埃氏筛法·acm/icpc·筛质数·欧拉筛法
智嵌电子2 小时前
【笔记篇】【硬件基础篇】电路 修订第5版 (邱关源) 第六章 储能元件
笔记
算法与编程之美2 小时前
损失函数与分类精度的关系
人工智能·算法·机器学习·分类·数据挖掘
d111111111d2 小时前
STM32 I2C通信详解:从机地址与寄存器地址的作用
笔记·stm32·单片机·嵌入式硬件·学习
天呐草莓2 小时前
聚类(Clustering)算法
人工智能·python·算法·机器学习·数据挖掘·数据分析·聚类
m0_743106462 小时前
【基础回顾】针孔相机、深度、逆深度、与SfM的统一
人工智能·算法·计算机视觉·3d·几何学