解密AVL树:让二叉搜索树永远保持平衡
一. AVL树简介
1.1 基本概念
AVL树(Adelson-Velsky and Landis Tree)是计算机科学中最早发明的自平衡二叉搜索树(BST),由苏联数学家G. M. Adelson-Velsky和E. M. Landis于1962年提出。其核心特性是:
- 平衡性:任意节点的左右子树高度差绝对值不超过1(即平衡因子为-1、0或1)。
- 自平衡机制:通过树旋转(左旋、右旋、左右双旋、右左双旋)动态调整结构,确保插入、删除操作后树的高度保持对数级别(O(log n))。
- 时间复杂度:所有操作(查找、插入、删除)的时间复杂度均稳定在O(log n),避免了普通BST在极端情况下退化为链表(时间复杂度O(n))的问题。
1.2 AVL树的意义
- 解决BST的退化问题
BST在数据有序插入时可能退化为链表,导致操作效率骤降。AVL树通过强制平衡,确保树的高度始终可控,从而维持高效的动态操作性能。
2. 性能稳定性
在频繁插入、删除的场景中,AVL树的严格平衡约束使其性能波动远小于其他自平衡树(如红黑树),尤其适合对响应时间敏感的实时系统。
3. 算法基础
AVL树是理解更复杂平衡树(如红黑树、B树)的基石,其旋转操作和平衡因子设计思想被广泛借鉴。
1.3 AVL树的应用场景
- 数据库索引
- 作为索引结构,AVL树支持高效的查询、插入和删除操作,确保数据库在处理动态数据时保持快速响应。
- 例如,MySQL的InnoDB引擎早期版本曾使用AVL树管理索引。
- 文件系统与内存管理
- 文件系统通过AVL树管理目录和元数据,提升文件查找效率。
- 操作系统用AVL树管理空闲内存块,实现快速分配与释放。
- 网络路由与实时系统
- 路由表维护:AVL树快速更新路由信息,优化数据传输路径。
- 事件调度:高效管理定时任务,确保事件按预定时间触发。
- 词典与拼写检查
- 存储单词列表时,AVL树支持动态更新,适用于需要频繁增删词汇的场景。
- 符号表与动态集合
- 实现集合、映射等数据结构,保证插入、删除、查找操作的高效性。
1.4 AVL树的前景
- 严格平衡需求的场景
在需要绝对低延迟的金融交易系统、高频交易平台中,AVL树的稳定性能仍具优势。 - 与现代技术的结合
- 内存优化:通过压缩节点存储或缓存友好设计,减少空间开销。
- 并行计算:结合无锁数据结构,提升多线程环境下的并发性能。
- 教育与研究价值
AVL树作为平衡树的经典案例,仍是算法教学和研究的重点,其设计思想持续启发新数据结构的创新。 - 替代方案的局限性
尽管红黑树在插入/删除次数较少的场景中更高效,但AVL树在查询密集型任务中仍具竞争力。未来,随着硬件性能提升和算法优化,AVL树的应用场景有望进一步扩展。
1.5 总结:
AVL树通过严格的平衡约束和高效的旋转机制,为需要频繁动态更新的场景提供了可靠的性能保障。尽管存在实现复杂度和旋转开销的挑战,但其在大规模数据管理、实时系统及教育领域的应用潜力仍不可忽视。随着技术发展,AVL树或将在性能优化与场景适配中迎来新的发展机遇。
二. AVL树的实现
2.1 AVL树的结构
//AVL树节点结构
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)
{}
};
注意:AVL树插入的是pair类型结构数据。
2.2 插入
插入的过程还是按照二搜索树的规则进行插入,插入之后进行平衡因子的更新,
更新规则:
- 平衡因子=右子树高度-左子树高度
- 只有子树高度变化才会影响当前节点的平衡因子
- 插入节点后,会增加高度,新增节点在parent右子树,parent平衡因子++,新增在parent左子树,平衡因子–
- parent所在子树的高度是否变化决定是否需要向上更新
- 插入节点后,如果parent平衡因子是0,说明是在parent的本来就是低的子树插入,不影响parent的父节点的平衡因子,直接跳出循环即可。下面以图来展示过程,更清楚点,毕竟有图才有原貌。
通过上图可以看出当前增加13这个节点后父亲节点10的平衡因子由1变成0,10对应的父亲节点并不需要更新它为-1,平衡因子合理的取值为-1,0,1。
- 插入节点后,如果parent平衡因子是1或-1,说明是在parent之前平衡因子就是0,增加节点后子树高度变高或变低需要继续向上跟新,因为所在节点的节点的子树都发生变化。下面以图来展示过程,更清楚点,毕竟有图才有原貌。
通过上图可以看出新增节点16后,parent的平衡因子由0变为1,parent的父节点(10)的平衡因子由0变为1,所以需要继续向上更新。
3. 插入节点后,如果parent平衡因子是2或-2,说明是在parent之前平衡因子就是1或-1,增加节点后在子树高度变高本来就高的子树继续增加高度,导致不平衡,需要旋转处理。
下面将详细使用特定的场景来讲述如何正确使用旋转,使子树高度平衡。
通过上述分析伪代码如下(缺少旋转处理代码):
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->_left = cur;
}
else
{
parent->_right = cur;
}
cur->_parent = parent;
//更新平衡因子
while (parent)
{
if (parent->_left == cur)
{
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)
{
//旋转处理
break;
}
else
{
assert(false);
}
}
return true;
}
2.2.1 右单旋场景
旋转原则:
- 保持搜索树的规则
- 让旋转的树从不满足变成平衡,其次降低树的高度。
场景图:
- 过程:
- 用parent的左孩子指向subL,用subL的右孩子指向subLR
- 同时跟新三个节点的父亲,subL的父节点指向parent的父节点,subLR的父节点指向parent,parent的父节点指向subL,需额外注意subLR可能为空,指向之前判空。
- subL的右孩子指向parent,parent的左孩子指向subLR。
- 最后更新平衡因子即可。
伪代码如下:
//右单旋
void RotateR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if(subLR)
subLR->_parent = parent;
Node* ppNode = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
//if(ppNode == nullptr)
if (parent == _root)//如果根节点就是parent,subL就是根节点,直接进行赋值即可
{
_root = subL;
subL->_parent = nullptr;
}
else
{
if (ppNode->_left == parent)
{
ppNode->_left = subL;
}
else
{
ppNode->_right = subL;
}
subL->_parent = ppNode;
}
subL->_bf = 0;
parent->_bf = 0;
}
何时进行右单旋,当parent的平衡因子为-2且左孩子平衡因子为-1时,进行右单旋即可。
2.2.2 左单旋场景
场景图:
- 过程:
- 用parent的右孩子指向subR,用subR的左孩子指向subRL
- 同时跟新三个节点的父亲,subR的父节点指向parent的父节点,subRL的父节点指向parent,parent的父节点指向subR,需额外注意subRL可能为空,指向之前进行判空即可。
- subR的左孩子指向parent,parent的右孩子指向subRL。
- 判断parent是否是根节点,如果是直接让subR成为新的根节点;否则将subR的父节点指向parent之前的父节点,如果parent是parent父节点的左孩子,则将parent父节点左孩子指向subR,否则右孩子指向subR。
- 最后更新平衡因子即可,跟新完后直接跳出循环即可,旋转后该树已经是平衡得了。
伪代码如下:
//左单旋
void RotateL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
Node* ppNode = parent->_parent;
subR->_left = parent;
parent->_parent = subR;
if (parent == _root)
{
_root = subR;
subR->_parent = nullptr;
}
else
{
if (ppNode->_left == parent)
{
ppNode->_left = subR;
}
else
{
ppNode->_right = subR;
}
subR->_parent = ppNode;
}
subR->_bf = 0;
parent->_bf = 0;
}
何时进行左单旋,当parent的平衡因子为2且左孩子平衡因子为1时,进行左单旋即可。
2.2.3 左右双旋场景
场景图如下:
- 场景1:
新插入的节点为subLR的左孩子,parent,subL,subLR成折线型基本上以双旋进行解决该不平衡问题。现以parent的左孩子进行左单旋,然后再以parent节点进行右单旋即可,咱们直接调用接口即可,最后跟新平衡因子,以最初subLR的平衡因子进行判断更新,这步在代码中会体现出来。
再将上图以10节点进行右单旋即可。如下图:
- 场景2:
过程与上述一致,就是平衡因子的更新不同。
- 场景3:
旋转过程与上述一致,唯一不同是平衡因子的更新不同。
- 伪代码如下:
//左右双旋
void RotateLR(Node* parent)
{
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(parent->_left);
RotateR(parent);
if (bf == -1)
{
subLR->_bf = 0;
subL->_bf = 0;
parent->_bf = 1;
}
else if (bf == 1)
{
subLR->_bf = 0;
subL->_bf = -1;
parent->_bf = 0;
}
else if (bf == 0)
{
subLR->_bf = 0;
subL->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);
}
}
何时进行左右双旋,当parent的平衡因子为-2且左孩子平衡因子为-1时,进行左右双旋即可。
2.2.4 右左双旋场景
场景图:
-
场景1:
新插入的节点为subRL的左孩子,parent,subL,subLR成折线型基本上以双旋进行解决该不平衡问题。现以parent的右孩子进行右单旋,然后再以parent节点进行左单旋即可,咱们直接调用接口即可,最后跟新平衡因子,以最初subRL的平衡因子进行判断更新,这步在代码中会体现出来。 -
以parent的右孩子(15这个节点)进行右单旋后的图解:
-
场景2:
过程与上述一致,就是平衡因子的更新不同。 -
场景3:
过程与上述一致,就是平衡因子的更新不同。
如何区别不同场景下平衡因子的准确跟新,通过最初subRL的平衡因子判断即可。 -
伪代码如下:
//右左双旋
void RotateRL(Node* parent)
{
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(parent->_right);
RotateL(parent);
if (bf == -1)
{
subRL->_bf = 0;
subR->_bf = 1;
parent->_bf = 0;
}
else if (bf == 1)
{
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = -1;
}
else if (bf == 0)
{
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = 0;
}
else
{
assert(false);//其他的直接断言报错
}
}
何时进行右左双旋,当parent的平衡因子为2且左孩子平衡因子为-1时,进行右左双旋即可。
2.3 查找
查找过程与二叉搜索树相同,不同的是返回值,因为它是key/value结构,需要通过key修改对应value的值,伪代码如下:
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;
}
2.4 AVL树平衡检测
递归检查每个树左右高度差同时判断平衡因子是否异常即可,递归该过程即可。
- 求每个节点左右子树高度差
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)
{
// 空树也是AVL树
if (nullptr == root)
return true;
// 计算pRoot结点的平衡因子:即pRoot左右子树的高度差
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
int diff = rightHeight - leftHeight;
// 如果计算出的平衡因子与pRoot的平衡因子不相等,或者
// pRoot平衡因子的绝对值超过1,则一定不是AVL树
if (abs(diff) >= 2)
{
cout << root->_kv.first << "高度差异常" << endl;
return false;
}
if (root->_bf != diff)
{
cout << root->_kv.first << "平衡因子异常" << endl;
return false;
}
// pRoot的左和右如果都是AVL树,则该树一定是AVL树
return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}
对上述代码进行测试:
Test.cpp
#include<iostream>
#include<assert.h>
#include<vector>
using namespace std;
#include"AVLTree.h"
// 测试代码
void TestAVLTree1()
{
AVLTree<int, int> t;
// 常规的测试用例
//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
// 特殊的带有双旋场景的测试用例
int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
for (auto e : a)
{
/*if (e == 14)
{
int x = 0;
}*/
t.Insert({ e, e });
cout << "Insert:" << e << "->";
cout << t.IsBalanceTree() << endl;
}
t.InOrder();
cout << t.IsBalanceTree() << endl;
}
// 插入一堆随机值,测试平衡,顺便测试一下高度和性能等
void TestAVLTree2()
{
const int N = 1000000;
vector<int> v;
v.reserve(N);
srand(time(0));
for (size_t i = 0; i < N; i++)
{
v.push_back(rand() + i);
}
size_t begin2 = clock();
AVLTree<int, int> t;
for (auto e : v)
{
t.Insert(make_pair(e, e));
}
size_t end2 = clock();
cout << t.IsBalanceTree() << endl;
cout << "Insert:" << end2 - begin2 << endl;
cout << "Height:" << t.Height() << endl;
cout << "Size:" << t.Size() << endl;
size_t begin1 = clock();
// 确定在的值
/*for (auto e : v)
{
t.Find(e);
}*/
// 随机值
for (size_t i = 0; i < N; i++)
{
t.Find((rand() + i));
}
size_t end1 = clock();
cout << "Find:" << end1 - begin1 << endl;
}
int main()
{
//TestAVLTree1();
TestAVLTree2();
return 0;
}
该代码主要用于验证AVL树实现的正确性和性能,既包含边界条件测试(双旋转场景),也包含大规模压力测试,能够全面评估AVL树实现的质量。
三. 最后
本文系统阐述了AVL树的原理与实现,涵盖其自平衡特性、应用场景及代码验证。AVL树通过严格的平衡因子(-1/0/1)约束和四类旋转操作(单旋/双旋)维持O(log n)时间复杂度,适用于数据库索引、实时系统等对性能稳定性要求高的场景。实现部分详细解析了节点结构、插入时的平衡因子更新策略及四种旋转场景,并提供了查找和递归平衡检测方法。测试代码通过边界用例和百万级数据压力测试,验证了实现的正确性与效率,展现了AVL树在动态数据管理中的可靠性。