一、AVL树概述:平衡的艺术
AVL树,这个名字来源于其发明者G.M. Adelson-Velsky 和E.M. Landis 两位前苏联科学家。作为最早的自平衡二叉搜索树,它在1962年的论文《An algorithm for the organization of information》中首次亮相,至今仍是数据结构课程中的重要内容。
AVL树的核心特性
AVL树要么是一棵空树,要么满足以下三个条件:
- 它是一棵二叉搜索树(BST)
- 它的左右子树都是AVL树
- 任意节点的左右子树高度差的绝对值不超过1
你可能会有疑问:为什么要求高度差不超过1,而不是完全平衡(高度差为0)呢?
答案是:物理上不可能。想象一下只有2个或4个节点的情况,无论如何都无法做到完全平衡。AVL树的这种设计是在可行性和性能之间找到的最佳平衡点。
平衡因子:AVL树的"风向标"
平衡因子(Balance Factor)是理解AVL树的关键:
平衡因子 = 右子树高度 - 左子树高度
对于AVL树,任何节点的平衡因子只能是-1、0或1。这个小小的数字就像风向标一样,告诉我们树是否倾斜以及倾斜的方向。
二、AVL树的结构设计
节点结构
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; // 平衡因子
AVLTreeNode(const pair<K, V>& kv)
: _kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
{}
};
为什么需要parent指针? 因为在更新平衡因子时,我们需要从新增节点向上回溯到根节点,parent指针使得这个操作变得高效。
树的结构框架
cpp
template<class K, class V>
class AVLTree {
typedef AVLTreeNode<K, V> Node;
public:
// 接口方法...
private:
Node* _root = nullptr;
};
三、AVL树的插入操作详解
插入操作是AVL树的核心,分为三个主要步骤:
3.1 标准BST插入
首先像普通二叉搜索树一样找到插入位置:
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; // 键已存在
}
}
// 创建新节点并链接
cur = new Node(kv);
if (parent->_kv.first < kv.first) {
parent->_right = cur;
} else {
parent->_left = cur;
}
cur->_parent = parent;
// 继续更新平衡因子...
}
3.2 平衡因子更新策略
更新平衡因子是AVL树平衡的关键,遵循以下原则:
-
更新规则:
- 新节点在parent右边:
parent->_bf++ - 新节点在parent左边:
parent->_bf--
- 新节点在parent右边:
-
三种终止情况:
cpp
while (parent) {
// 更新平衡因子
if (cur == parent->_left)
parent->_bf--;
else
parent->_bf++;
if (parent->_bf == 0) {
// 情况1:插入后子树高度不变,停止更新
break;
} else if (parent->_bf == 1 || parent->_bf == -1) {
// 情况2:子树高度变化,继续向上更新
cur = parent;
parent = parent->_parent;
} else if (parent->_bf == 2 || parent->_bf == -2) {
// 情况3:不平衡,需要旋转
// 旋转处理...
break;
} else {
assert(false); // 不应出现其他值
}
}
三种情况的直观理解:
- 平衡因子变为0:原来一边高一边低,新节点插在矮的一边,整棵树"变平"了,高度不变
- 平衡因子变为±1:原来两边一样高,现在一边高了,整棵树长高了
- 平衡因子变为±2:原来就一边高,新节点还插在高的一边,树"倾斜过度"了
四、AVL树的旋转操作
当平衡因子达到±2时,需要通过旋转来恢复平衡。AVL树有四种旋转方式:
4.1 右单旋(RR旋转)
适用场景:左边太高(平衡因子为-2)且左孩子的平衡因子为-1
cpp
void RotateR(Node* parent) {
Node* subL = parent->_left;
Node* subLR = subL->_right;
// 1. subLR成为parent的左孩子
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
// 2. parent成为subL的右孩子
Node* parentParent = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
// 3. subL连接到原parent的父节点
if (parentParent == nullptr) {
_root = subL;
subL->_parent = nullptr;
} else {
if (parent == parentParent->_left) {
parentParent->_left = subL;
} else {
parentParent->_right = subL;
}
subL->_parent = parentParent;
}
// 4. 更新平衡因子
parent->_bf = subL->_bf = 0;
}
旋转效果:
10 (bf=-2) 5
/ \ / \
5 15 ==> 3 10
/ \ / / \
3 8 a 8 15
/
a (新插入)
4.2 左单旋(LL旋转)
适用场景:右边太高(平衡因子为2)且右孩子的平衡因子为1
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 左右双旋(LR旋转)
适用场景:左边太高(平衡因子为-2)但左孩子的平衡因子为1
cpp
void RotateLR(Node* parent) {
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
// 先左旋subL,再右旋parent
RotateL(parent->_left);
RotateR(parent);
// 根据subLR原来的平衡因子更新平衡因子
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 右左双旋(RL旋转)
适用场景:右边太高(平衡因子为2)但右孩子的平衡因子为-1
代码实现与左右双旋对称,这里不再赘述。
五、AVL树的验证与性能
5.1 平衡性验证
实现完成后,我们需要验证AVL树的正确性:
cpp
int _Height(Node* root) {
if (root == nullptr) return 0;
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
return max(leftHeight, 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);
}
5.2 性能测试
对10万个随机数进行测试:
cpp
void TestAVLTree2() {
const int N = 100000;
vector<int> v(N);
srand(time(0));
for (size_t i = 0; i < N; i++) {
v[i] = rand() + i;
}
AVLTree<int, int> t;
// 插入测试
size_t begin2 = clock();
for (auto e : v) {
t.Insert(make_pair(e, e));
}
size_t end2 = clock();
cout << "插入耗时:" << end2 - begin2 << "ms" << endl;
cout << "是否平衡:" << t.IsBalanceTree() << endl;
cout << "树高度:" << t.Height() << endl;
// 查找测试
size_t begin1 = clock();
for (size_t i = 0; i < N; i++) {
t.Find(rand() + i);
}
size_t end1 = clock();
cout << "查找耗时:" << end1 - begin1 << "ms" << endl;
}
六、总结
AVL树的优势
- 严格的平衡:保证最坏情况下的时间复杂度为O(log N)
- 查找高效:对于查找密集型应用非常合适
- 结构稳定:不会退化成链表
AVL树的局限
- 维护成本高:每次插入删除都可能需要多次旋转
- 存储开销:每个节点需要额外存储平衡因子和父指针
- 删除复杂:删除操作比插入更复杂(本文未展开)
应用场景
- 数据库索引
- 内存中的有序数据存储
- 需要频繁查找但较少插入删除的场景
AVL树是理解自平衡数据结构的重要基础。虽然在实际应用中,红黑树可能更常见(因为维护成本较低),但学习AVL树能帮助我们深入理解平衡的概念和实现原理。
下期咱们将带来红黑树的详解~~~
