STL库——AVL树
ʕ • ᴥ • ʔ
づ♡ど
🎉 欢迎点赞支持🎉
个人主页:励志不掉头发的内向程序员;
专栏主页:C++语言;
文章目录
前言
一、AVL 的概念
二、AVL 树的实现
2.1、AVL 树的结构
2.2、AVL 树的插入
(1)AVL 树插入一个值的大概过程
(2)平衡因子更新
(3)插入结点及更新平衡因子的代码实现
2.3、旋转
(1)旋转的原则
(2)右单旋
(3)左单旋
(4)左右双旋
(5)右左双旋
2.4、AVL 树查找
2.5、AVL 树平衡检测
2.6、AVL 树的删除
总结
前言
本章节我们来讲讲 AVL 树是怎么实现的,这一章节讲解的内容比较复杂,主要在于旋转,大家在学习的过程中一定要多多画图去感悟其精髓,事不宜迟,我们赶紧来看看吧。
一、AVL 的概念
AVL 树是最先发明的自平衡二叉查找树,AVL 是一颗空树,或者具备下列性质的二叉搜索树:它的左右子树都是 AVL 树,且左右子树的高度差的绝对值不超过 1。AVL 树是一颗高度平衡搜索二叉树,通过控制高度差去控制平衡。
AVL 树得名于它的发明者 G.M.Adelson-Velsky 和 E.M.Landis 是两个前苏联的科学家,他们在 1962 年的论文《An algorithm for the organization of information》中发表了它。
AVL 树的实现在这里我们引入一个平衡因子(balance factor)的概念,每一个结点都有一个平衡因子,任何结点的平衡因子等于右子树的高度减去左子树的高度,也就是说任何结点的平衡因子等于 0/1/-1,AVL 树并不是必须要平衡因子,但是有了平衡因子可以更方便我们去进行观察和控制树是否平衡,就像一个风向标一样。
我们让这个二叉树的高度差全部为 0 是最好的情况,但是确实没有办法实现的,如果我们只有 2、4 个结点等情况下,那高度差就不可能会是 0。
AVL 树整体结点数量和分布和完全二叉树类型,高度可以控制在 logN,那么增删查改的效率也可以控制在 O(logN),相比二叉搜索树有了本质的提升。
二、AVL 树的实现
2.1、AVL 树的结构
我们 AVL 树的基本结构和二叉搜索树差不多,在构建结点时,我们可以引入 pair 模板,来减少变量,同时增加一个平衡因子变量。同时我们要在原本二叉搜索树的基础上,增加一个 parent 指针指向上一个结点,这是因为我们有平衡因子,在我们插入时要对平衡因子进行修改,所以如果不知道上一个结点就会很糟糕了。
namespace zxl
{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){ }};template <class K, class V>class AVLTree{typedef AVLTreeNode<K, V> Node;public:private:Node* _root = nullptr;};
}
2.2、AVL 树的插入
(1)AVL 树插入一个值的大概过程
- 插入一个值按二叉搜索树的规则进行插入。
- 新增结点后,只会影响祖先结点的高度,也就是可能会影响部分祖先结点的平衡因子,所以更新从新增结点 -> 根结点路径上的平衡因子,实际中最坏情况下要更新到根,有些情况更新到中间及可以停止了,具体情况我们下面再详细分析。
- 更新平衡因子过程中没有出现,则插入结束。
- 更新平衡因子过程中出现不平衡,对不平衡子树旋转,旋转本质调平衡的同时,本质降低了子树的高度,不会再影响上一层,所以插入结束。
(2)平衡因子更新
更新原则:
- 平衡因子 = 右子树高度 - 左子树高度(反过来也是可以的,逻辑是一样的)。
- 只有子树高度变化才会影响当前结点的平衡因子。
- 插入结点会增加高度,所以新增结点在 parent 的右子树,parent 的 平衡因子 ++,新增结点在 parent 的左子树,parent 平衡因子 --。
- parent 所在子树的高度是否变化决定了是否会继续往上更新。
更新停止条件:
更新后 parent 的平衡因子等于 0,更新中 parent 的平衡因子变化为 -1 -> 0 或 1 -> 0,说明更新前 parent 子树一边高一边低,新增的结点插入在低的那边,插入后 parent 所在子树高度不变,不会影响 parent 的父亲结点的平衡因子,更新结束。
更新后 parent 的平衡因子等于 1 或 -1,更新前更新中 parent 的平衡因子变化为 0 -> 1 或 0 ->-1,说明更新前 parent 子树两边一样高,新增的插入结束后,parent 所在的子树一边高一边低,parent 所在的子树符合平衡要求,但是高度增加了 1,会影响 parent 的父亲结点的平衡因子,所以要继续向上更新。
更新后 parent 的平衡因子等于 2 或 -2,更新前更新中 parent 的平衡因子变化为 1 -> 2 或 -1->-2,说明更新前 parent 子树一边高一边低,新增的插入结点在高的那边,parent 所在的子树高的那边更高了,破坏了平衡,parent 所在的子树不符合平衡要求,需要旋转处理,旋转的目标有两个:把 parent 子树旋转平衡;降低 parent 子树高度,恢复到插入结点以前的高度。所以旋转后也不需要继续往上更新,插入结束。
不断更新,更新到根,根的平衡因子是 1 或 -1 也停止了。
更新到 10 结点,平衡因子为 2,10 所在的子树已经不平衡,需要旋转处理。
更新到中间结点,3 为根的子树高度不变,不会影响上一层,更新结束。
最坏更新到根停止。
(3)插入结点及更新平衡因子的代码实现
从上面我们可以总结出这么几点:
- 如果我们的 parent 结点从 -1 -> 0 或 1 -> 0,我们的平衡因子就可以结束更新。
- 如果我们的 parent 结点从 0 -> -1 或 0 -> 1,我们就得继续往上更新,一直更新到我们的 parent 结点的平衡因子变成 0 或者更新到根结点时停止。
- 如果我们的 parent 结点从 -1 -> -2 或 1 -> 2,我们此时平衡因子出现问题,开始旋转操作,所以我们不可能出现从 -2 -> -1 或 2 -> 1。因为平衡因子为 2 或 -2 时就不平衡了。
- 如果是在 parent 右边插入的值平衡因子 ++,左边插入的值平衡因子 --。
这就是我们更新平衡因子的全部操作。
bool insert(const pair<K, V>& kv)
{if (_root == nullptr){_root = new Node(kv);return true;}Node* cur = _root;Node* parent = nullptr;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;while (parent){if (cur == parent->_left)parent->_bf--;elseparent->_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.3、旋转
(1)旋转的原则
- 保持搜索树的规则。
- 让旋转的树从不满足变平衡,其次降低旋转树的高度。
(2)右单旋
如图展示的是 10 为根的树,有 a/b/c 抽象为三棵高度为 h 的子树(h >= 0),a/b/c 均符合 AVL 树的要求。10 可能是整棵树的根,也可能是一个整棵树中局部的子树的根。这里 a/b/c 是高度为 h 的子树,是一种概括抽象的表示,它代表了所有右单旋的场景。
我们通过上面的抽象出来的旋转方式来解释说明我们该如何旋转。
它的旋转方式其实也不复杂,我们可以让 5 变成这个树的根,此时我们原本的根就会变成新的根 5 的右子树 10,这样做就会使得我们原来旧根的右子树高度从 h -> h + 1,此时我们就会使得我们新的根的平衡因子变成了 0。
但是有细心的人就会注意到了,我们原本 5 的 left 和 right 分别指向 a 和 b,但是这个时候 5 的right 指向了 10 了,那 b 的数据不就没有了嘛,的确如此,所以我们在让 5 的指针发生改变时,我们应该先让 10 的 left 指向 b,再让 5 的 right 指向 10。这样做就可以实现我们的旋转了,而且我们平衡因子也平衡了。
那我们的搜索树的规则会不会出现因为随便指而出现问题呢?其实是不会的,这里改变了可能会影响树结构的两个指针,一个是 5 结点 right -> 10;一个是 10 结点 left -> b。 原本 10 的左子树都是比 10 小的值,而 5 的右子树都是比 5 大的值。此时如果我们想要把 5 变成一个新的根,就是要把左子树都变成比 5 小的值,而右子树都变成比 5 大的值。
刚好符合这种情况,所以我们的旋转是符合我们的搜索树的规则的。其实我们可以想象就是把 10 摁下去,把 5 提起来。
实际右单旋具体形态有很多种。
此时我们来尝试使用代码来实现我们的右旋操作:
我们利用这个图来看看我们该这么实现。
由于我们主要是修改 5 结点和 10 结点和 b 结点,所以我们可以用两个变量保存下来 5 结点和 b 结点,10结点就是参数 parent。
Node* subL = parent->_left;
Node* subLR = subL->_right;
在修改时我们也要记住不单单是要修改孩子指针指向,还要修改父亲的。
parent->_left = subLR;
if (subLR) // 如果LR不存在就是 nullptr,就不用连接subLR->_parent = parent;
Node* parentParent = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
此时我们在把这个局部的子树和整树做衔接,后修改平衡因子即可。
if (parentParent == nullptr)
{_root = subL;subL->_parent = nullptr;
}
if (parent == parentParent->_left)
{parentParent->_left = subL;
}
else
{parentParent->_right = subL;
}
最终让 subL 的 parent 指针和 parentParent 连接,然后更新平衡因子即可完成。
subL->_parent = parentParent;
parent->_bf = subL->_bf = 0;
全部代码:
void RotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;// 需要注意除了要修改孩⼦指针指向,还是修改⽗亲parent->_left = subLR;if (subLR) // 如果LR不存在就是 nullptr,就不用连接subLR->_parent = parent;Node* parentParent = parent->_parent;subL->_right = parent;parent->_parent = subL;// parent有可能是整棵树的根,也可能是局部的⼦树// 如果是整棵树的根,要修改_root// 如果是局部的指针要跟上⼀层链接if (parentParent == nullptr){_root = subL;subL->_parent = nullptr;}else{if (parent == parentParent->_left){parentParent->_left = subL;}else{parentParent->_right = subL;}subL->_parent = parentParent;}parent->_bf = subL->_bf = 0;
}
(3)左单旋
我们讲完了右单旋,左单旋就十分简单了,如图展示的是 10 为根的树,有 a/b/c 抽象为三棵高度为 h 的子树(h >= 0),a/b/c 均符合 AVL 树的要求。10 可能是整棵树的根,也可能是一个整棵树中局部的子树的根。这里 a/b/c 是高度为 h 的子树,是一种概括抽象的表示,它代表了所有左单旋的场景。
实际左单旋具体形态有很多种,和右单旋差不多,这里就不过多赘述。
我们可以参照以上的图,自己尝试实现左旋,和右旋的逻辑是一模一样的。
全部代码:
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)左右双旋
通过下面两张图可以看到,左边高时,如果插入位置不是在 a 子树,而是插入在 b 子树,b 子树的高度从 h 变成 h + 1,引发旋转,右单旋无法解决问题,右单旋后,我们的树依旧不平衡。右单旋解决的纯粹的左边高,但是插入在 b 子树中,10 为根的子树不再是单纯的左边高,对于 10 是左边高,但是对于 5 是右边高,需要用两次旋转才能解决,以 5 为旋转点进行一个左单旋,以 10 为旋转点进行一个右单旋,这棵树就平衡了。
在上面的两个图中我们就可以看出来,我们如果只是单旋是没有办法实现平衡的,想要平衡必须双旋,我们第一次单旋会把我们不单纯是一边高的子树变成一边高的子树,此时就变成了上面单旋的场景,我们在进行一次单旋即可实现平衡了,我们可以看看下面动画演示。
这两个图分别为左右双旋中 h == 0 和 h == 1 的具体场景,下面我们将 a/b/c 子树抽象为高度 h 的 AVL 子树进行分析,另外我们需要把 b 子树 的细节进一步展开为 8 和左子树高度为 h - 1 的 e 和 f 子树,因为我们要对 b 的父亲 5 为旋转点进行左单旋,左单旋需要动 b 树中的左子树。b 子树中新结点的位置不同,平衡因子更新的细节也不同,通过观察 8 的平衡因子不同,这里我们要分三个场景讨论。
场景1:h >= 1 时,新增结点插入在 e 子树,e 子树高度从 h - 1 变为 h 并不断更新 8 -> 5 -> 10 平衡因子。引发旋转,其中 8 的平衡因子为 -1,旋转后 8 和 5 平衡因子为 0,10 平衡因子为 1。
场景2:h >= 1 时,新增结点插入在 f 子树上,f 子树高度从 h - 1 变为 h 并不断更新 8 -> 5 -> 10 平衡因子,引发旋转,其中 8 的平衡因子为 1,旋转后 8 和 10 平衡因子为 0,5 平衡因子为 -1。
场景3:h == 0 时,a/b/c 都是空树,b 自己就是一个新增结点,不断更新 5 -> 10 平衡因子,引发旋转,其中 8 的平衡因子为 0,旋转后 8 和 10 和 5 平衡因子均为 0。
我们这三种情况的平衡因子都是不相同的,大家要理解记忆。
代码:
void RotateLR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;RotateL(parent->_left);RotateR(parent);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);}
}
(5)右左双旋
跟左右双旋类似,下面我们将 a/b/c 子树抽象为高度 h 的 AVL 子树进行分析,另外我们需要把 b 子树的细节进一步展开为 12 和左子树高度为 h - 1 的 e 和 f 子树,因为我们要对 b 的父亲 15 为旋转点进行右单旋,右单旋需要动 b 树中的右子树。b 子树中新结点的位置不同,平衡因子更新的细节也不同,通过观察 12 的平衡因子不同,这里我们要分三个场景讨论。
场景1:h >= 1 时,新增结点插入在 e 子树,e 子树高度从 h - 1 变为 h 并不断更新 12 -> 15 -> 10 平衡因子。引发旋转,其中 12 的平衡因子为 -1,旋转后 10 和 15 平衡因子为 0,15 平衡因子为 1。
场景2:h >= 1 时,新增结点插入在 f 子树上,f 子树高度从 h - 1 变为 h 并不断更新 12 -> 15 -> 10 平衡因子,引发旋转,其中 12 的平衡因子为 1,旋转后 15 和 12 平衡因子为 0,10 平衡因子为 -1。
场景3:h == 0 时,a/b/c 都是空树,b 自己就是一个新增结点,不断更新 15 -> 10 平衡因子,引发旋转,其中 12 的平衡因子为 0,旋转后 10 和 12 和 15 平衡因子均为 0。
代码:
void RotateRL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;RotateR(parent->_right);RotateL(parent);if (bf == 0){subR->_bf = 0;subRL->_bf = 0;parent->_bf = 0;}else if (bf == 1){subR->_bf = 0;subRL->_bf = 0;parent->_bf = -1;}else if (bf == -1){subR->_bf = 1;subRL->_bf = 0;parent->_bf = 0;}else{assert(false);}
}
以上便是我们处理平衡因子出现 2 的所有情况,此时我们应该对这些情况进行分类,什么情况应该调用左旋,什么应该调用右旋等。
父结点_bf | 子结点_bf | 旋转类型 | 调用函数 |
2 | 1 | L型 | 左单旋 |
-2 | -1 | R型 | 右单旋 |
-2 | 1 | LR型 | 左右双旋 |
2 | -1 | RL型 | 右左双旋 |
此时我们在插入中的平衡平衡因子的调用方式就是这样。
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.4、AVL 树查找
这个我们直接使用二叉搜索树的逻辑即可实现,由于已经进行了优化,所以我们的查找效率为 O(logN)。
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.5、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;
}
当我们算出我们的高度时,我们直接就去计算我们左右子树的高度差是否不超过 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);
}
2.6、AVL 树的删除
AVL 树的删除比较困难,这里就不做讲解了。
总结
以上便是我们 AVL 树的实现啦,主要困难的地方就在于我们在刚学的时候对旋转方式不够熟练,等我们熟练掌握怎么去旋转时,AVL 树其实也不会太困难,所以大家可以多去画图感受我们的旋转方式。
🎇坚持到这里已经很厉害啦,辛苦啦🎇
ʕ • ᴥ • ʔ
づ♡ど