【 C++ 】AVL树
目录
1、底层结构
2、AVL树的概念
3、AVL树节点的定义
4、基本框架
5、AVL树的插入
6、AVL树的旋转
左单旋
右单旋
左右双旋
右左双旋
7、AVL树的验证
8、AVL树的查找
9、AVL树的删除(了解)
10、AVL树的性能
11、源码链接
1、底层结构
前面对map、multimap、set、multiset进行了简单的介绍,这几个容器有个共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。
2、AVL树的概念
二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii和E.M.Landis在1962年发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。
一棵AVL树或者是空树,或者是具有以下性质的二叉搜索树:
- 它的左右子树都是AVL树
- 任何一颗左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
- 0代表左右高度相等
- 1代表右子树高1
- -1代表左子树高1
下面给出一幅图来判断是否为AVL树:
如果一棵二叉搜索树是高度平衡的(相对平衡),它就是AVL树。如果它有n个结点,其高度可保持在
O(logN),搜索时间复杂度O(logN)。
3、AVL树节点的定义
这里我们实现的AVL树为KV模型,自然节点的模板参数有两个,并且节点定义为三叉连结构(左孩子,右孩子,父亲),在二叉链表的基础上加了一个指向父结点的指针域,使得即便于查找孩子结点,又便于查找父结点。接着还需要创建一个变量_bf作为平衡因子(右子树 - 左子树的高度差)。最后写一个构造函数初始化变量即可。
//节点类 template<class K, class V> struct AVLTreeNode {//存储的键值对pair<K, V> _kv;//三叉连结构AVLTreeNode<K, V>* _left;//左孩子AVLTreeNode<K, V>* _right;//右孩子AVLTreeNode<K, V>* _parent;//父亲//平衡因子_bfint _bf;//右子树 - 左子树的高度差//构造函数AVLTreeNode(const pair<K, V>& kv):_kv(kv), _left(nullptr), _right(nullptr), _bf(0){} };
4、基本框架
此部分内容为AVL树的类,主要作用是来完成后续的插入旋转删除……操作:
//AVL树的类 template<class K, class V> class AVLTree {typedef AVLTreeNode<K, V> Node; public://…… private:Node* _root; };
5、AVL树的插入
插入主要分为这几大步骤:
- 1、一开始为空树,直接new新节点
- 2、一开始非空树,寻找插入的合适位置
- 3、找到插入的合适位置后,进行父亲与孩子的双向链接
- 4、更新新插入的节点祖先的平衡因子
- 5、针对不合规的平衡因子进行旋转调整
接下来对其进行逐个分析:
- 1、一开始为空树,直接new新节点:
因为树为空的,所以直接new一个新插入的节点,将其作为根_root即可,接着更新平衡因子_bf为0,最后返回true。
- 2、一开始非空树,寻找插入的合适位置:
这里和二叉搜索树的寻找合适的插入位置的思想一样,都要遵循以下几步:
- 插入的值 > 节点的值,更新到右子树查找
- 插入的值 < 节点的值,更新到左子树查找
- 插入的值 = 节点的值,数据冗余插入失败,返回false
当循环结束的时候,就说明已经找到插入的合适位置,即可进行下一步链接。
- 3、找到插入的合适位置后,进行父亲与孩子的双向链接:
注意这里节点的构成为三叉链,因此最后链接后端孩子和父亲是双向链接,具体操作如下:
- 插入的值 > 父亲的值,把插入的值链接在父亲的右边
- 插入的值 < 父亲的值,把插入的值链接在父亲的左边
- 因为是三叉连,插入后记得双向链接(孩子链接父亲)
走到这,说明节点已经插入完毕,但是接下来就要更新平衡因子了
- 4、更新新插入的节点祖先的平衡因子:
当我们插入新节点后,子树的高度可能会发生变化,针对这一变化,我们给出以下要求:
- 子树的高度变了,就要继续往上更新
- 子树的高度不变,则更新完成
- 子树违反平衡规则(平衡因子的绝对值 >= 2),则停止更新,需要旋转子树进行调整
具体的更新规则如下:
- 新增结点在parent的右边,parent的平衡因子++
- 新增结点在parent的左边,parent的平衡因子 --
每更新完一个节点的平衡因子后,都要进行如下判断:
- 如果parent的平衡因子等于-1或者1(说明原先是0,左右登高,插入节点后使左子树或右子树增高了)。表明还需要继续往上更新平衡因子。
- 如果parent的平衡因子等于0(说明原先是1或-1,一高一低,插入节点后填上了矮的那一方)表明无需更新平衡因子了。
- 如果parent的平衡因子等于-2或者2(说明原先是1或-1,一高一低,插入节点后填上了高的那一方),表明此时以parent结点为根结点的子树已经不平衡了,需要进行旋转处理。
图示如下:
- 5、针对不合规的平衡因子进行旋转调整:
当父亲parent的平衡因子为2或-2时,就要进行旋转调整了,而又要分为以下4类进行旋转:
- 当parent的平衡因子为2,cur的平衡因子为1时,进行左单旋。
- 当parent的平衡因子为-2,cur的平衡因子为-1时,进行右单旋
- 当parent的平衡因子为-2,cur的平衡因子为1时,进行左右双旋。
- 当parent的平衡因子为2,cur的平衡因子为-1时,进行右左双旋。
代码如下:
//Insert插入 bool Insert(const pair<K, V>& kv) {//1、一开始为空树,直接new新节点if (_root == nullptr){//如果_root一开始为空树,直接new一个kv的节点,更新_root和_bf_root = new Node(kv);_root->_bf = 0;return true;}//2、寻找插入的合适位置Node* cur = _root;//记录插入的位置Node* parent = nullptr;//保存parent为cur的父亲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{//插入的值 = 节点的值,数据冗余插入失败,返回falsereturn false;}}//3、找到了插入的位置,进行父亲与插入节点的链接cur = new Node(kv);if (parent->_kv.first < kv.first){//插入的值 > 父亲的值,链接在父亲的右边parent->_right = cur;}else{//插入的值 < 父亲的值,链接在父亲的左边parent->_left = cur;}//因为是三叉连,插入后记得双向链接(孩子链接父亲)cur->_parent = parent;//4、更新新插入节点的祖先的平衡因子while (parent)//最远要更新到根{if (cur == parent->_right){parent->_bf++;//新增结点在parent的右边,parent的平衡因子++}else{parent->_bf--;//新增结点在parent的左边,parent的平衡因子 --}//判断是否继续更新?if (parent->_bf == 0)// 1 or -1 -> 0 填上了矮的那一方{//1 or -1 -》 0 填上了矮的那一方,此时正好,无需更新break;}else if (parent->_bf == 1 || parent->_bf == -1){//0 -》 1或-1 此时说明插入节点导致一边变高了,继续更新祖先cur = cur->_parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2){//1 or -1 -》2或-2 插入节点导致本来高的一边又变更高了//此时子树不平衡,需要进行旋转if (parent->_bf == 2 && cur->_bf == 1){RotateL(parent);//右边高,左单旋}else if (parent->_bf == -2 && cur->_bf == -1){RotateR(parent);//左边高,右单旋}else if (parent->_bf == -2 && cur->_bf == 1){RotateLR(parent);//左右双旋}else if (parent->_bf == 2 && cur->_bf == -1){RotateRL(parent);//右左双旋}break;}else{//插入之前AVL树就存在不平衡树,|平衡因子| >= 2的节点//实际上根据前面的判断不可能走到这一步,不过这里其实是为了检测先前的插入是否存在问题assert(false);}}return true; }
6、AVL树的旋转
AVL树的旋转分为4种:
- 左单旋
- 右单旋
- 左右双旋
- 右左双旋
AVL树的旋转要遵循下面两个原则:
- 1、保持搜索树的规则
- 2、子树变平衡
左单旋
- 条件:新节点插入较高右子树的右侧
图示:
鉴于左单旋的情况非常多,这里我们画一张抽象图来演示:
这里的长方形条(a、b、c)表示的是子树,h为子树的高度,而30和60为实打实的节点。上述左单旋操作主要是完成了四件事:
- 让subRL变成parent的右子树,更新subRL的父亲为parent
- 让subR变成根节点
- 让parent变成subR的左子树,更新parent的父亲为subR
- 更新平衡因子
注意:
- parent可能为整棵树的一个子树,则需要链接parent的父亲和subR。
- subRL可能为空,但是更新subRL的父亲为parent是建立在subRL不为空的前提下完成的。
解释为何上述左单旋的可行性:
- 首先,根据底层二叉搜索树的结构:b节点的值肯定是在30~60之间的,b去做30的右子树没有任何问题,且这里把60挪到根部,随即把30作为60的右子树,这样整体的变化就像是一个左旋一样,而且也满足二叉搜索树的性质且均平衡。
代码如下:
void RotateL(Node* parent) {Node* subR = parent->_right;Node* subRL = subR->_left;Node* ppNode = parent->_parent;//提前保持parent的父亲//1、建立parent和subRL之间的关系parent->_right = subRL;if (subRL)//防止subRL为空{subRL->_parent = parent;}//2、建立subR和parent之间的关系subR->_left = parent;parent->_parent = subR;//3、建立ppNode和subR之间的关系if (parent == _root){_root = subR;_root->_parent = nullptr;}else{if (parent == ppNode->_left){ppNode->_left = subR;}else{ppNode->_right = subR;}subR->_parent = ppNode;//三叉链双向链接关系}//4、更新平衡因子subR->_bf = parent->_bf = 0; }
右单旋
- 条件:新节点插入较高左子树的左侧
图示:
同样这里的长方形条(a、b、c)表示的是子树,h为子树的高度,而30和60为实打实的节点。上述左单旋操作主要是完成了四件事:
- 让subLR变成parent的左子树,更新subLR的父亲为parent
- 让subL变成根节点
- 让parent变成subL的右子树,更新parent的父亲为subL
- 更新平衡因子
注意:
- parent可能为整棵树的一个子树,则需要链接parent的父亲和subL。
- subLR可能为空,但是更新subLR的父亲为parent是建立在subLR不为空的前提下完成的。
代码如下:
//2、右单旋 void RotateR(Node* parent) {Node* subL = parent->_left;Node* subLR = subL->_right;Node* ppNode = parent->_parent;//1、建立parent和subLR之间的关系parent->_left = subLR;if (subLR){subLR->_parent = parent;}//2、建立subL和parent之间的关系subL->_right = parent;parent->_parent = subL;//3、建立ppNode和subL的关系if (parent == _root){_root = subL;_root->_parent = nullptr;}else{if (parent == ppNode->_left){ppNode->_left = subL;}else{ppNode->_right = subL;}subL->_parent = ppNode;//三叉链双向关系}//4、更新平衡因子subL->_bf = parent->_bf = 0; }
左右双旋
- 条件:新节点插入较高左子树的右侧
接下来图示解析左右双旋的具体解法。
- 1、插入新节点:
此类模型既不满足左单旋的条件也不满足右单旋的条件,但是我们可以将其组合起来,即左右双旋(先左单旋,再右单旋)的办法重新建立平衡。接下来执行下一步左单旋:
- 2、以节点30(subL)为旋转点左单旋:
此时再观察这幅图,这部就是一个妥妥的右单旋模型吗,把60的左子树看成一个整体,此时新插入的节点即插入较高左子树的左侧,刚好符合右单旋的性质,接下来即可进行右单旋:
- 3、以节点90(parent)为旋转点进行右单旋:
此时左右双旋已经完成,最后一步为更新平衡因子,但是更新平衡因子又分如下三类:
- 1、当subLR原始平衡因子是-1时,左右双旋后parent、subL、subLR的平衡因子分别更新为1、0、0。
- 2、当subLR原始平衡因子是1时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、-1、0。
- 3、当subLR原始平衡因子是0时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、0、0。
这里可以看出唯有subLR平衡因子为0的情况下在进行左右旋转后,三个节点的平衡因子都要更新为0。
- 代码如下:
//3、左右双旋 void RotateLR(Node* parent) {Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf;//提前记录subLR的平衡因子//1、以subL为根传入左单旋RotateL(subL);//2、以parent为根传入右单旋RotateR(parent);//3、重新更新平衡因子if (bf == 0){parent->_bf = 0;subL->_bf = 0;subLR->_bf = 0;}else if (bf == 1){parent->_bf = 0;subL->_bf = -1;subLR->_bf = 0;}else if (bf == -1){parent->_bf = 1;subL->_bf = 0;subLR->_bf = 0;}else{assert(false);//此时说明旋转前就有问题,检查} }
右左双旋
- 条件:新节点插入较高右子树的左侧
接下来图示解析左右双旋的具体解法。
- 1、插入新节点:
注意这里的新节点插在了较高右子树的左侧,不能用上文的单旋转以及左右旋转,相反而应使用右左旋转(先右单旋,再左单旋)来解决,接下来执行下一步右单旋:
- 2、以节点90(subR)为旋转点右单旋:
此时再观察这幅图,这部就是一个妥妥的左单旋模型吗,把60的右子树看成一个整体,此时新插入的节点即插入较高右子树的右侧,刚好符合左单旋的性质,接下来即可进行左单旋:
- 3、以节点30(parent)为旋转点左单旋:
此时左右双旋已经完成,最后一步为更新平衡因子,但是更新平衡因子又分如下三类:
- 1、当subRL原始平衡因子是-1时,右左双旋后parent、subR、subRL的平衡因子分别更新为0、1、0。
- 2、当subRL原始平衡因子是1时,右左双旋后parent、subR、subRL的平衡因子分别更新为-1、0、0。
- 3、当subRL原始平衡因子是0时,右左双旋后parent、subR、subRL的平衡因子分别更新为0、0、0。
这里可以看出唯有subRL平衡因子为0的情况下在进行左右旋转后,三个节点的平衡因子都要更新为0。
- 代码如下:
//4、右左双旋 void RotateRL(Node* parent) {Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf;//提前记录subLR的平衡因子//1、以subL为根传入左单旋RotateR(subR);//2、以parent为根传入右单旋RotateL(parent);//3、重新更新平衡因子if (bf == 0){parent->_bf = 0;subR->_bf = 0;subRL->_bf = 0;}else if (bf == 1){parent->_bf = -1;subR->_bf = 0;subRL->_bf = 0;}else if (bf == -1){parent->_bf = 0;subR->_bf = 1;subRL->_bf = 0;}else{assert(false);//此时说明旋转前就有问题,检查} }
7、AVL树的验证
AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,就是看它是否为二叉搜索树,以及是否为一颗平衡树。接下来分别讨论:
- 1、验证其为二叉搜索树:
这里我们只需要进行中序遍历看看结果是否可得到一个有序的序列,如果可以则证明是二叉搜索树,而中序遍历的实现非常简单,先前的二叉搜索树的实现已然完成过,这里直接给出代码:
//中序遍历的子树 void _InOrder(Node* root) {if (root == nullptr)return;_InOrder(root->_left);cout << root->_kv.first << " ";_InOrder(root->_right); } //中序遍历 void InOrder() {_InOrder(_root);cout << endl; }
检测是否为二叉搜索树的代码实现后,接下来开始验证是否为平衡树。
- 2、验证其为平衡树:
规则如下:(递归的思想)
- 空树也是平衡树,一开始就要判断
- 封装一个专门计算高度的函数(递归计算高度)后续用来计算高度差(平衡因子diff)
- 如过diff不等于root的平衡因子(root->_bf),或root平衡因子的绝对值超过1,则一定不是AVL树
- 继续递归到子树&&右树,直至结束
代码如下:
//验证一棵树是否为平衡树 bool IsBalanceTree() {return _IsBalanceTree(_root); } //判读是否平衡的子树 bool _IsBalanceTree(Node* root) {//空树也是AVL树if (nullptr == root)return true;//计算root节点的平衡因子diff:即root左右子树的高度差int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);int diff = rightHeight - leftHeight;//如果计算出的平衡因子与root的平衡因子不相等,或root平衡因子的绝对值超过1,则一定不是AVL树if ((abs(diff) > 1)){cout << root->_kv.first << "节点平衡因子异常" << endl;return false;}if (diff != root->_bf){cout << root->_kv.first << "节点平衡因子与root的平衡因子不等,不符合实际" << endl;return false;}//继续递归检测,直到结束return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right); } //求高度的子树 int _Height(Node* root) {if (root == nullptr)return 0;int lh = _Height(root->_left);int rh = _Height(root->_right);return lh > rh ? lh + 1 : rh + 1; }
综合上述两大步骤的操作即可对一棵树充分的验证是否为AVL树。
8、AVL树的查找
Find查找函数的思路很简单,定义cur指针从根部开始按如下规则遍历:
- 若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。
- 若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。
- 若key值等于当前结点的值,则查找成功,返回true。
- 若遍历一圈cur走到nullptr了说明没有此结点,返回false
//Find查找 bool Find(const K& key) {Node* cur = _root;while (cur){if (cur->_key < key){cur = cur->_right;//若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。}else if (cur->_key > key){cur = cur->_left;//若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。}else{return true;//若key值等于当前结点的值,则查找成功,返回true。}}return false;//遍历一圈没找到返回false }
9、AVL树的删除(了解)
因为AVL树也是二叉搜索树,主要是三个大思路:
- 按二叉搜索树的规则删除
- 更新平衡因子
- 出现不平衡,需要旋转调整
只不过与搜索二叉树的删除不同的是,删除节点后的平衡因子需要不断更新,最差情况下一直要调整到根节点的位置。具体这里就不再实现了,因为着实有点复杂。不过《算法导论》或《数据结构-用面向对象方法与C++描述》殷人昆版这两本书上是有详细的讲解的哈。
10、AVL树的性能
AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即O(logN)。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树,但一个结构经常修改,就不太适合。
11、源码链接
链接直达:AVL树的模拟实现完整版
【 C++ 】AVL树相关推荐
- AVL树、splay树(伸展树)和红黑树比较
AVL树.splay树(伸展树)和红黑树比较 一.AVL树: 优点:查找.插入和删除,最坏复杂度均为O(logN).实现操作简单 如过是随机插入或者删除,其理论上可以得到O(logN)的复杂度,但是实 ...
- 算法基础知识科普:8大搜索算法之AVL树(上)
前段时间介绍了二叉搜索树(BST),我们知道这种搜索结构存在的弊端是对输入序列存在强依赖,若输入序列基本有序,则BST近似退化为链表.这样就会大大降低搜索的效率.AVL树以及Red-Black树就是为 ...
- 【漫画】以后在有面试官问你AVL树,你就把这篇文章扔给他。
S 推荐阅读 对于另一半,你真正的爱过吗? 背景 西天取经的路上,一样上演着编程的乐趣..... 1.若它的左子树不为空,则左子树上所有的节点值都小于它的根节点值. 2.若它的右子树不为空,则右子树上 ...
- 【从蛋壳到满天飞】JS 数据结构解析和算法实现-AVL树(一)
前言 [从蛋壳到满天飞]JS 数据结构解析和算法实现,全部文章大概的内容如下: Arrays(数组).Stacks(栈).Queues(队列).LinkedList(链表).Recursion(递归思 ...
- 数据结构与算法——AVL树类的C++实现
关于AVL树的简单介绍能够參考: 数据结构与算法--AVL树简单介绍 关于二叉搜索树(也称为二叉查找树)能够參考:数据结构与算法--二叉查找树类的C++实现 AVL-tree是一个"加上了额 ...
- 纸上谈兵: AVL树
作者:Vamei 出处:http://www.cnblogs.com/vamei 欢迎转载,也请保留这段声明.谢谢! 二叉搜索树的深度与搜索效率 我们在树, 二叉树, 二叉搜索树中提到,一个有n个节点 ...
- AVL树入门(转载)
原文链接:http://lib.csdn.net/article/datastructure/9204 作者:u011469062 前言:本文不适合 给一组数据15分钟就能实现AV ...
- PAT1123 Is It a Complete AVL Tree(AVL树完全二叉树)
题意: 给出一系列数,要求组成AVL树,最后层序输出,并且判断是否为一个完全二叉树 要点: 这题就是一个AVL树的插入和判断完全二叉树,之前分别都有出现过,AVL树的建立需要记忆. #include& ...
- Linux内核之于红黑树and AVL树
为什么Linux早先使用AVL树而后来倾向于红黑树? 实际上这是由红黑树的实用主义特质导致的结果,本短文依然是形而上的观点.红黑树可以直接由2-3树导出,我们可以不再提红黑树,而只提2- ...
- 算法(6) —— AVL树
AVL树二叉查找树的一种,所以其操作和二叉查找树的很多操作是相同的. 1. 1 #ifndef AVLTREE_H 2 #define AVLTREE_H 3 4 struct AvlNode; 5 ...
最新文章
- 第十六 django进一步了解
- 哪个更快:while(1)或while(2)?
- wxpython使用folium_wxPython实现文本框基础组件
- 【Windows Phone】Metro设计语言
- MTK 移植泰文输入法
- keepalived配置高可用集群
- JStorm—实时流式计算框架入门介绍
- pat1045. Favorite Color Stripe (30)
- 十万大学生都已成为猿粉,你还在等什么?
- xp计算机管理窗口,XP系统设备管理器的打开技巧
- 趣学算法 陈小玉 书中所有问题的实现代码
- Testbed单元测试
- ewebeditor php漏洞,ewebeditor for php任意文件上传漏洞
- 地球物理中的有限单元法-第二类边界条件-三角剖分-线性插值 matlab编程实现
- NOIP 2015 蒟蒻做题记录
- 深入理解Java7.pdf
- MATLAB 数据频数统计
- 中小企业如何选择进销存软件?
- Python——打开文件
- 2018.12.30【NOIP提高组】模拟A组 JZOJ 5353 村通网
热门文章
- Android DataBinding RecyclerView AAPT: error: attribute adapter (aka......) not found.
- zend guard loader php ts,安装Zend Guard Loader说明
- hadoop大数据工程师、数据开发工程师、数据仓库工程师 面试题目分享
- 【青龙面板】返利好省
- word2vec训练中文词向量
- wkhtmltopdf乱码解决方案
- 奋斗吧,程序员——第二十四章 想佳人、妆楼凝望,误几回、天际识归舟
- 计算机科学与技术考研双非,985弱势“好考”专业与双非王牌专业大汇总!考研报考必备!...
- 豆瓣电影Top250信息爬取并保存到excel文件中
- click和touchmove vue_移动端touch事件影响click事件以及在touchmove添加preventDefault导致页面无法滚动的解决方法...