【C++】AVL树的模拟实现
目录
- 一、AVL树的概念
- 1.1 为什么需要 AVL 树?
- 1.2 AVL 树的核心:平衡因子
- 1.3 AVL 树的节点结构
- 二、AVL树的实现
- 2.1 AVL树的 insert 操作
- 2.1.2 平衡因子的更新
- 更新原则
- 更新停止条件
- 2.2 AVL树的 旋转 操作
- 2.2.1 旋转的原则
- 2.2.2 右单旋
- 2.2.3 左单旋
- 2.2.4 左右双旋
- 2.2.5 右左双旋
- 2.3 中序遍历
- 2.4 其它接口的实现
- 2.4.1 查找函数
- 2.4.2 Size 和 Height 函数
- 2.4.3 AVL树的判断函数
个人主页<—请点击
C++专栏<—请点击
一、AVL树的概念
1.1 为什么需要 AVL 树?
我们都知道二叉搜索树BST
。在理想情况下,BST
的搜索、插入、删除操作的时间复杂度是O(log n)
。但是,如果插入的数据是有序的(例如 1, 2, 3, 4, 5),BST
就会退化成一条链表,时间复杂度恶化到O(n)
。
AVL
树 就是为了解决这个问题而诞生的。它是一种 自平衡的二叉搜索树。它的核心思想是:在插入和删除节点时,通过一系列的 旋转 操作,始终保持树的左右子树高度大致相等,从而确保树的高度始终保持在 O(log n)
级别,进而保证所有操作的时间复杂度都是稳定的 O(log n)
。
1.2 AVL 树的核心:平衡因子
AVL
树的核心机制是 平衡因子。
-
平衡因子:对于树中的任意一个节点,它的平衡因子定义为 其左子树的高度减去其右子树的高度。
平衡因子 = 右子树高度 - 左子树高度
。 -
平衡条件:
AVL
树要求每个节点的平衡因子只能是-1, 0, 或 1
。如果任何一个节点的平衡因子的绝对值超过了1
,那么这个树就是 不平衡 的,需要通过旋转来恢复平衡。
1.3 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; //balance factorAVLTreeNode(const pair<K, V>& kv): _kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _bf(0){ }
};
如上图,我们采用了K-V
这一通用结构,使用 std::pair
存储键值对,父指针的作用:便于回溯更新平衡因子,简化旋转操作中的链接调整。
二、AVL树的实现
2.1 AVL树的 insert 操作
insert
操作也遵循二叉搜索树的规则,但和以往不同的是当插入一个结点时,我们需要关注平衡因子的变化,新插入的结点会影响祖先结点的平衡因子。所以我们需要维护更新从新增结点 -> 根节点
的平衡因子,更新平衡因子过程中可能会出现不平衡,这时要对不平衡子树进行旋转,旋转本质在调平衡的同时,也降低了子树的高度,不会再影响上一层,所以插入结束。
2.1.2 平衡因子的更新
更新原则
插入节点会影响parent
结点的平衡因子变化,根据公式 平衡因子 = 右子树高度 - 左子树高度
,新增节点是parent
结点的右子树时,parent
的平衡因子++
,否则--
。parent
所在子树的高度是否变化决定了是否向上更新。
更新停止条件
- 当更新后的
parent
结点的平衡因子是0
,说明是由1
或-1
变成的0
,那么这时候子树由一边高一边低变成了一样高,高度没有发生变化,不会影响parent
的祖先,插入结束。 - 当更新后的
parent
结点的平衡因子是1
或者-1
,说明更新之前是0
。那么这时候子树由一样高变成了一边高一边低,高度发生变化,会影响parent
的祖先,需要继续向上更新。 - 当更新后的
parent
结点的平衡因子是2
或者-2
,说明是由1 -> 2
或者-1 -> -2
,这时候子树的平衡被破坏了需要进行旋转操作,旋转的目标有两个:1、把parent
子树旋转平衡。2、降低parent
子树的高度,恢复到插入结点以前的高度。所以旋转后也不需要继续往上更新,插入结束。
了解到以上这些我们来实现一下 insert
操作。
bool insert(const pair<K, V>& kv)
{if (_root == nullptr){_root = new Node(kv);return true;}Node* parent = nullptr;Node* cur = _root;while (cur){parent = cur;if (kv.first > cur->_kv.first){cur = cur->_right;}else if(kv.first < cur->_kv.first){cur = cur->_left;}else{return false;}}cur = new Node(kv);cur->_bf = 0;if (cur->_kv.first > parent->_kv.first) parent->_right = cur;else parent->_left = cur;cur->_parent = parent;//更新维护平衡因子while (parent){if (parent->_right == 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){//旋转操作}else{//不是上述情况,说明这棵树之前就不是 AVL 树assert(false);}}return true;
}
如上就是除去旋转之外,插入函数的核心代码。代码中的插入部分和二叉搜索树一摸一样,就是最后需要维护平衡因子。
2.2 AVL树的 旋转 操作
2.2.1 旋转的原则
旋转的前提是要遵守搜索树的规则,其次要让不平衡的树变平衡,降低树的高度,旋转分为左单旋、右单旋、左右双旋、右左双旋
。
2.2.2 右单旋
旋转核心步骤,因为5 < b子树的值 < 10
,将b
变成10
的左子树,10
变成5
的右子树,5
变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的高度恢复到了插入之前的h+2
,高度没有变化,插入结束。
实现细节:当结点的链接更改时,不要忘记更改结点的_parent
指针,同时右单旋之后,parent
和subL
结点的平衡因子都是0
。
//右单旋
void RotateR(Node* parent)
{//parent 是平衡因子不符合规则的结点Node* subL = parent->_left;//相当于插入函数部分的 curNode* subLR = subL->_right;//进行旋转操作parent->_left = subLR;if (subLR)//维护 subLR 的父指针{subLR->_parent = parent;}Node* pparent = parent->_parent; //subL 之后要更改的父指针指向subL->_right = parent;parent->_parent = subL;//判断之前 parent 是什么角色, 便于更改 subL 的父指针if (parent == _root){_root = subL;_root->_parent = nullptr;}else{if (pparent->_left == parent){pparent->_left = subL;}else pparent->_right = subL;subL->_parent = pparent;}//维护平衡因子parent->_bf = subL->_bf = 0;
}
右单旋在AVL
树插入部分旋转处理的应用:
else if (parent->_bf == 2 || parent->_bf == -2)
{//旋转操作if (parent->_bf == -2 && cur->_bf == -1){//右单旋RotateR(parent);}break;
}
2.2.3 左单旋
旋转核心步骤,因为10 < b子树的值 < 15
,将b
变成10
的右子树,10
变成15
的左子树,15
变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的高度恢复到了插入之前的h+2
,子树的高度没有发生变化,插入结束。
代码实现的细节部分和右单旋一样。
//左单旋
void RotateL(Node* parent)
{//parent 是平衡因子不符合规则的结点Node* subR = parent->_right; //相当于插入函数部分的 curNode* subRL = subR->_left;//进行旋转操作parent->_right = subRL;if (subRL)//维护 subRL 的父指针{subRL->_parent = parent;}Node* pparent = parent->_parent; //subR 之后要更改的父指针指向subR->_left = parent;parent->_parent = subR;//判断之前 parent 是什么角色, 便于更改 subR 的父指针if (parent == _root){_root = subR;subR->_parent = nullptr;}else{if (pparent->_left == parent){pparent->_left = subR;}else pparent->_right = subR;subR->_parent = pparent;}//维护平衡因子parent->_bf = subR->_bf = 0;
}
左单旋在AVL
树插入部分旋转处理的应用:
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);}break;
}
2.2.4 左右双旋
情况一::h >= 1
时,新增结点插入在e
子树,e
子树高度从h-1
变为h
,并不断更新8->5->10
平衡因子,引发旋转,其中8
的平衡因子为-1
,先以5
为旋转点进行一个左单旋,再以10
为旋转点进行一个右单旋,这棵树就平衡了,旋转后8
和5
平衡因子为0
,10
平衡因子为1
。
情况二:h >= 1
时,新增结点插入在f
子树,f
子树高度从h-1
变为h
并不断更新8->5->10
平衡因子,引发旋转,其中8
的平衡因子为1
,先以5
为旋转点进行一个左单旋,再以10
为旋转点进行一个右单旋,这棵树就平衡了,旋转后8
和10
平衡因子为0
,5
平衡因子为-1
。
情况三:h == 0
时,a/b/c
都是空树,b
自己就是一个新增结点,不断更新5->10
平衡因子,引发旋转,其中8
的平衡因子为0
,先以5
为旋转点进行一个左单旋,再以10
为旋转点进行一个右单旋,这棵树就平衡了,旋转后8
和10
和5
平衡因子均为0
。
搞清楚左右双旋的三种情况之后就可以实现代码了。
// 左右双旋
void RotateLR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf; // 维护平衡因子的关键点RotateL(subL);RotateR(parent);if (bf == 0){parent->_bf = subL->_bf = subLR->_bf = 0;}else if (bf == -1){parent->_bf = 1;subL->_bf = subLR->_bf = 0;}else if (bf == 1){parent->_bf = 0;subL->_bf = -1;subLR->_bf = 0;}else{assert(false);}
}
左右双旋在AVL
树插入部分旋转处理的应用:
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);}break;
}
2.2.5 右左双旋
情况一:h >= 1
时,新增结点插入在e
子树,e
子树高度从h-1
变为h
,并不断更新12->15->10
平衡因子,引发旋转,其中12
的平衡因子为-1
,先以15
为旋转点进行一个右单旋,再以10
为旋转点进行一个左单旋,这棵树就平衡了,旋转后10
和12
平衡因子为0
,15
平衡因子为1
。
情况二:h >= 1
时,新增结点插入在f
子树,f
子树高度从h-1
变为h
,并不断更新12->15->10
平衡因子,引发旋转,其中12
的平衡因子为1
,先以15
为旋转点进行一个右单旋,再以10
为旋转点进行一个左单旋,这棵树就平衡了,旋转后15
和12
平衡因子为0
,10
平衡因子为-1
。
情况三:h == 0
时,a/b/c
都是空树,b
自己就是一个新增结点,不断更新15->10
平衡因子,引发旋转,其中12
的平衡因子为0
,先以15
为旋转点进行一个右单旋,再以10
为旋转点进行一个左单旋,这棵树就平衡了,旋转后10
和12
和15
平衡因子均为0
。
和左右双旋一样,搞清楚右左双旋的三种情况,就可以实现代码了。
// 右左双旋
void RotateRL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf; // 维护平衡因子的关键点RotateR(subR);RotateL(parent);if (bf == 0){parent->_bf = subR->_bf = subRL->_bf = 0;}else if (bf == -1){parent->_bf = 0;subR->_bf = 1;subRL->_bf = 0;}else if (bf == 1){parent->_bf = -1;subR->_bf = subRL->_bf = 0;}else{assert(false);}
}
右左双旋在AVL
树插入部分旋转处理的应用:
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);}else{assert(false);}break;
}
2.3 中序遍历
public:void InOrder(){_InOrder(_root);cout << endl;}
private:void _InOrder(Node* root){if (root == nullptr)return;_InOrder(root->_left);cout << root->_kv.first << " ";_InOrder(root->_right);}
测试代码:
void TestAVLTree1()
{AVLTree<int, int> t;// 特殊的带有双旋场景的测试用例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 });}t.InOrder();
}
测试结果:
2.4 其它接口的实现
2.4.1 查找函数
Node* Find(const K& key)
{Node* cur = _root;while (cur){if (key > cur->_kv.first)cur = cur->_right;else if (key < cur->_kv.first)cur = cur->_left;elsereturn cur;}return nullptr;
}
2.4.2 Size 和 Height 函数
public:int Size(){return _Size(_root);}int Height(){return _Height(_root);}
private: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 leftH = _Height(root->_left);int rightH = _Height(root->_right);return leftH > rightH ? leftH + 1 : rightH + 1;}
2.4.3 AVL树的判断函数
public:bool IsBalanceTree(){return _IsBalanceTree(_root);}
private:bool _IsBalanceTree(Node* root){if (root == nullptr){return true;}int leftH = _Height(root->_left);int rightH = _Height(root->_right);int bf = rightH - leftH;if (abs(bf) > 2 || bf != root->_bf){cout << root->_kv.first << ":" << root->_kv.second << "平衡因子异常" << endl;return false;}return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);}
测试代码:
void TestAVLTree2()
{AVLTree<int, int> t;// 特殊的带有双旋场景的测试用例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 << "->" << t.IsBalanceTree() << endl;}t.InOrder();cout << t.IsBalanceTree() << endl;cout << t.Size() << " " << t.Height() << endl;cout << t.Find(2) << " " << t.Find(17) << endl;
}
测试结果:
总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~