深入理解 AVL 树
在二叉查找树(BST)的学习中,我们知道它能实现高效的插入、删除和查找操作(时间复杂度 O (log n)),但这种高效性依赖于树的 “平衡性”—— 如果插入的元素有序(如 1,2,3,4,5),BST 会退化为链表,时间复杂度骤降为 O (n)。为解决这一问题,计算机科学家 Adelson-Velsky 和 Landis 在 1962 年提出了AVL 树,它是最早的自平衡二叉查找树,通过严格控制左右子树的高度差,确保树始终保持平衡状态。
本文将从 AVL 树的核心概念出发,逐步拆解其实现逻辑,最终提供可直接运行的完整 C++ 代码,并通过实例解析关键操作的执行过程。
一、AVL 树的核心定义与性质
AVL 树本质上是一棵 “高度平衡” 的二叉查找树,除了满足 BST 的所有特性(左子树所有节点值 < 根节点值,右子树所有节点值 > 根节点值),还额外满足:
- 左右子树均为 AVL 树(递归定义);
- 左右子树的高度差(称为平衡因子)的绝对值不超过 1;
关键概念:平衡因子(BF)
平衡因子是 AVL 树维持平衡的核心指标,定义为:平衡因子(BF)= 右子树高度 - 左子树高度
根据 AVL 树的性质,合法的平衡因子取值只能是 -1、0、1:
- BF = -1:左子树比右子树高 1 层;
- BF = 0:左右子树高度相等;
- BF = 1:右子树比左子树高 1 层。
当插入或删除节点导致某节点的 BF 变为2 或 - 2时,树就会失衡,此时需要通过 “旋转” 操作恢复平衡。
二、AVL 树的节点结构设计
AVL 树的节点需要存储以下信息:
- 键值对(Key-Value):实现字典式的映射功能;
- 左 / 右子节点指针:构建二叉树结构;
- 父节点指针:便于插入后回溯更新平衡因子;
- 平衡因子(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; // 平衡因子:右高 - 左高// 构造函数:初始化节点AVLTreeNode(const pair<k, v> kv): _kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _bf(0) // 新节点初始平衡因子为0{}
};
三、AVL 树的核心操作:插入与平衡维护
插入是 AVL 树最复杂的操作,需分为 “查找插入位置”“插入节点”“回溯更新平衡因子”“旋转平衡” 四个步骤。
步骤 1:查找插入位置(遵循 BST 规则)
从根节点出发,根据键值大小向左 / 右子树遍历,直到找到空位置(即插入点),同时记录插入节点的父节点。
步骤 2:插入新节点
创建新节点,根据父节点的键值确定插入到左子树还是右子树,并建立父子节点的双向关联。
步骤 3:回溯更新平衡因子
插入节点后,需要从父节点开始向上回溯,依次更新祖先节点的平衡因子:
- 若新节点是父节点的左子节点:父节点左子树高度 + 1 → BF -= 1;
- 若新节点是父节点的右子节点:父节点右子树高度 + 1 → BF += 1。
根据更新后的 BF,分三种情况处理:
- BF = 0:说明插入后父节点左右子树高度相等,后续祖先节点 BF 不会变化,直接退出回溯;
- BF = ±1:说明父节点仍平衡,但祖先节点 BF 可能受影响,继续向上回溯;
- BF = ±2:说明当前子树失衡,需执行旋转操作恢复平衡。
步骤 4:旋转操作(核心平衡手段)
当节点 BF 为 ±2 时,根据失衡节点及其子节点的 BF 方向,分为四种失衡场景,对应四种旋转策略:
场景 1:左左失衡(BF=-2 + 子节点 BF=-1)
- 失衡原因:失衡节点的左子树过高,且左子树的左子树也过高(插入节点在左子树的左侧);
- 解决策略:右旋(以失衡节点为轴,向右旋转)。
// 右旋操作:处理左左失衡(参数parent为失衡节点,BF=-2) void revolveR(Node* parent) {Node* left_child = parent->_left; // 失衡节点的左孩子(旋转后成为新根)Node* left_right_child = left_child->_right; // 左孩子的右子树(需重新挂载)Node* grandparent = parent->_parent; // 失衡节点的父节点(祖父节点)// 1. 将左孩子的右子树挂载到失衡节点的左子树parent->_left = left_right_child;if (left_right_child != nullptr) {left_right_child->_parent = parent; // 维护父指针}// 2. 将失衡节点挂载到左孩子的右子树left_child->_right = parent;parent->_parent = left_child; // 维护父指针// 3. 处理与祖父节点的连接(若失衡节点是根节点,左孩子成为新根)if (grandparent == nullptr) {_root = left_child;left_child->_parent = nullptr;} else {// 判断失衡节点是祖父节点的左/右孩子,将左孩子挂载到对应位置if (grandparent->_left == parent) {grandparent->_left = left_child;} else {grandparent->_right = left_child;}left_child->_parent = grandparent;}// 4. 重置平衡因子(旋转后两者均平衡)parent->_bf = 0;left_child->_bf = 0; }
场景 2:右右失衡(BF=2 + 子节点 BF=1)
- 失衡原因:失衡节点的右子树过高,且右子树的右子树也过高(插入节点在右子树的右侧);
- 解决策略:左旋(以失衡节点为轴,向左旋转)
// 左旋操作:处理右右失衡(参数parent为失衡节点,BF=2)
void revolveL(Node* parent) {Node* right_child = parent->_right; // 失衡节点的右孩子(旋转后成为新根)Node* right_left_child = right_child->_left; // 右孩子的左子树(需重新挂载)Node* grandparent = parent->_parent; // 失衡节点的父节点(祖父节点)// 1. 将右孩子的左子树挂载到失衡节点的右子树parent->_right = right_left_child;if (right_left_child != nullptr) {right_left_child->_parent = parent; // 维护父指针}// 2. 将失衡节点挂载到右孩子的左子树right_child->_left = parent;parent->_parent = right_child; // 维护父指针// 3. 处理与祖父节点的连接(若失衡节点是根节点,右孩子成为新根)if (grandparent == nullptr) {_root = right_child;right_child->_parent = nullptr;} else {// 判断失衡节点是祖父节点的左/右孩子,将右孩子挂载到对应位置if (grandparent->_left == parent) {grandparent->_left = right_child;} else {grandparent->_right = right_child;}right_child->_parent = grandparent;}// 4. 重置平衡因子(旋转后两者均平衡)parent->_bf = 0;right_child->_bf = 0;
}
场景 3:左右失衡(BF=-2 + 子节点 BF=1)
- 失衡原因:失衡节点的左子树过高,但左子树的右子树过高(插入节点在左子树的右侧);
- 解决策略:先左旋后右旋(先对左子节点左旋,将场景转为 “左左失衡”,再对失衡节点右旋)。
// 左右双旋:处理左右失衡(先左旋左孩子,再右旋父节点)
void revolveLR(Node* parent) {Node* left_child = parent->_left; // 父节点的左孩子(待左旋)Node* mid_node = left_child->_right; // 中间节点(左孩子的右孩子)int mid_bf = mid_node->_bf; // 保存中间节点的BF(用于后续调整)// 第一步:对左孩子执行左旋,将左右失衡转为左左失衡revolveL(left_child);// 第二步:对父节点执行右旋,解决左左失衡revolveR(parent);// 根据中间节点的原始BF,调整三个节点的平衡因子if (mid_bf == 0) {parent->_bf = 0;left_child->_bf = 0;mid_node->_bf = 0;} else if (mid_bf == 1) {parent->_bf = 0;left_child->_bf = -1;mid_node->_bf = 0;} else if (mid_bf == -1) {parent->_bf = 1;left_child->_bf = 0;mid_node->_bf = 0;}
}
场景 4:右左失衡(BF=2 + 子节点 BF=-1)
- 失衡原因:失衡节点的右子树过高,但右子树的左子树过高(插入节点在右子树的左侧);
- 解决策略:先右旋后左旋(逻辑与 “左右失衡” 对称)。
// 右左双旋:处理右左失衡(先右旋右孩子,再左旋父节点)
void revolveRL(Node* parent) {Node* right_child = parent->_right; // 父节点的右孩子(待右旋)Node* mid_node = right_child->_left; // 中间节点(右孩子的左孩子)int mid_bf = mid_node->_bf; // 保存中间节点的BF(用于后续调整)// 第一步:对右孩子执行右旋,将右左失衡转为右右失衡revolveR(right_child);// 第二步:对父节点执行左旋,解决右右失衡revolveL(parent);// 根据中间节点的原始BF,调整三个节点的平衡因子if (mid_bf == 0) {parent->_bf = 0;right_child->_bf = 0;mid_node->_bf = 0;} else if (mid_bf == 1) {parent->_bf = -1;right_child->_bf = 0;mid_node->_bf = 0;} else if (mid_bf == -1) {parent->_bf = 0;right_child->_bf = 1;mid_node->_bf = 0;}
}
四、AVL 树的其他辅助操作
1. 中序遍历
AVL 树作为 BST,中序遍历结果为升序序列,可用于验证树的合法性
// 对外接口:中序遍历
void Inorder() {_inorder(_root);cout << endl;
}// 私有递归实现:左→根→右
void _inorder(Node* root) {if (root == nullptr) return;_inorder(root->_left); // 遍历左子树// 输出键值对(格式:键:值)cout << root->_kv.first << ":" << root->_kv.second << " ";_inorder(root->_right); // 遍历右子树
}
2. 树节点数统计
通过递归计算树的节点总数:
// 对外接口:获取节点总数
int size() {return _size(_root);
}// 私有递归实现:空树返回0,非空树=左子树节点数+右子树节点数+1
int _size(Node* root) {return (root == nullptr) ? 0 : (_size(root->_left) + _size(root->_right) + 1);
}
3. 平衡校验
验证树是否满足 AVL 树的性质(所有节点 BF 绝对值≤1,且 BF 计算值与存储值一致)
// 对外接口:校验是否为合法AVL树
bool isbalanenceTree() {return _IsBalaenceTree(_root);
}// 私有递归实现:平衡校验
bool _IsBalaenceTree(Node* root) {if (root == nullptr) return true; // 空树是平衡的// 计算当前节点的
4. 树的高度计算
树的高度是平衡因子计算的基础,通过递归获取左右子树的最大高度并加 1(当前节点)。
// 对外接口:获取树的高度
int height() {return _height(_root);
}// 私有递归实现:空树高度为0,非空树高度=max(左子树高度, 右子树高度)+1
int _height(Node* root) {if (root == nullptr) return 0;int left_h = _height(root->_left);int right_h = _height(root->_right);return left_h > right_h ? left_h + 1 : right_h + 1;
}
五、完整 C++ 代码与使用示例
5.1 完整代码
// 防止头文件重复包含,确保该头文件在一个编译单元中仅被编译一次
#pragma once
// 引入输入输出流库,用于控制台打印(如中序遍历结果输出)
#include<iostream>
// 引入数学库,用于计算绝对值(判断平衡因子是否合法)
#include<cmath>
// 引入断言库,用于调试时检查程序逻辑正确性(预留扩展)
#include<assert.h>
// 使用标准命名空间,避免频繁书写“std::”前缀
using namespace std;// AVL树是最先发明的自平衡二叉查找树,AVL是一颗空树,或者具备下列性质的二叉搜索树:
// 它的左右子树都是AVL树,且左右子树的高度差的绝对值不超过1。
// AVL树是一颗高度平衡搜索二叉树,通过控制高度差去控制平衡。// 定义AVL树的节点结构
// 模板参数 k:键(Key)的类型,需支持比较操作
// 模板参数 v:值(Value)的类型,与键对应存储
template<class k,class v>
struct AVLTreeNode//节点
{pair<k, v> _kv; // 存储键值对(Key-Value)AVLTreeNode<k, v>* _left; // 指向左子节点的指针AVLTreeNode<k, v>* _right; // 指向右子节点的指针AVLTreeNode<k, v>* _parent; // 指向父节点的指针(用于回溯更新平衡因子)size_t _bf; // 平衡因子 = 右子树高度 - 左子树高度(合法值:-1,0,1)// 节点构造函数:初始化键值对,指针置空,平衡因子初始化为0AVLTreeNode(const pair<k, v> kv):_kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _bf(0){}
};// AVL树类的实现
template<class k,class v>
class AVLTree
{// 类型别名:简化节点指针的书写typedef AVLTreeNode<k, v> Node;
public:// 中序遍历接口:对外提供统一调用入口void Inorder(){_inorder(_root);cout << endl;}// 统计树的节点总数int size(){return _size(_root);}// 计算树的高度int height(){return _height(_root);}// 校验AVL树的平衡性bool isbalanenceTree(){return _IsBalaenceTree(_root);}// 插入键值对// 返回值:true(插入成功),false(键已存在,插入失败)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->_left; // 插入值小于当前节点,向左子树查找}else if (cur->_kv.first < kv.first){parent = cur;cur = cur->_right; // 插入值大于当前节点,向右子树查找}else{return false; // 键已存在,插入失败}}// 插入新节点cur = new Node(kv);if (parent->_kv.first > kv.first){parent->_left = cur; // 作为左孩子插入}else{parent->_right = cur; // 作为右孩子插入}cur->_parent = parent; // 设置父指针// 更新平衡因子并处理失衡while (cur != _root){// 根据当前节点是父节点的左/右孩子更新父节点平衡因子if (parent->_left == cur){parent->_bf--; // 左子树高度增加,平衡因子减1}else if (parent->_right == cur){parent->_bf++; // 右子树高度增加,平衡因子加1}// 根据平衡因子判断后续操作if (parent->_bf == 0){// 平衡因子为0,说明父节点左右子树高度相等,无需继续向上更新break;}else if (parent->_bf == 1 || parent->_bf == -1){// 平衡因子为±1,仍保持平衡,但需继续向上更新祖先节点cur = parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2){// 平衡因子为±2,树已失衡,需要旋转处理if (parent->_bf == -2 && cur->_bf == -1){// 左左失衡:右旋revolveR(parent);}else if (parent->_bf == 2 && cur->_bf == 1){// 右右失衡:左旋revolveL(parent);}else if (parent->_bf == -2 && cur->_bf == 1){// 左右失衡:先左旋后右旋revolveLR(parent);}else if (parent->_bf == 2 && cur->_bf == -1){// 右左失衡:先右旋后左旋revolveRL(parent);}break; // 旋转后已平衡,无需继续向上更新}}return true;}private:// 中序遍历的递归实现void _inorder(Node* root){if (root == nullptr){return;}_inorder(root->_left); // 遍历左子树cout << root->_kv.first << ':' << root->_kv.second << ' ' << endl; // 访问当前节点_inorder(root->_right); // 遍历右子树}// 统计节点总数的递归实现int _size(Node* root){return (root == nullptr) ? 0 : (_size(root->_left) + _size(root->_right) + 1);}// 计算树高度的递归实现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; // 当前节点高度= max(左右子树高度)+1}// 平衡校验的递归实现bool _IsBalaenceTree(Node* root){if (root == nullptr){return true; // 空树是平衡的}int LeftHeight = _height(root->_left);int RightHeight = _height(root->_right);int bf = RightHeight - LeftHeight; // 计算实际平衡因子// 校验平衡因子是否合法且与存储值一致if (abs(bf) >= 2 && bf != root->_bf){return false;}// 递归校验左右子树return _IsBalaenceTree(root->_left) && _IsBalaenceTree(root->_right);}// 右旋操作:处理左左失衡// 参数parent:失衡节点(平衡因子为-2)void revolveR(Node* parent){Node* revolvel = parent->_left; // 失衡节点的左孩子Node* revolvelr = revolvel->_right; // 左孩子的右子树Node* parentparent = parent->_parent; // 失衡节点的父节点// 1. 将左孩子的右子树挂到失衡节点的左子树parent->_left = revolvelr;if (revolvelr != nullptr){revolvelr->_parent = parent;}// 2. 将失衡节点挂到左孩子的右子树revolvel->_right = parent;parent->_parent = revolvel;// 3. 处理与祖父节点的连接if (parentparent == nullptr){// 失衡节点是根节点,更新根节点_root = revolvel;revolvel->_parent = nullptr;}else{// 根据失衡节点是祖父节点的左/右孩子进行连接if (parentparent->_left == parent){parentparent->_left = revolvel;}else{parentparent->_right = revolvel;}revolvel->_parent = parentparent;}// 4. 重置平衡因子parent->_bf = revolvel->_bf = 0;}// 左旋操作:处理右右失衡// 参数parent:失衡节点(平衡因子为2)void revolveL(Node* parent){Node* revolvel = parent->_right; // 失衡节点的右孩子Node* revolvelr = revolvel->_left; // 右孩子的左子树Node* parentparent = parent->_parent; // 失衡节点的父节点// 1. 将右孩子的左子树挂到失衡节点的右子树parent->_right = revolvelr;if (revolvelr != nullptr){revolvelr->_parent = parent;}// 2. 将失衡节点挂到右孩子的左子树revolvel->_left = parent;parent->_parent = revolvel;// 3. 处理与祖父节点的连接if (parentparent == nullptr){// 失衡节点是根节点,更新根节点_root = revolvel;revolvel->_parent = nullptr;}else{// 根据失衡节点是祖父节点的左/右孩子进行连接if (parentparent->_left == parent){parentparent->_left = revolvel;}else{parentparent->_right = revolvel;}revolvel->_parent = parentparent;}// 4. 重置平衡因子parent->_bf = revolvel->_bf = 0;}// 左右双旋:处理左右失衡(先左旋左孩子,再右旋父节点)void revolveLR(Node* parent){Node* revolvel1 = parent->_left; // 父节点的左孩子Node* revolvelr1 = revolvel1->_right; // 左孩子的右孩子int bf = revolvelr1->_bf; // 保存中间节点的平衡因子// 先对左孩子进行左旋,再对父节点进行右旋revolveL(revolvel1);revolveR(parent);// 根据中间节点的平衡因子调整三个节点的平衡因子if (bf == 0){// 中间节点平衡因子为0:三个节点平衡因子均为0parent->_bf = 0;revolvel1->_bf = 0;revolvelr1->_bf = 0;}else if (bf == 1){// 中间节点平衡因子为1:父节点平衡因子0,左孩子平衡因子-1parent->_bf = 0;revolvel1->_bf = -1;revolvelr1->_bf = 0;}else if (bf == -1){// 中间节点平衡因子为-1:父节点平衡因子1,左孩子平衡因子0parent->_bf = 1;revolvel1->_bf = 0;revolvelr1->_bf = 0;}}// 右左双旋:处理右左失衡(先右旋右孩子,再左旋父节点)void revolveRL(Node* parent){Node* revolver1 = parent->_right; // 父节点的右孩子Node* revolverl1 = revolver1->_left; // 右孩子的左孩子int bf = revolverl1->_bf; // 保存中间节点的平衡因子// 先对右孩子进行右旋,再对父节点进行左旋revolveR(revolver1);revolveL(parent);// 根据中间节点的平衡因子调整三个节点的平衡因子if (bf == 0){// 中间节点平衡因子为0:三个节点平衡因子均为0parent->_bf = 0;revolver1->_bf = 0;revolverl1->_bf = 0;}else if (bf == 1){// 中间节点平衡因子为1:父节点平衡因子-1,右孩子平衡因子0parent->_bf = -1;revolver1->_bf = 0;revolverl1->_bf = 0;}else if (bf == -1){// 中间节点平衡因子为-1:父节点平衡因子0,右孩子平衡因子1parent->_bf = 0;revolver1->_bf = 1;revolverl1->_bf = 0;}}private:Node* _root = nullptr; // 根节点指针
};
5.2 使用示例
#include "AVLTree.h"
#include <iostream>
using namespace std;int main() {// 创建AVL树,键为int,值为stringAVLTree<int, string> avl;// 插入键值对(包含有序数据,测试平衡维护)avl.insert({3, "a"});avl.insert({2, "b"});avl.insert({1, "c"}); // 触发左左失衡,执行右旋avl.insert({4, "d"});avl.insert({5, "e"}); // 触发右右失衡,执行左旋avl.insert({6, "f"});avl.insert({7, "g"});avl.insert({10, "h"});avl.insert({9, "i"});avl.insert({8, "j"}); // 触发右左失衡,执行右左双旋// 中序遍历(应输出升序键值对)cout << "中序遍历结果:";avl.Inorder();// 输出树的基本信息cout << "节点总数:" << avl.size() << endl;cout << "树的高度:" << avl.height() << endl;cout << "是否为合法AVL树:" << (avl.isbalanenceTree() ? "是" : "否") << endl;return 0;
}
输出结果:
中序遍历结果:1:c 2:b 3:a 4:d 5:e 6:f 7:g 8:j 9:i 10:h
节点总数:10
树的高度:4
是否为合法AVL树:是
六、AVL 树的优缺点与适用场景
优点
- 高效稳定:插入、查找、删除的时间复杂度均为 O (logn),不受数据插入顺序影响;
- 有序性:中序遍历可直接得到升序序列,无需额外排序;
- 实现基础:是红黑树、Splay 树等高级平衡树的基础,理解 AVL 树有助于掌握其他平衡结构。
缺点
- 平衡维护复杂:插入 / 删除时需频繁更新平衡因子和执行旋转,性能开销略高于红黑树;
- 删除操作未实现:本文代码未包含删除逻辑(删除比插入更复杂,需处理更多失衡场景)。
适用场景
- 对查询效率要求高,且数据插入 / 删除频率适中的场景;
- 不允许树退化为链表,需严格保证 O (logn) 时间复杂度的场景(如数据库索引、有序映射表)。