AVL树原理与实现详解——旋转如何掰正一棵树

写在前面

📖 目录

在这里插入图片描述


写在前面

前面的文章里我们已经讲清楚了二叉搜索树如何大大提高查找数据的效率,但也留下了一个扎心的结论:普通 BST 有可能退化成一条链表

一旦你按 {1, 3, 4, 6, 7, 8, 10} 这种有序的顺序往里插,这棵"树"就会歪成一根竹竿:

1
 \
  3
   \
    4
     \
      6
       \
        7   ← 查找 7 要比较 5 次,和遍历链表没区别

查找效率从我们期望的 O(logN) 一路滑到 O(N)。对于数据库索引、STL 的 map/set 这种要求稳定高效的场景,这种不稳定的性能是绝对不能接受的。

那怎么办?既然树歪了是因为左右子树高度失衡,那我们能不能在插入的过程中随时盯着树的形状,一旦发现快歪了,就动手把它掰正

这,就是 AVL 树的核心思想。这篇文章会带你:

  • 搞懂 AVL 树是什么、它靠什么控制平衡;
  • 逐行剖析插入时平衡因子的更新逻辑(这是最绕的部分);
  • 左单旋、右单旋、左右双旋、右左双旋四种旋转彻底讲透,配合抽象图一步步推导;
  • 给出一套可运行的完整代码,并用百万级数据验证它的平衡性和性能。

准备好了吗?发车。


一、AVL 树到底是什么

AVL 树是最早被发明出来的自平衡二叉查找树。它得名于两位前苏联科学家 G. M. Adelson-VelskyE. M. Landis,他们在 1962 年的论文《An algorithm for the organization of information》中提出了它——AVL 就是两人姓氏首字母的组合。

它的定义很简洁。一棵 AVL 树,要么是空树,要么是一棵满足下列性质的二叉搜索树

它的左右子树都是 AVL 树,且左右子树的高度差的绝对值不超过 1。

注意,这是一个递归定义:不仅整棵树要满足高度差不超过 1,每一个子树、每一个结点为根的局部,都得满足。正是这种"处处平衡"的要求,让 AVL 树的高度被死死摁在 logN 的量级,从而保证增删查改都稳定在 O(logN)——相比普通 BST,这是本质上的提升。

来看一棵合格的 AVL 树长什么样(结点上方的数字是它的平衡因子,下面马上会讲):

            8  (-1)
          /   \
      (1)3     10 (1)
        / \      \
   (0)1  6(0)    14(0)
        / \
   (0)4   7(0)

一个问题:为什么是"高度差不超过 1",而不是"高度差为 0"?

高度差为 0,左右两边一样高,那不是更完美的平衡吗?

道理是这个道理,但现实做不到。你随手画几个图就明白了:一棵树只有 2 个结点时,它的高度差最好也只能是 1(一个根 + 一个孩子,另一边是空);4 个结点时同理。很多结点数量下,高度差为 0 根本无法实现。

所以 AVL 树的设计者退了一步:不追求绝对的对称,只要求高度差不超过 1。这已经足够把树的高度控制在 logN 附近了。

平衡因子:给每个结点装一个"风向标"

怎么高效地判断一个结点有没有失衡?每次都去递归计算左右子树高度显然太慢。AVL 树引入了一个巧妙的概念——平衡因子(balance factor,简称 bf)

平衡因子 = 右子树高度 − 左子树高度

对于一棵合格的 AVL 树,任何结点的平衡因子只能是 -1、0、1 这三个值之一:

  • bf == 0:左右子树一样高;
  • bf == 1:右子树比左子树高 1;
  • bf == -1:左子树比右子树高 1。

一旦某个结点的平衡因子变成了 2 或 -2,就说明这棵子树失衡了,需要旋转处理。

严格来说,AVL 树并不是必须要平衡因子——你完全可以每次现算高度。但有了平衡因子这个"风向标",我们就能实时、以较低成本观察和控制树的平衡状态,代码写起来也清爽得多。本文的实现就是基于平衡因子的。


二、先搭骨架:结点的结构

正式写逻辑之前,先把结点定义下来。相比普通 BST,AVL 树的结点多了两样东西:一个指向父结点的 _parent 指针,一个平衡因子 _bf

template<class K, class V>
struct AVLTreeNode
{
    pair<K, V> _kv;              // 键值对
    AVLTreeNode<K, V>* _left;    // 左孩子
    AVLTreeNode<K, V>* _right;   // 右孩子
    AVLTreeNode<K, V>* _parent;  // 父结点——这是关键!
    int _bf;                     // balance factor 平衡因子

    AVLTreeNode(const pair<K, V>& kv)
        : _kv(kv)
        , _left(nullptr)
        , _right(nullptr)
        , _parent(nullptr)
        , _bf(0)                 // 新结点平衡因子初始为 0
    {}
};

为什么要有 _parent 指针? 因为插入一个新结点后,我们需要从新结点一路往上回溯,逐个更新祖先结点的平衡因子。没有父指针,就没法向上走。这个设计后面你会反复体会到它的必要性。

树的框架也很简单:

template<class K, class V>
class AVLTree
{
    typedef AVLTreeNode<K, V> Node;
public:
    // ... 各种接口
private:
    Node* _root = nullptr;
};

三、插入:AVL 树的重头戏

插入操作是 AVL 树的灵魂,它可以拆成三步:

  1. 按二叉搜索树的规则找到位置,把新结点挂上去——这部分和普通 BST 一模一样;
  2. 从新结点往上更新平衡因子,看看有没有祖先失衡;
  3. 如果发现失衡,就对失衡的子树做旋转,把它掰平。

第一步大家都熟,我们重点啃第二步和第三步。

3.1 第一步:按 BST 规则插入

bool Insert(const pair<K, V>& kv)
{
    if (_root == nullptr)   // 空树,直接作为根
    {
        _root = new Node(kv);
        return true;
    }

    // 从根开始找插入位置,parent 记录路径上的父结点
    Node* parent = nullptr;
    Node* cur = _root;
    while (cur)
    {
        if (cur->_kv.first < kv.first)      // 要插的值更大,往右走
        {
            parent = cur;
            cur = cur->_right;
        }
        else if (cur->_kv.first > kv.first) // 要插的值更小,往左走
        {
            parent = cur;
            cur = cur->_left;
        }
        else
        {
            return false;   // 值已存在,不允许重复,插入失败
        }
    }

    // 走到这里,cur 为空,parent 就是新结点该挂的父亲
    cur = new Node(kv);
    if (parent->_kv.first < kv.first)
        parent->_right = cur;
    else
        parent->_left = cur;
    cur->_parent = parent;   // 别忘了链接父指针!

    // ... 接下来是更新平衡因子(见 3.3)
}

到这里,新结点已经正确地挂在了树上。但是——它的加入很可能已经破坏了某些祖先的平衡。真正的挑战现在才开始。

3.2 第二步的理论基础:平衡因子该怎么更新

这是全文最需要动脑子的地方,我建议你拿张纸跟着推一遍。

先记住三条更新原则

  • 平衡因子 = 右子树高度 − 左子树高度
  • 只有子树高度发生变化,才会影响当前结点的平衡因子
  • 新增结点会让某一边变高:如果新结点在 parent子树,parent->_bf++;在子树,parent->_bf--

新结点插入后,只会影响它的祖先们的高度,所以我们要沿着 新结点 → 根 这条路径向上更新。关键问题是:更新到什么时候可以停? 这取决于「parent 所在子树的高度是否发生了变化」。分三种情况:

情况 A:更新后 parent->_bf == 0 → 停止更新

平衡因子从 1 → 0-1 → 0,说明更新前 parent 是一边高一边低的,新结点恰好插进了矮的那边,把它填平了。此时 parent 这棵子树的高度没变,不会影响再上面的祖先。更新结束。

更新前:parent(bf=-1)      更新后:parent(bf=0)
          /      \                 /   \
         h       h-1              h     h   ← 填平了,总高度不变

情况 B:更新后 parent->_bf == 1 或 -1 → 继续往上更新

平衡因子从 0 → 10 → -1,说明更新前 parent 两边一样高,新结点插进来后一边高了。子树依然平衡(bf 绝对值没超 1),但它的高度增加了 1。这个"长高"会传递给上一层,影响祖先的平衡因子。所以要 cur = parent; parent = parent->_parent; 继续向上更新

情况 C:更新后 parent->_bf == 2 或 -2 → 旋转处理

平衡因子从 1 → 2-1 → -2,说明更新前 parent 已经是一边高了,新结点又插进了高的那边,让它雪上加霜,高度差达到 2,违反了平衡规则!这时必须旋转。旋转有两个目标:① 把子树重新掰平;② 把子树高度降回插入之前的样子。旋转完成后高度复原,也就不会再影响上层,插入结束。

用一张图直观感受这三种停止时机(往树里插入 13,触发向上更新):

更新到 10:bf 变成 2,失衡,需要旋转(情况 C)
            8(-1)
          /      \
      (1)3        10 (1→2) ← parent 失衡了!
        / \          \
   (0)1  6(0)        14 (0→-1)
        / \          /
   (0)4  7(0)      13 ← cur,刚插入

而如果插入后某个中间结点的 bf 归零,就会像下面这样提前刹车:

更新到 3:bf 从 1 变成 0(情况 A),3 为根的子树高度不变
              8(-1)
           /       \
       (1→0)3       10(1)
          /  \        \
   (0→-1)1   6(0)     14(0)
        /
      -1  ← cur,插在了 1 的矮边,把 3 填平了,更新结束

最坏情况下,会一路更新到根才停止。

3.3 第二步的代码实现

理解了上面的逻辑,代码就是一个 while 循环加三个分支,几乎是逐字翻译:

    // 接 3.1 的代码:更新平衡因子
    while (parent)
    {
        // 新结点在左就 --,在右就 ++
        if (cur == parent->_left)
            parent->_bf--;
        else
            parent->_bf++;

        if (parent->_bf == 0)
        {
            // 情况 A:填平了矮的那边,子树高度不变,更新结束
            break;
        }
        else if (parent->_bf == 1 || parent->_bf == -1)
        {
            // 情况 B:子树长高了 1,继续往上更新
            cur = parent;
            parent = parent->_parent;
        }
        else if (parent->_bf == 2 || parent->_bf == -2)
        {
            // 情况 C:失衡了,旋转处理
            if (parent->_bf == -2 && cur->_bf == -1)
                RotateR(parent);    // 左边高 + 左边的左边高 → 右单旋
            else if (parent->_bf == 2 && cur->_bf == 1)
                RotateL(parent);    // 右边高 + 右边的右边高 → 左单旋
            else if (parent->_bf == -2 && cur->_bf == 1)
                RotateLR(parent);   // 左边高 + 左边的右边高 → 左右双旋
            else if (parent->_bf == 2 && cur->_bf == -1)
                RotateRL(parent);   // 右边高 + 右边的左边高 → 右左双旋
            else
                assert(false);      // 理论上不会到这,到了说明前面 bf 更新有 bug

            break;   // 旋转后高度复原,不再影响上层,结束
        }
        else
        {
            assert(false);   // bf 出现了 ±2 以外的值,一定是逻辑错误
        }
    }
    return true;
}

注意情况 C 里的那四个分支,它由 parent->_bfcur->_bf符号组合决定用哪种旋转。这正是我们接下来要逐一攻破的四种旋转。记住这张对照表:

parent 的 bfcur 的 bf含义旋转方式
-2-1左边高,且是"纯左高"右单旋 RotateR
+2+1右边高,且是"纯右高"左单旋 RotateL
-2+1左边高,但左孩子右边高(折线)左右双旋 RotateLR
+2-1右边高,但右孩子左边高(折线)右左双旋 RotateRL

四、四种旋转:把歪掉的树掰正

旋转必须遵守两条铁律:

  1. 旋转后仍然是一棵二叉搜索树(不能破坏"左小右大"的规则);
  2. 让失衡的子树重新平衡,同时把高度降回插入之前

下面用抽象的高度为 h 的子树 a / b / c 来分析——它们代表了所有可能的具体形态,只要大小关系符合搜索树性质即可。

4.1 右单旋(RotateR):应对"纯左高"

看这个抽象场景:以 10 为根,左孩子是 5。在 a 子树里插入新结点,a 的高度从 h 变成 h+1,向上更新导致 10 的平衡因子从 -1 变成 -2。10 的左边太高了,需要往右边"压"下去

        10 (-2)              右单旋后:        5 (0)
       /    \                              /     \
   (−1)5     c(h)      ==========>     a(h+1)    10 (0)
     /  \                                        /   \
 a(h+1) b(h)                                   b(h)  c(h)

旋转的核心动作:因为 5 < b子树的值 < 10,所以把 b 变成 10 的左子树,10 变成 5 的右子树,5 上位成为新的根。旋转后:搜索树规则依然成立(a < 5 < b < 10 < c),子树平衡了,而且高度从 h+2 恢复了——完美满足两条铁律。

代码实现的关键,是别只顾着改孩子指针,父指针也要一起改,还要处理"parent 是否是整棵树的根"这个边界:

void RotateR(Node* parent)
{
    Node* subL = parent->_left;      // subL 是 parent 的左孩子(要上位的 5)
    Node* subLR = subL->_right;      // subLR 是 subL 的右孩子(要过继的 b)

    // 第一步:b 过继给 parent 当左孩子
    parent->_left = subLR;
    if (subLR)                       // b 可能是空树,判空再改父指针
        subLR->_parent = parent;

    Node* pParent = parent->_parent; // 提前记住 parent 的父亲,后面要用

    // 第二步:parent 变成 subL 的右孩子
    subL->_right = parent;
    parent->_parent = subL;

    // 第三步:把 subL 接到原来 parent 的位置上
    if (parent == _root)             // parent 原本就是整棵树的根
    {
        _root = subL;
        _root->_parent = nullptr;
    }
    else                             // parent 只是个局部子树,要跟上一层接好
    {
        if (pParent->_left == parent)
            pParent->_left = subL;
        else
            pParent->_right = subL;
        subL->_parent = pParent;
    }

    // 第四步:更新平衡因子。旋转后 subL 和 parent 都恢复平衡
    subL->_bf = 0;
    parent->_bf = 0;
}

注意:链接指针时一定要注意 subLR 可能为空(当 h == 0b 是空树),直接 subLR->_parent 会解引用空指针。判空是必须的。

4.2 左单旋(RotateL):右单旋的镜像

左单旋和右单旋完全对称。场景:以 10 为根,右孩子是 15,在 c 子树(这里习惯把最右的抽象子树叫 a)里插入新结点,导致 10 的平衡因子从 1 变成 2,右边太高,需要往左边"压"

    10 (2)                  左单旋后:      15 (0)
   /    \                                 /     \
 c(h)    15 (1)      ==========>       10 (0)   a(h+1)
        /   \                         /   \
     b(h)  a(h+1)                   c(h)  b(h)

核心动作:把 b10 < b < 15)过继给 10 当右孩子,10 变成 15 的左孩子,15 上位。代码和右单旋是完美的镜像——把所有 left 换成 rightsubL 换成 subR 即可:

void RotateL(Node* parent)
{
    Node* subR = parent->_right;
    Node* subRL = subR->_left;

    parent->_right = subRL;
    if (subRL)
        subRL->_parent = parent;

    Node* pParent = parent->_parent;

    subR->_left = parent;
    parent->_parent = subR;

    if (parent == _root)
    {
        _root = subR;
        _root->_parent = nullptr;
    }
    else
    {
        if (pParent->_left == parent)
            pParent->_left = subR;
        else
            pParent->_right = subR;
        subR->_parent = pParent;
    }

    subR->_bf = 0;
    parent->_bf = 0;
}

4.3 左右双旋(RotateLR):单旋救不了的"折线"

单旋只能解决直线型的失衡(左边高,且高在左孩子的左边)。但如果失衡是折线型的呢?

看这个场景:10 左边高(bf = -2),但新结点不是插在左孩子 5 的子树 a,而是插在了 5 的子树 b 里。这时如果你还用右单旋,会发现——树依然不平衡

        10 (-2)          直接右单旋 →      5 (还是歪的)
       /    \                              \
   (1)5      c(h)                          10
     / \                                  /  \
  a(h) b(h+1)                            b?   c
        ↑ 新结点插这儿

问题出在哪?右单旋只能处理"纯左高",但现在的情况是:对 10 来说是左边高,对 5 来说却是右边高——这是一个"折线",得拐两个弯才能捋直。

解法是两次旋转:先以 5 为轴做一次左单旋,把折线捋成直线;再以 10 为轴做一次右单旋,把树压平。

     10(-2)          先对 5 左单旋       10(-2)        再对 10 右单旋       8(0)
    /    \           ============>     /    \        ============>     /    \
 (1)5    c(h)                      (-2)8    c(h)                      5      10
   / \                              /                               / \    / \
 a  8(subLR)                     5                                 a   e  f   c
    / \                          / \
   e   f                        a   e

代码上,RotateLR 直接复用前面写好的两个单旋。真正的技术难点在于旋转后平衡因子怎么更新——它取决于新结点当初到底插在 subLR(也就是上图的 8)的左边还是右边,这体现为 subLR->_bf 旋转前的值。所以要先把 subLR->_bf 记下来,分三种场景赋值:

void RotateLR(Node* parent)
{
    Node* subL = parent->_left;
    Node* subLR = subL->_right;
    int bf = subLR->_bf;      // 关键:先记录 subLR 的平衡因子

    RotateL(parent->_left);   // 先对 subL 左单旋
    RotateR(parent);          // 再对 parent 右单旋

    // 根据 bf 的三种情况,修正三个结点的平衡因子
    if (bf == 0)              // 场景3:subLR 自己就是新增结点(h==0)
    {
        subLR->_bf = 0;
        subL->_bf = 0;
        parent->_bf = 0;
    }
    else if (bf == -1)        // 场景1:新结点插在 subLR 的左子树 e
    {
        subLR->_bf = 0;
        subL->_bf = 0;
        parent->_bf = 1;
    }
    else if (bf == 1)         // 场景2:新结点插在 subLR 的右子树 f
    {
        subLR->_bf = 0;
        subL->_bf = -1;
        parent->_bf = 0;
    }
    else
    {
        assert(false);
    }
}

这三种场景的推导,本质是问"新结点到底落在 subLR 左右哪一侧的子树,旋转后各个结点分到多少高度"。如果你想彻底吃透,强烈建议拿笔画出 h==0h==1 两个具体的小图,把 e、f 子树的高度标出来,跟着旋转走一遍,你会豁然开朗。

4.4 右左双旋(RotateRL):左右双旋的镜像

右左双旋是左右双旋的镜像:10 右边高(bf = 2),但新结点插在了右孩子(比如 15)的子树里,形成折线。解法是先对右孩子右单旋,再对 parent 左单旋

void RotateRL(Node* parent)
{
    Node* subR = parent->_right;
    Node* subRL = subR->_left;
    int bf = subRL->_bf;      // 同样先记录 subRL 的平衡因子

    RotateR(parent->_right);  // 先对 subR 右单旋
    RotateL(parent);          // 再对 parent 左单旋

    if (bf == 0)
    {
        subRL->_bf = 0;
        subR->_bf = 0;
        parent->_bf = 0;
    }
    else if (bf == -1)        // 新结点插在 subRL 的左子树
    {
        subRL->_bf = 0;
        subR->_bf = 1;
        parent->_bf = 0;
    }
    else if (bf == 1)         // 新结点插在 subRL 的右子树
    {
        subRL->_bf = 0;
        subR->_bf = 0;
        parent->_bf = -1;
    }
    else
    {
        assert(false);
    }
}

四种旋转怎么记? 抓住 parent->_bfcur->_bf 的符号:同号用单旋-2/-1 右单旋,+2/+1 左单旋),异号用双旋-2/+1 左右双旋,+2/-1 右左双旋)。同号是"直线",异号是"折线",折线要先掰成直线再压平。


五、查找:回归朴素

聊完了最烧脑的插入和旋转,查找简直像度假。AVL 树本质还是二叉搜索树,查找逻辑和普通 BST 一模一样——大了往右走,小了往左走。区别只在于:因为 AVL 树的高度稳定在 logN,所以查找效率被牢牢锁定在 O(logN),不会再退化。

Node* Find(const K& key)
{
    Node* cur = _root;
    while (cur)
    {
        if (cur->_kv.first < key)
            cur = cur->_right;
        else if (cur->_kv.first > key)
            cur = cur->_left;
        else
            return cur;   // 找到了
    }
    return nullptr;       // 没找到
}

六、如何验证我们的 AVL 树是"合格"的

代码写完,怎么证明它真的时刻保持平衡?我们写一个反向验证程序:递归检查每个结点,看它左右子树的真实高度差是否超过 1,同时顺便验证平衡因子有没有更新错

// 求以 root 为根的树的高度
int _Height(Node* root)
{
    if (root == nullptr)
        return 0;
    int leftHeight = _Height(root->_left);
    int rightHeight = _Height(root->_right);
    return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

bool _IsBalanceTree(Node* root)
{
    if (nullptr == root)     // 空树也是 AVL 树
        return true;

    // 实打实地算出左右子树高度
    int leftHeight = _Height(root->_left);
    int rightHeight = _Height(root->_right);
    int diff = rightHeight - leftHeight;

    // 检查一:高度差超过 1,直接判死
    if (abs(diff) >= 2)
    {
        cout << root->_kv.first << " 高度差异常" << endl;
        return false;
    }

    // 检查二:实算的高度差是否和结点存的 _bf 一致
    //         不一致说明我们更新平衡因子的逻辑有 bug!
    if (root->_bf != diff)
    {
        cout << root->_kv.first << " 平衡因子异常" << endl;
        return false;
    }

    // 左右子树都是 AVL 树,这棵树才是 AVL 树
    return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}

这里的检查二特别值得称道:它不仅验证了"树是否平衡",还顺带验证了"我们在插入/旋转时维护的 _bf 是否和真实高度差吻合"。很多平衡因子处理中的隐藏 bug,可以靠这一行看出来。


七、实战测试:百万数据见真章

光说不练假把式。我们用两组测试来收尾:一组小数据专门覆盖双旋场景,一组大数据检验平衡性和性能。

void TestAVLTree1()
{
    AVLTree<int, int> t;
    // 这组数据能触发双旋,是很好的边界测试用例
    int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
    for (auto e : a)
        t.Insert({ e, e });

    t.InOrder();                        // 中序遍历应输出升序
    cout << t.IsBalanceTree() << endl;  // 应输出 1(平衡)
}

// 插入一百万个随机值,检验平衡性、高度和性能
void TestAVLTree2()
{
    const int N = 1000000;
    vector<int> v;
    v.reserve(N);
    srand(time(0));
    for (size_t i = 0; i < N; i++)
        v.push_back(rand() + i);

    size_t begin2 = clock();
    AVLTree<int, int> t;
    for (auto e : v)
        t.Insert(make_pair(e, e));
    size_t end2 = clock();

    cout << "Insert:" << end2 - begin2 << endl;
    cout << t.IsBalanceTree() << endl;   // 百万数据依然平衡?
    cout << "Height:" << t.Height() << endl;
    cout << "Size:" << t.Size() << endl;

    size_t begin1 = clock();
    for (auto e : v)
        t.Find(e);
    size_t end1 = clock();
    cout << "Find:" << end1 - begin1 << endl;
}

这里最有说服力的是 Height 这个输出。100 万个结点,如果是理想的完全二叉树,高度大约是 log₂(10⁶) ≈ 20。运行后你会发现 AVL 树的实际高度也就在 20 出头——哪怕数据里混着大量近乎有序的值(rand() + i),它依然稳如老狗。这就是自平衡的威力:无论你怎么"刁难"它,它都能把高度死死摁在 logN


八、关于删除

细心的你可能发现了:这篇文章从头到尾只讲了插入,没讲删除。

这不是偷懒。AVL 树的删除比插入还要复杂——删除后同样要向上更新平衡因子、可能触发旋转,但旋转后子树高度可能继续变化,导致需要一路旋转到根,情况的组合远比插入繁多。对于初学者,把插入的四种旋转彻底吃透,收益已经足够大了。

如果你想挑战删除,推荐参考《殷人昆 数据结构:用面向对象方法与 C++ 语言描述》中的详细讲解。


九、写在最后:AVL 树的功与过

我们花了一整篇文章,讲清楚了一件事:通过控制平衡因子、在插入时适时旋转,把一棵可能歪成链表的搜索树,牢牢按在 logN 的高度上。

AVL 树的优点很突出:查找、插入、删除都稳定在 O(logN),性能可预测,没有普通 BST"看脸吃饭"的毛病。

但它也有代价:为了维持"高度差不超过 1"这个近乎苛刻的要求,它在插入和删除时可能频繁旋转。旋转虽然是 O(1) 的操作,但过于严格的平衡标准让旋转发生得比较频繁,在写操作密集的场景下会有一定开销。

正因如此,工业界更常用的其实是它的"表弟"——红黑树。红黑树放松了平衡标准(最长路径不超过最短路径的 2 倍),用更少的旋转换取了"足够好"的平衡。STL 的 map / set、Java 的 TreeMap、Linux 内核的进程调度,用的都是红黑树。

不过别小看今天学的 AVL 树——它是理解一切平衡搜索树的基石。旋转的思想、平衡因子的维护、向上回溯更新的套路,在红黑树里会以更精妙的形式再次出现。

下一篇,我们就来会一会这位大名鼎鼎的红黑树。


如果有建议或问题,欢迎在评论区交流~

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值