【C++】AVL树完全解析:从平衡因子到四种旋转

引言

二叉搜索树在数据有序插入时会退化为单支树,导致查找、插入、删除的时间复杂度从预期的 O(log N)恶化至 O(N)。为了解决这一问题,1962 年苏联科学家 G.M. Adelson-Velsky 和 E.M. Landis 在论文《An algorithm for the organization of information》中首次提出了自平衡二叉搜索树 ,后以其名字首字母命名为 AVL 树

AVL 树通过引入平衡因子 (balance factor)来监控每个节点的左右子树高度差,并在插入或删除导致不平衡时通过旋转操作使树恢复平衡,从而保证所有操作的时间复杂度稳定在 O(\\log N)。本文将从 AVL 树的基本概念出发,详细讲解其节点结构、插入算法、平衡因子更新规则,以及四种旋转(左单旋、右单旋、左右双旋、右左双旋)的具体实现与平衡因子修正细节,最后给出平衡检测和查找的实现。


目录

引言

[一、AVL 树的概念与性质](#一、AVL 树的概念与性质)

[1.1 定义](#1.1 定义)

[1.2 为什么不能要求高度差为 0?](#1.2 为什么不能要求高度差为 0?)

[1.3 AVL 树的性能](#1.3 AVL 树的性能)

[二、AVL 树的节点结构](#二、AVL 树的节点结构)

[三、AVL 树的插入](#三、AVL 树的插入)

[3.1 平衡因子的更新规则](#3.1 平衡因子的更新规则)

[3.2 插入代码框架](#3.2 插入代码框架)

四、旋转操作

[4.1 右单旋(RotateR)](#4.1 右单旋(RotateR))

[4.2 左单旋(RotateL)](#4.2 左单旋(RotateL))

[4.3 左右双旋(RotateLR)](#4.3 左右双旋(RotateLR))

[4.4 右左双旋(RotateRL)](#4.4 右左双旋(RotateRL))

[五、AVL 树的查找](#五、AVL 树的查找)

[六、AVL 树的平衡检测](#六、AVL 树的平衡检测)

七、关于删除的说明

总结


一、AVL 树的概念与性质

1.1 定义

一棵 AVL 树要么是空树,要么满足以下两个条件:

  1. 其左子树和右子树都是 AVL 树;

  2. 左子树和右子树的高度之差的绝对值不超过 1。

这里的高度差通常记为 平衡因子(balance factor):

bf=右子树高度−左子树高度bf=右子树高度−左子树高度

因此,任意节点的平衡因子只能取 -1、0 或 1。

1.2 为什么不能要求高度差为 0?

高度差为 0(即完全平衡)是一种理想状态,但在某些节点数量下无法实现。例如:

  • 树中只有 2 个节点时,必然一个为根、另一个为左或右孩子,高度差为 1;

  • 树中有 4 个节点时,无论如何构造,高度差至少为 1。

因此 AVL 树退而求其次,允许高度差为 1,仍能保证树的高度接近 \\lceil \\log_2(N+1) \\rceil,从而获得对数级的操作效率。

1.3 AVL 树的性能

AVL 树是高度平衡的二叉搜索树,其高度严格控制在 O(\\log N),因此增删查改的时间复杂度均为 O(\\log N),相比普通二叉搜索树的最坏 O(N) 有了本质提升。


二、AVL 树的节点结构

为了实现平衡因子的更新和旋转操作,每个节点需要存储指向父节点的指针。节点中同时存储键值对(pair<K, V>)以及左右孩子指针和平衡因子。

cpp

复制代码
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)
    {}
};

树的封装类 AVLTree 仅需维护一个根节点指针:

cpp

复制代码
template<class K, class V>
class AVLTree {
    typedef AVLTreeNode<K, V> Node;
public:
    // 公有接口(插入、查找、平衡检测等)
private:
    Node* _root = nullptr;
};

三、AVL 树的插入

AVL 树的插入分为三个步骤:

  1. 按照二叉搜索树的规则插入新节点;

  2. 更新从新节点到根节点路径上所有祖先的平衡因子;

  3. 若某祖先的平衡因子绝对值变为 2,则通过旋转使其恢复平衡,并结束更新(因为旋转后子树高度恢复原状,不再影响上层)。

3.1 平衡因子的更新规则

设新插入节点为 cur,其父节点为 parent。插入后:

  • curparent 的左孩子,则 parent->_bf--

  • curparent 的右孩子,则 parent->_bf++

更新后检查 parent 的平衡因子:

  • parent->_bf == 0 :说明更新前 parent 的平衡因子为 ±1(一边高),插入节点在较矮的一侧,使子树高度不变。停止向上更新

  • parent->_bf == ±1 :说明更新前 parent 的平衡因子为 0(左右等高),插入后一侧变高,子树高度增加 1,需要继续向上更新。

  • parent->_bf == ±2 :说明更新前 parent 的平衡因子为 ±1,插入节点在已经较高的一侧,导致不平衡。需要进行旋转,旋转后子树高度恢复原状,停止向上更新。

3.2 插入代码框架

cpp

复制代码
bool Insert(const pair<K, V>& kv) {
    if (_root == nullptr) {
        _root = new Node(kv);
        return true;
    }

    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;   // key 已存在
        }
    }

    cur = new Node(kv);
    if (parent->_kv.first < kv.first)
        parent->_right = cur;
    else
        parent->_left = cur;
    cur->_parent = parent;

    // 更新平衡因子并处理不平衡
    while (parent) {
        if (cur == parent->_left)
            parent->_bf--;
        else
            parent->_bf++;

        if (parent->_bf == 0) {
            break;
        } else if (parent->_bf == 1 || parent->_bf == -1) {
            cur = parent;
            parent = parent->_parent;
        } else if (parent->_bf == 2 || parent->_bf == -2) {
            // 旋转处理
            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);     // 右左双旋
            break;
        } else {
            assert(false); // 平衡因子异常
        }
    }
    return true;
}

四、旋转操作

旋转的目标:

  1. 保持二叉搜索树的规则(左 < 根 < 右);

  2. 使不平衡的子树恢复平衡;

  3. 降低子树高度,使其恢复到插入前的高度。

4.1 右单旋(RotateR)

适用场景parent->_bf == -2cur->_bf == -1,即左边过高且左孩子的左边也过高(纯左边高)。

抽象模型 :设 parent 为 10,其左孩子 subL 为 5,subL 的右子树为 subLR(高度为 h)。在 subL 的左子树(a)中插入新节点导致 a 高度变为 h+1,引发不平衡。

旋转步骤

  • subLR 变为 parent 的左孩子;

  • parent 变为 subL 的右孩子;

  • subL 成为新的子树根;

  • 更新父指针连接;

  • 最后将 parentsubL 的平衡因子设为 0。

代码实现

cpp

复制代码
void RotateR(Node* parent) {
    Node* subL = parent->_left;
    Node* subLR = subL->_right;

    parent->_left = subLR;
    if (subLR)
        subLR->_parent = parent;

    Node* parentParent = parent->_parent;
    subL->_right = parent;
    parent->_parent = subL;

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

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

4.2 左单旋(RotateL)

适用场景parent->_bf == 2cur->_bf == 1,即右边过高且右孩子的右边也过高(纯右边高)。

对称步骤

  • subRL 变为 parent 的右孩子;

  • parent 变为 subR 的左孩子;

  • subR 成为新根;

  • 平衡因子清零。

代码实现

cpp

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

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

    Node* parentParent = parent->_parent;
    subR->_left = parent;
    parent->_parent = subR;

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

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

4.3 左右双旋(RotateLR)

适用场景parent->_bf == -2cur->_bf == 1,即左边过高,但左孩子的右边过高(非纯左边高)。此时单次右旋无法解决,需要先对左孩子左旋,再对 parent 右旋。

抽象分析 :设 parent = 10,subL = 5,subLR = 8(即 subL 的右孩子)。插入位置可能在 subLR 的左子树(e)或右子树(f),也可能 subLR 本身就是新增节点(h = 0)。三种情况下旋转后的平衡因子不同。

旋转步骤

  1. subL 执行左单旋(RotateL(parent->_left));

  2. parent 执行右单旋(RotateR(parent));

  3. 根据 subLR 原来的平衡因子(bf)调整 subLparentsubLR 的平衡因子。

平衡因子修正规则 (记 bf = subLR->_bf):

  • bf == 0:所有节点平衡因子均为 0。

  • bf == -1:说明插入在 e 子树,旋转后 parent->_bf = 1subL->_bf = 0subLR->_bf = 0

  • bf == 1:说明插入在 f 子树,旋转后 subL->_bf = -1parent->_bf = 0subLR->_bf = 0

代码实现

cpp

复制代码
void RotateLR(Node* parent) {
    Node* subL = parent->_left;
    Node* subLR = subL->_right;
    int bf = subLR->_bf;

    RotateL(parent->_left);
    RotateR(parent);

    if (bf == 0) {
        subL->_bf = 0;
        subLR->_bf = 0;
        parent->_bf = 0;
    } else if (bf == -1) {
        subL->_bf = 0;
        subLR->_bf = 0;
        parent->_bf = 1;
    } else if (bf == 1) {
        subL->_bf = -1;
        subLR->_bf = 0;
        parent->_bf = 0;
    } else {
        assert(false);
    }
}

4.4 右左双旋(RotateRL)

适用场景parent->_bf == 2cur->_bf == -1,即右边过高,但右孩子的左边过高。

旋转步骤

  1. subR 执行右单旋(RotateR(parent->_right));

  2. parent 执行左单旋(RotateL(parent));

  3. 根据 subRL 原来的平衡因子调整。

平衡因子修正 (记 bf = subRL->_bf):

  • bf == 0:全为 0。

  • bf == 1parent->_bf = -1subR->_bf = 0subRL->_bf = 0

  • bf == -1subR->_bf = 1parent->_bf = 0subRL->_bf = 0

代码实现

cpp

复制代码
void RotateRL(Node* parent) {
    Node* subR = parent->_right;
    Node* subRL = subR->_left;
    int bf = subRL->_bf;

    RotateR(parent->_right);
    RotateL(parent);

    if (bf == 0) {
        subR->_bf = 0;
        subRL->_bf = 0;
        parent->_bf = 0;
    } else if (bf == 1) {
        subR->_bf = 0;
        subRL->_bf = 0;
        parent->_bf = -1;
    } else if (bf == -1) {
        subR->_bf = 1;
        subRL->_bf = 0;
        parent->_bf = 0;
    } else {
        assert(false);
    }
}

五、AVL 树的查找

查找逻辑与普通二叉搜索树完全相同,时间复杂度 O(\\log N)

cpp

复制代码
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 树的平衡检测

为了验证实现的 AVL 树是否正确,需要编写递归函数计算每个节点的左右子树高度,并检查:

  1. 平衡因子绝对值是否小于 2;

  2. 节点存储的平衡因子是否等于实际右高减左高。

cpp

复制代码
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 (root == nullptr) return true;

    int leftHeight = _Height(root->_left);
    int rightHeight = _Height(root->_right);
    int diff = rightHeight - leftHeight;

    if (abs(diff) >= 2) {
        cout << root->_kv.first << "高度差异常" << endl;
        return false;
    }
    if (root->_bf != diff) {
        cout << root->_kv.first << "平衡因子异常" << endl;
        return false;
    }
    return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}

测试时可用随机数据大量插入,验证平衡性和高度是否接近 \\log N


七、关于删除的说明

AVL 树的删除操作同样需要更新平衡因子并进行旋转,其复杂度与插入相当,但实现更为繁琐。由于篇幅和常见教学安排,本文未给出删除的实现,有兴趣的读者可参考经典教材如《殷人昆 数据结构:用面向对象方法与 C++ 语言描述》。


总结

AVL 树是历史上第一种自平衡二叉搜索树,通过在每个节点上维护平衡因子(左右子树高度差)并在插入导致不平衡时执行四种旋转(左单旋、右单旋、左右双旋、右左双旋),使树始终保持高度平衡。其插入、查找、删除的时间复杂度稳定为 O(\\log N),相比普通二叉搜索树具有决定性的性能优势。

理解 AVL 树的旋转机制和平衡因子更新规则,是掌握更复杂的平衡树(如红黑树)的基础。在实际工程中,C++ 标准库的 std::mapstd::set 通常使用红黑树而非 AVL 树(因为红黑树插入删除旋转次数更少,常数更小),但 AVL 树在查找密集的场景下仍有一定优势。无论如何,AVL 树的设计思想------通过局部旋转保持全局平衡------是数据结构领域的重要里程碑。

相关推荐
苏三说技术1 小时前
Claude Code从失控到起飞,只用了这些技巧
后端
天衍四九2 小时前
Git从0到实战(四):冲突解决与版本回退 —— 别怕,出错了也能救
github
长栎2 小时前
写 for 循环写了十年,你却从没用过迭代器模式最狠的那一面
后端
LiaCode2 小时前
Redis 在生产项目的使用
前端·后端
用户559822481222 小时前
Docker Compose Down 导致容器数据误删——ext4 日志恢复全记录
后端
LiaCode2 小时前
一天学完 redis 的爽翻版核心知识总结
前端·后端
大刚测试开发实战2 小时前
如何内网穿透访问本地私有化部署的TestHub
前端·后端·github
xiaodaoluanzha3 小时前
迄今為止,最簡單的編程語言 Nolang
前端·后端
Csvn3 小时前
Docker 容器管理入门 — 从镜像到容器编排
后端
用户762352425913 小时前
ShardingJDBC
后端