AVL 树:旋转、插入、删除 C++ 例如:

⚡ 智能摘要

AVL 树是一种自平衡二叉搜索树,其中每个节点的左子树和右子树之间的高度差保持在 -1、0 或 +1 之间,保证了 O(log n) 的搜索性能。

  • 🌲 定义: 一种二叉搜索树,其中每个节点的平衡因子都在 {-1, 0, +1} 范围内,以发明者 Adelson-Velsky 和 ​​Landis 的名字命名。
  • 平衡系数: 计算公式为 height(left) − height(right);值超出 {-1, 0, +1} 范围会触发旋转以恢复平衡。
  • 🔄 轮换: 四种情况——LL、RR、LR 和 RL——在不平衡的插入或删除后重新对齐节点,以保持树的高度呈对数增长。
  • 插入: 标准 BST 插入,然后向上移动,重新计算平衡因子,最多执行一次单次或两次旋转。
  • 删除: 与 BST 删除相同,但可能会沿着树向上级联多次旋转,因为子树的高度可能会在每个祖先处缩小。
  • 🚀 应用环境: 数据库、内存索引、文件系统元数据和 AI 搜索结构使用 AVL 树进行快速有序查找。

AVL 树

什么是 AVL 树?

AVL 树 是二叉搜索树,其中每个节点的左子树和右子树之间的高度差为 -1、0 或 +1。它们是自平衡二叉搜索树,保持对数搜索时间,以发明者 Adelson-Velsky 和 ​​Landis (AVL) 的名字命名。

AVL 树如何工作?

要理解 AVL 树存在的意义,首先要看看普通 AVL 树会出现什么问题。 二进制搜索树假设按以下顺序插入这些键:

AVL 树的工作

AVL 树可视化

当键按递增顺序插入时,树会线性增长,导致搜索复杂度退化为 O(n)。这违背了二叉搜索树的初衷——只有平衡树才能保持搜索复杂度对数级。现在考虑以不同顺序插入相同的键。

AVL 树的工作

相同的键,不同的插入顺序会产生更浅的树形,因此每次搜索的时间复杂度为 O(log n)。AVL 树通过监控每次插入的高度并纠正不平衡来强制执行这种树形,同时保持二叉搜索树的顺序不变。

AVL 树中的平衡因子

平衡因子(BF) tracks 计算每个节点的高度,以便树可以动态地自我平衡。

平衡因子的性质

AVL 树中的平衡因子

平衡因子 AVL 树

  • 平衡因子是左子树高度与右子树高度之差。
  • Balance factor(node) = height(node->left) − height(node->right)
  • 只允许取 -1、0 和 +1 这三个值。
  • 值为 -1 表示右子树包含一个额外的层——该节点是右重的。
  • 值为 +1 表示左子树包含额外的一层——该节点是左重的。
  • 值为 0 表示两侧高度相等——节点完全平衡。

AVL 旋转

当插入或删除操作打破平衡因子规则时,就会进行轮换。这四种情况分别是 LL、RR、LR 和 RL。

左 – 左旋转

当在左子树的左孩子处插入新节点时,执行此旋转。

AVL 树左 - 左旋转

AVL 树左 - 左旋转

执行一次向右旋转。当节点的 BF 值为 +2 且其左子节点的 BF 值为 +1 时,触发此操作。

右 – 右旋转

当在右子树的右孩子处插入新节点时,执行此旋转。

AVL 树右 – 右旋转

执行一次左旋转。当节点的 BF 值为 -2 且其右子节点的 BF 值为 -1 时,触发此操作。

右 - 左旋转

当在右子树的左孩子处插入新节点时,执行此旋转。

AVL 树右 - 左旋转

当 BF(节点) = −2 且 BF(右子节点) = +1 时触发。将右子节点向右旋转,然后将节点向左旋转。

左 - 右旋转

当在左子树的右孩子处插入新节点时,执行此旋转。

AVL 树左 - 右旋转

当 BF(节点) = +2 且 BF(左子节点) = −1 时触发。左子节点向左旋转,然后节点向右旋转。

AVL 树中的插入

插入操作几乎与普通的二叉搜索树插入操作完全相同。每次插入后,树都会向上遍历并重新平衡。插入操作的最坏情况时间复杂度为 O(log n)。

AVL 树中的插入

AVL 树插入实现

第三步: 使用标准二叉搜索树算法插入节点。在上面的例子中,插入节点 160。

第三步: 更新插入路径上每个祖先的平衡因子。

第三步: 如果任何祖先节点的平衡因子超出了范围,则执行匹配旋转。在本例中,节点 350 的平衡因子超出了范围,因此执行 LL 旋转可以恢复平衡。

  1. If BF(node) = +2BF(left-child) = +1进行LL旋转。
  2. If BF(node) = −2BF(right-child) = −1进行 RR 轮换。
  3. If BF(node) = −2BF(right-child) = +1执行 RL 旋转。
  4. If BF(node) = +2BF(left-child) = −1执行 LR 旋转。

AVL 树中的删除

删除操作遵循与普通二叉搜索树相同的逻辑,之后会重新平衡。

第三步: 在树中查找元素。

第三步: 使用标准的二叉搜索树删除方法删除节点。

第三步: 有两种可能的情况。

案例1: 从右子树删除。

  • 1A。 If BF(node) = +2BF(left-child) = +1进行LL旋转。
  • 1B。 If BF(node) = +2BF(left-child) = −1执行 LR 旋转。
  • 1C。 If BF(node) = +2BF(left-child) = 0进行LL旋转。

AVL 树中的删除

案例2: 从左侧子树中删除。

  • 2A。 If BF(node) = −2BF(right-child) = −1进行 RR 轮换。
  • 2B。 If BF(node) = −2BF(right-child) = +1执行 RL 旋转。
  • 2C。 If BF(node) = −2BF(right-child) = 0进行 RR 轮换。

AVL 树中的删除

C++ AVL 树的示例

下面是一个 C++ 实现AVL树的程序:

#include <iostream>
#include <queue>
#include <unordered_map>
using namespace std;

struct node {
    struct node *left;
    int data;
    int height;
    struct node *right;
};

class AVL {
public:
    struct node *root;

    AVL() {
        this->root = NULL;
    }

    int calheight(struct node *p) {
        if (p->left && p->right) {
            if (p->left->height < p->right->height)
                return p->right->height + 1;
            else
                return p->left->height + 1;
        }
        else if (p->left && p->right == NULL) {
            return p->left->height + 1;
        }
        else if (p->left == NULL && p->right) {
            return p->right->height + 1;
        }
        return 0;
    }

    int bf(struct node *n) {
        if (n->left && n->right)
            return n->left->height - n->right->height;
        else if (n->left && n->right == NULL)
            return n->left->height;
        else if (n->left == NULL && n->right)
            return -n->right->height;
        return 0;
    }

    struct node *llrotation(struct node *n) {
        struct node *p = n;
        struct node *tp = p->left;
        p->left = tp->right;
        tp->right = p;
        return tp;
    }

    struct node *rrrotation(struct node *n) {
        struct node *p = n;
        struct node *tp = p->right;
        p->right = tp->left;
        tp->left = p;
        return tp;
    }

    struct node *rlrotation(struct node *n) {
        struct node *p = n;
        struct node *tp = p->right;
        struct node *tp2 = p->right->left;
        p->right = tp2->left;
        tp->left = tp2->right;
        tp2->left = p;
        tp2->right = tp;
        return tp2;
    }

    struct node *lrrotation(struct node *n) {
        struct node *p = n;
        struct node *tp = p->left;
        struct node *tp2 = p->left->right;
        p->left = tp2->right;
        tp->right = tp2->left;
        tp2->right = p;
        tp2->left = tp;
        return tp2;
    }

    struct node *insert(struct node *r, int data) {
        if (r == NULL) {
            r = new struct node;
            r->data = data;
            r->left = r->right = NULL;
            r->height = 1;
            return r;
        }
        if (data < r->data)
            r->left = insert(r->left, data);
        else
            r->right = insert(r->right, data);

        r->height = calheight(r);

        if (bf(r) == 2 && bf(r->left) == 1)       r = llrotation(r);
        else if (bf(r) == -2 && bf(r->right) == -1) r = rrrotation(r);
        else if (bf(r) == -2 && bf(r->right) == 1)  r = rlrotation(r);
        else if (bf(r) == 2 && bf(r->left) == -1)   r = lrrotation(r);

        return r;
    }

    void levelorder_newline() {
        if (this->root == NULL) {
            cout << "\nEmpty tree\n";
            return;
        }
        levelorder_newline(this->root);
    }

    void levelorder_newline(struct node *v) {
        queue<struct node *> q;
        struct node *cur;
        q.push(v);
        q.push(NULL);
        while (!q.empty()) {
            cur = q.front();
            q.pop();
            if (cur == NULL && q.size() != 0) {
                cout << "\n";
                q.push(NULL);
                continue;
            }
            if (cur != NULL) {
                cout << " " << cur->data;
                if (cur->left != NULL)  q.push(cur->left);
                if (cur->right != NULL) q.push(cur->right);
            }
        }
    }

    struct node *deleteNode(struct node *p, int data) {
        if (p->left == NULL && p->right == NULL) {
            if (p == this->root) this->root = NULL;
            delete p;
            return NULL;
        }
        struct node *q;
        if (p->data < data)      p->right = deleteNode(p->right, data);
        else if (p->data > data) p->left  = deleteNode(p->left, data);
        else {
            if (p->left != NULL) {
                q = inpre(p->left);
                p->data = q->data;
                p->left = deleteNode(p->left, q->data);
            } else {
                q = insuc(p->right);
                p->data = q->data;
                p->right = deleteNode(p->right, q->data);
            }
        }

        if (bf(p) == 2 && bf(p->left) == 1)         p = llrotation(p);
        else if (bf(p) == 2 && bf(p->left) == -1)    p = lrrotation(p);
        else if (bf(p) == 2 && bf(p->left) == 0)     p = llrotation(p);
        else if (bf(p) == -2 && bf(p->right) == -1)  p = rrrotation(p);
        else if (bf(p) == -2 && bf(p->right) == 1)   p = rlrotation(p);
        else if (bf(p) == -2 && bf(p->right) == 0)   p = rrrotation(p);

        return p;
    }

    struct node *inpre(struct node *p) {
        while (p->right != NULL) p = p->right;
        return p;
    }

    struct node *insuc(struct node *p) {
        while (p->left != NULL) p = p->left;
        return p;
    }

    ~AVL() {}
};

int main() {
    AVL b;
    int c, x;
    do {
        cout << "\n1.Display levelorder on newline";
        cout << "\n2.Insert";
        cout << "\n3.Delete\n";
        cout << "\n0.Exit\n";
        cout << "\nChoice: ";
        cin >> c;
        switch (c) {
        case 1: b.levelorder_newline(); break;
        case 2:
            cout << "\nEnter no. "; cin >> x;
            b.root = b.insert(b.root, x);
            break;
        case 3:
            cout << "\nWhat to delete? "; cin >> x;
            b.root = b.deleteNode(b.root, x);
            break;
        case 0: break;
        }
    } while (c != 0);
}

上述代码的运行示例:

  1. 复制上面的代码并将其保存到名为“ avl.cpp.
  2. 编译代码:
g++ avl.cpp -o run
  1. 运行代码。
./run

C++ AVL 树的示例

AVL 树的优点

  • AVL 树的高度始终保持平衡,永远不会超过 log N。
  • 由于该树不会退化,因此搜索速度比普通的二叉搜索树更快。
  • 自平衡是自动的——无需重建步骤。
  • 确定性性能适用于实时系统和内存索引。

常见问题

AVL树是一种自平衡二叉搜索树,其中每个节点的平衡因子始终位于{-1, 0, +1}范围内。旋转操作会在每次插入或删除操作后恢复此不变性。ping 搜索、插入和删除操作的时间复杂度为 O(log n)。

节点的平衡因子等于左子树高度减去右子树高度。其值必须介于 {-1, 0, +1} 之间。平衡因子为 +2 或 -2 表示插入或删除操作导致该节点失去平衡,需要进行旋转操作。

这四种旋转分别是 LL、RR、LR 和 RL。LL 使用单次右旋转,RR 使用单次左旋转,而 LR 和 RL 是双重旋转,将子节点的一次旋转与主节点的反向旋转结合起来。

插入操作遵循标准的二叉搜索树规则,然后树向上回溯并更新高度。如果任何祖先节点破坏了平衡规则,则只需一次或两次旋转即可恢复平衡。每次插入操作最多只需要一次旋转。

AVL 树严格平衡,平衡因子至多为 1,因此查找速度更快。红黑树允许更宽松的平衡,这使得插入和删除操作的成本更低,但搜索速度略慢。数据库在处理写入密集型负载时更倾向于使用红黑树。

AVL 树为内存数据库索引、文件系统元数据、优先级队列、电话簿查找、拼写检查器以及任何需要确定性 O(log n) 搜索和范围查询的中序遍历的工作负载提供支持。

是的。人工智能系统使用AVL树来构建符号表、有序特征存储、进行kd树平衡以及在结构化数据上进行最近邻查找。它们也是智能搜索管道中排名检索索引的基础。

是的。GitHub Copilot 和类似的 AI 助手会搭建插入、删除和旋转等例程。 C++, Java 或 Python并生成单元测试,以验证平衡因子在每个操作中是否不变。

总结一下这篇文章: