平衡二叉树:AVL树

说到二叉搜索树,你可能会想——这不就是个排好序的树吗?查找、插入、删除都是 O(log n),挺完美的。但现实往往很骨感。我在项目里遇到过一种情况:数据是按顺序插入的,结果二叉搜索树直接退化成了链表,查找效率从 O(log n) 掉到了 O(n)。

嗯,这就是 AVL 树要解决的问题。它通过一种叫「平衡因子」的机制,让树始终保持平衡。说白了,就是不让树长歪。

什么是 AVL 树?

AVL 树是自平衡二叉搜索树。它的发明者是 Adelson-Velsky 和 Landis,所以叫 AVL。它的核心要求是:任意节点的左右子树高度差不超过 1

你想想看,如果每个节点都满足这个条件,整棵树的高度就能维持在 O(log n)。查找效率自然就稳了。

核心定义:AVL 树 = 二叉搜索树 + 平衡条件

平衡因子

平衡因子是 AVL 树的灵魂。它的计算公式很简单:

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

平衡因子的取值范围只有三个:-1、0、1。如果某个节点的平衡因子绝对值大于 1,那就说明这棵树不平衡了,需要旋转。

平衡因子 含义
0 左右子树等高,完美平衡
1 左子树比右子树高 1,可以接受
-1 右子树比左子树高 1,可以接受
2 或 -2 不平衡!需要旋转

我个人习惯在节点结构体里直接存高度,而不是平衡因子。因为高度更容易维护,计算平衡因子时直接相减就行。

typedef struct AVLNode {
    int key;
    struct AVLNode *left;
    struct AVLNode *right;
    int height;  // 节点高度
} AVLNode;

旋转操作

旋转是 AVL 树的核心操作。一共有四种情况:LL、RR、LR、RL。别被这些缩写吓到,其实逻辑很清晰。

我曾经在调试一个内存数据库时,因为旋转写错了,导致数据丢失。那次教训让我深刻理解了旋转的本质——旋转不改变中序遍历顺序

LL 旋转(右旋)

当新节点插入到左子树的左子树时,触发 LL 旋转。说白了就是左边太重了,往右掰一下。

AVLNode* rotateLL(AVLNode *y) {
    AVLNode *x = y->left;
    AVLNode *T2 = x->right;

    // 旋转
    x->right = y;
    y->left = T2;

    // 更新高度
    y->height = max(height(y->left), height(y->right)) + 1;
    x->height = max(height(x->left), height(x->right)) + 1;

    return x;  // 新根节点
}

RR 旋转(左旋)

新节点插入到右子树的右子树时,触发 RR 旋转。和 LL 对称,往左掰。

AVLNode* rotateRR(AVLNode *x) {
    AVLNode *y = x->right;
    AVLNode *T2 = y->left;

    y->left = x;
    x->right = T2;

    x->height = max(height(x->left), height(x->right)) + 1;
    y->height = max(height(y->left), height(y->right)) + 1;

    return y;
}

LR 旋转(先左旋后右旋)

新节点插入到左子树的右子树时,需要先对左子树做 RR 旋转,再对当前节点做 LL 旋转。嗯,这里容易搞混,我建议你画图理解。

AVLNode* rotateLR(AVLNode *z) {
    z->left = rotateRR(z->left);
    return rotateLL(z);
}

RL 旋转(先右旋后左旋)

新节点插入到右子树的左子树时,先对右子树做 LL 旋转,再对当前节点做 RR 旋转。

AVLNode* rotateRL(AVLNode *z) {
    z->right = rotateLL(z->right);
    return rotateRR(z);
}

记忆技巧:LL 和 RR 是单旋,LR 和 RL 是双旋。双旋的名字就是旋转顺序——LR 就是先左旋(L)后右旋(R)。

插入操作

AVL 树的插入分三步:

  1. 像普通二叉搜索树一样插入节点
  2. 更新沿途节点的高度
  3. 检查平衡因子,如果不平衡就旋转
AVLNode* insert(AVLNode *node, int key) {
    // 1. 普通 BST 插入
    if (node == NULL)
        return newNode(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. 更新高度
    node->height = 1 + max(height(node->left), height(node->right));

    // 3. 获取平衡因子
    int balance = getBalance(node);

    // 4. 判断旋转类型
    if (balance > 1 && key < node->left->key)
        return rotateLL(node);

    if (balance < -1 && key > node->right->key)
        return rotateRR(node);

    if (balance > 1 && key > node->left->key)
        return rotateLR(node);

    if (balance < -1 && key < node->right->key)
        return rotateRL(node);

    return node;
}

注意:插入后要递归更新高度。我曾经漏掉了这一步,结果平衡因子算出来全是错的,调试了一下午才发现。

删除操作

删除比插入复杂一些。因为删除后,不仅要考虑当前节点是否平衡,还要考虑它的祖先节点。

删除的步骤:

  1. 找到要删除的节点(和 BST 一样)
  2. 处理三种情况:叶子节点、单子节点、双子节点
  3. 更新高度并检查平衡
AVLNode* delete(AVLNode *root, int key) {
    // 1. 标准 BST 删除
    if (root == NULL) return root;

    if (key < root->key)
        root->left = delete(root->left, key);
    else if (key > root->key)
        root->right = delete(root->right, key);
    else {
        // 找到要删除的节点
        if ((root->left == NULL) || (root->right == NULL)) {
            AVLNode *temp = root->left ? root->left : root->right;
            if (temp == NULL) {
                temp = root;
                root = NULL;
            } else
                *root = *temp;
            free(temp);
        } else {
            // 双子节点:找中序后继
            AVLNode *temp = minValueNode(root->right);
            root->key = temp->key;
            root->right = delete(root->right, temp->key);
        }
    }

    if (root == NULL) return root;

    // 2. 更新高度并平衡
    root->height = 1 + max(height(root->left), height(root->right));
    int balance = getBalance(root);

    // 旋转判断(和插入类似)
    if (balance > 1 && getBalance(root->left) >= 0)
        return rotateLL(root);
    if (balance > 1 && getBalance(root->left) < 0)
        return rotateLR(root);
    if (balance < -1 && getBalance(root->right) <= 0)
        return rotateRR(root);
    if (balance < -1 && getBalance(root->right) > 0)
        return rotateRL(root);

    return root;
}

AVL 树知识体系

下面这张图帮你理清 AVL 树的整体脉络:

AVL 树 定义与性质 平衡因子 旋转操作 左子树高度 右子树高度 LL 右旋 RR 左旋 LR / RL 双旋 插入操作 删除操作 时间复杂度 O(log n)

AVL 树的优缺点

优点 缺点
查找效率稳定 O(log n) 插入/删除需要频繁旋转
不会退化成链表 需要额外存储高度信息
适合读多写少的场景 实现比红黑树复杂

我的建议:如果项目对查找性能要求极高,且插入删除不频繁,选 AVL 树。如果插入删除很频繁,红黑树可能更合适。我在做数据库索引时,就根据这个原则做了选择。

AVL 树的核心就这些。记住一句话:平衡是手段,不是目的。我们做平衡,是为了保证查找效率。别为了平衡而平衡,那会陷入过度设计的陷阱。


公众号:蓝海资料掘金营,微信deep3321