【C++进阶篇】AVL树的实现(赋源码)
解密AVL树:让二叉搜索树永远保持平衡
- 一. AVL树简介
- 1.1 基本概念
- 1.2 AVL树的意义
- 1.3 AVL树的应用场景
- 1.4 AVL树的前景
- 1.5 总结:
- 二. AVL树的实现
- 2.1 AVL树的结构
- 2.2 插入
- 2.2.1 右单旋场景
- 2.2.2 左单旋场景
- 2.2.3 左右双旋场景
- 2.2.4 右左双旋场景
- 2.3 查找
- 2.4 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树在动态数据管理中的可靠性。