C++ AVL树概念与实现详解

前言

普通的二叉搜索树(BST)有个致命缺陷:插入有序数据时会退化成链表。

cpp 复制代码
BST<int> t;

for (int i = 1; i <= 1000; i++) t.insert(i);   // 树退化成一条右斜链

// 查找复杂度从 O(log n) 变成 O(n)

AVL 树 是最早被发明的自平衡二叉搜索树(1962 年,Adelson-Velsky 和 Landis)。它通过旋转 维持左右子树高度差不超过 1,从而把最坏复杂度稳定在 O(log n)。

本文从平衡因子讲起,完整实现插入、旋转和删除。


一、核心概念

1.1 平衡因子(Balance Factor)

复制代码
BF(node) = 左子树高度 - 右子树高度

AVL 树要求:每个节点的 BF ∈ {-1, 0, 1}。

只要有一个节点的 BF 变成 ±2,就说明失衡了,必须旋转修复。

1.2 为什么高度差限制在 1

这是 AVL 比红黑树更严格的地方:

树 平衡条件 树高上界

|-------|-------------|--------------|
| AVL 树 | 左右子树高度差 ≤ 1 | ≈ 1.44 log₂n |

|-----|-----------------|-----------|
| 红黑树 | 最长路径 ≤ 2 × 最短路径 | ≈ 2 log₂n |

代价与收益:

  • AVL 查询更快 (树更矮),但插入删除更慢(旋转更频繁)。
  • 红黑树插入删除更快,查询略慢。

所以:读多写少用 AVL,写多读少用红黑树 。这就是为什么 std::map 选了红黑树。


二、四种旋转

旋转是 AVL 的灵魂。只有四种情况,记住它们的形状即可。

2.1 LL 型(左左)→ 右旋

新节点插在左子树的左侧,导致失衡。

复制代码
        z(BF=2)                y(BF=0)

       /      \               /      \

   y(BF=1)     T4    ==>    x         z

   /     \                 /  \      /  \

  x       T3              T1  T2    T3  T4

 / \

T1  T2

2.2 RR 型(右右)→ 左旋

新节点插在右子树的右侧。

复制代码
   z(BF=-2)                    y(BF=0)

   /     \                    /      \

  T1    y(BF=-1)    ==>      z        x

       /    \               /  \     /  \

      T2     x             T1  T2   T3  T4

            / \

           T3  T4

2.3 LR 型(左右)→ 先左旋再右旋

新节点插在左子树的右侧。

复制代码
     z              z                x

    / \            / \             /   \

   y   T4  ==>    x   T4   ==>    y     z

  / \            / \             / \   / \

 T1   x         y   T3          T1 T2 T3 T4

     / \       / \

    T2  T3    T1  T2

   (先对 y 左旋)   (再对 z 右旋)

2.4 RL 型(右左)→ 先右旋再左旋

新节点插在右子树的左侧,是 LR 的镜像。

判断口诀:

  1. 看失衡节点 z 的 BF 是 +2 还是 -2 → 决定往哪边旋
  2. 再看 z 较重的那侧孩子的 BF → 决定是单旋还是双旋

三、完整实现

cpp 复制代码
#include <iostream>

#include <algorithm>

#include <vector>



template <typename T>

class AVLTree {

private:

    struct Node {

        T      key;

        Node*  left;

        Node*  right;

        int    height;          // 以该节点为根的子树高度



        explicit Node(const T& k)

            : key(k), left(nullptr), right(nullptr), height(1) {}

    };



    Node* root_ = nullptr;



    /* ---------- 基础工具 ---------- */



    int height(Node* n) const { return n ? n->height : 0; }



    int balanceFactor(Node* n) const {

        return n ? height(n->left) - height(n->right) : 0;

    }



    void updateHeight(Node* n) {

        if (n) n->height = 1 + std::max(height(n->left), height(n->right));

    }



    /* ---------- 旋转 ---------- */



    // 右旋:处理 LL 型

    Node* rotateRight(Node* z) {

        Node* y  = z->left;

        Node* T3 = y->right;



        y->right = z;

        z->left  = T3;



        updateHeight(z);        // 先更新下面的 z

        updateHeight(y);        // 再更新上面的 y

        return y;               // y 成为新的子树根

    }



    // 左旋:处理 RR 型

    Node* rotateLeft(Node* z) {

        Node* y  = z->right;

        Node* T2 = y->left;



        y->left  = z;

        z->right = T2;



        updateHeight(z);

        updateHeight(y);

        return y;

    }



    /* ---------- 插入 ---------- */



    Node* insert(Node* node, const T& key) {

        // 1. 标准 BST 插入

        if (!node) return new Node(key);



        if (key < node->key)       node->left  = insert(node->left,  key);

        else if (key > node->key)  node->right = insert(node->right, key);

        else                       return node;        // 不允许重复键



        // 2. 更新高度

        updateHeight(node);



        // 3. 计算平衡因子,判断是否需要旋转

        int bf = balanceFactor(node);



        // LL:左子树偏高,且新键插在左子树的左侧

        if (bf > 1 && key < node->left->key)

            return rotateRight(node);



        // RR:右子树偏高,且新键插在右子树的右侧

        if (bf < -1 && key > node->right->key)

            return rotateLeft(node);



        // LR:左子树偏高,但新键插在左子树的右侧

        if (bf > 1 && key > node->left->key) {

            node->left = rotateLeft(node->left);

            return rotateRight(node);

        }



        // RL:右子树偏高,但新键插在右子树的左侧

        if (bf < -1 && key < node->right->key) {

            node->right = rotateRight(node->right);

            return rotateLeft(node);

        }



        return node;            // 未失衡

    }



    /* ---------- 删除 ---------- */



    Node* minNode(Node* node) const {

        while (node->left) node = node->left;

        return node;

    }



    Node* remove(Node* node, const T& key) {

        if (!node) return nullptr;



        if (key < node->key) {

            node->left = remove(node->left, key);

        } else if (key > node->key) {

            node->right = remove(node->right, key);

        } else {

            // 找到目标节点

            if (!node->left || !node->right) {

                // 至多一个孩子

                Node* child = node->left ? node->left : node->right;

                delete node;

                return child;

            }

            // 两个孩子:用右子树最小值替换,再删除那个最小值

            Node* succ = minNode(node->right);

            node->key  = succ->key;

            node->right = remove(node->right, succ->key);

        }



        // 重新平衡(删除后失衡有四种,判断依据是孩子的 BF)

        updateHeight(node);

        int bf = balanceFactor(node);



        if (bf > 1 && balanceFactor(node->left) >= 0)

            return rotateRight(node);



        if (bf > 1 && balanceFactor(node->left) < 0) {

            node->left = rotateLeft(node->left);

            return rotateRight(node);

        }



        if (bf < -1 && balanceFactor(node->right) <= 0)

            return rotateLeft(node);



        if (bf < -1 && balanceFactor(node->right) > 0) {

            node->right = rotateRight(node->right);

            return rotateLeft(node);

        }



        return node;

    }



    /* ---------- 遍历与清理 ---------- */



    void inorder(Node* n, std::vector<T>& out) const {

        if (!n) return;

        inorder(n->left, out);

        out.push_back(n->key);

        inorder(n->right, out);

    }



    void destroy(Node* n) {

        if (!n) return;

        destroy(n->left);

        destroy(n->right);

        delete n;

    }



    // 校验:是否满足 BST 性质 + 平衡性质

    bool check(Node* n, bool& balanced) const {

        if (!n) return true;



        if (n->left && n->left->key >= n->key) return false;

        if (n->right && n->right->key <= n->key) return false;



        int bf = balanceFactor(n);

        if (bf < -1 || bf > 1) balanced = false;



        return check(n->left, balanced) && check(n->right, balanced);

    }



public:

    AVLTree() = default;

    ~AVLTree() { destroy(root_); }



    AVLTree(const AVLTree&) = delete;              // 简化:禁止拷贝

    AVLTree& operator=(const AVLTree&) = delete;



    void insert(const T& key) { root_ = insert(root_, key); }

    void remove(const T& key) { root_ = remove(root_, key); }



    bool contains(const T& key) const {

        Node* cur = root_;

        while (cur) {

            if      (key < cur->key) cur = cur->left;

            else if (key > cur->key) cur = cur->right;

            else                     return true;

        }

        return false;

    }



    int height() const { return height(root_); }



    std::vector<T> toVector() const {

        std::vector<T> out;

        inorder(root_, out);

        return out;

    }



    bool isValid() const {

        bool balanced = true;

        return check(root_, balanced) && balanced;

    }

};

四、测试

cpp 复制代码
#include <cassert>

#include <random>



int main() {

    AVLTree<int> t;



    // 1. 插入有序数据(普通 BST 会退化成链,AVL 必须保持平衡)

    for (int i = 1; i <= 1000; i++) t.insert(i);



    assert(t.isValid());

    std::cout << "插入 1000 个有序元素后,树高 = " << t.height() << '\n';

    // 输出约 10~14,而普通 BST 会是 1000



    // 2. 中序遍历必须有序

    auto v = t.toVector();

    for (size_t i = 1; i < v.size(); i++) assert(v[i - 1] < v[i]);



    // 3. 随机打乱插入,对比高度

    AVLTree<int> t2;

    std::mt19937 gen(42);

    std::vector<int> data(1000);

    for (int i = 0; i < 1000; i++) data[i] = i;

    std::shuffle(data.begin(), data.end(), gen);

    for (int x : data) t2.insert(x);

    std::cout << "随机插入 1000 个元素,树高 = " << t2.height() << '\n';



    // 4. 删除后仍然平衡

    for (int i = 1; i <= 500; i++) t.remove(i);

    assert(t.isValid());

    std::cout << "删除 500 个元素后,树高 = " << t.height() << '\n';



    std::cout << "全部测试通过\n";

    return 0;

}

预期输出(高度取决于具体实现,但都会远小于 1000):

复制代码
插入 1000 个有序元素后,树高 = 10

随机插入 1000 个元素,树高 = 12

删除 500 个元素后,树高 = 10

全部测试通过

五、踩坑要点

坑 1:更新高度的顺序

cpp 复制代码
// ❌ 错误:先更新 y 再更新 z,y 的高度用到了还没更新的 z

updateHeight(y);

updateHeight(z);



// ✅ 正确:先更新旋转后处于下层的节点

updateHeight(z);

updateHeight(y);

规则 :旋转后,先更新沉下去的那个节点。

坑 2:判断失衡类型不能只看平衡因子

插入 时可以直接比较 key 与孩子的大小;但删除 时不行 ------ 因为删除后 key 已经不在树里了,必须改用孩子的平衡因子判断:

cpp 复制代码
// 删除时的判断依据

if (bf > 1 && balanceFactor(node->left) >= 0)      // LL

if (bf > 1 && balanceFactor(node->left) <  0)      // LR

if (bf < -1 && balanceFactor(node->right) <= 0)    // RR

if (bf < -1 && balanceFactor(node->right) >  0)    // RL

这是初学者最容易写错的地方。

坑 3:删除有两个孩子的节点

不要直接删!用右子树的最小值(中序后继)替换键值,再递归删除那个后继节点。这样保持了 BST 性质,且把问题转化成"删除至多一个孩子的节点"。

坑 4:高度定义不一致

本实现中空节点高度 = 0,叶子节点高度 = 1 。只要全程一致就没问题,但如果你混用"空节点高度 = -1"的定义,updateHeight 和 balanceFactor 都要跟着改,否则测试会莫名失败。

坑 5:AVL 的删除代价很高

删除一个节点,从它到根一路上所有失衡节点都要旋转 ,可能触发 O(log n) 次旋转。

相比之下,红黑树删除最多只需 3 次旋转。这是 AVL 写性能差的主要原因。


六、AVL vs 红黑树

维度 AVL 树 红黑树

|--------|-------------|---------------|
| 平衡严格程度 | 高度差 ≤ 1(严格) | 最长 ≤ 2×最短(宽松) |

|----|--------------|-----------|
| 树高 | ≈ 1.44 log n | ≈ 2 log n |

|----|--------|----|
| 查找 | 更快 | 稍慢 |

|----|-------------------------|----------|
| 插入 | 最多 1 次旋转 + O(log n) 次更新 | 最多 2 次旋转 |

|----|---------------------|----------|
| 删除 | 最多 O(log n) 次旋转 | 最多 3 次旋转 |

|------|----|-----------|
| 实现难度 | 中等 | 较难(要处理颜色) |

|------|------------|--------------------------------------|
| 典型应用 | 内存数据库索引、字典 | std::map / std::set、Linux 内核 CFS |

选择建议:

  • 场景以查询为主 ,很少改动(比如配置表、静态字典)→ AVL
  • 场景增删频繁 ,查询也有一定量(比如通用容器)→ 红黑树

七、总结

  1. AVL 靠平衡因子(左右高度差 ∈ {-1,0,1})维持平衡,比红黑树更严格,树更矮。
  2. 旋转只有四种:LL 右旋、RR 左旋、LR 先左后右、RL 先右后左。
  3. 插入最多一次旋转即可恢复平衡;删除可能一路旋到根。
  4. 删除时判断失衡类型必须看孩子的平衡因子,这是最容易错的地方。
  5. 旋转后先更新下层节点的高度。
  6. AVL 适合读多写少,红黑树适合增删频繁。

理解 AVL 是理解红黑树的前置条件 ------ 红黑树本质上是用"颜色"来代替"严格高度差",用更少的旋转换取可接受的平衡度。


代码在 GCC 13 / C++17 下编译测试通过。如果对你有帮助,欢迎点赞收藏,有问题评论区交流。

相关推荐
炘爚1 小时前
C++(STL)
开发语言·c++
波力海苔夹心脆6751 小时前
C# 海康威视摄像头二次开发入门:HCNetSDK 登录、实时预览、云台控制、录像与抓图(WinForms 实战)
开发语言·windows·经验分享·tcp/ip·c#
数据狐(Datafox)1 小时前
淘宝商品详情API实战:多语言代购商城自动同步数据完整方案
开发语言·前端·数据库·爬虫·json
芯次元玩家2 小时前
技术岗转物联网解决方案架构师,行业洞察、商业模式该从哪开始学?
java·开发语言
2601_962218612 小时前
C++ string 类原理、踩坑与对象语义详解
开发语言·c++
隔窗听雨眠2 小时前
FunProxy用Rust构建跨平台全链路测试抓包代理工具
开发语言·后端·rust
y = xⁿ2 小时前
关于Agent智能体开发
开发语言·前端·javascript
Zhou1411362 小时前
MyBatisPlus_02_条件构造器与高级功能
java·开发语言·python
geovindu2 小时前
rust: search
开发语言·后端·rust