当前位置: 首页 > news >正文

AVL树实现

我们前面已经说过了搜索二叉树,和map,set的用法,我们通过学习可以知道, 搜索二叉树的实现还是有一些问题的,比如果所有值都在一条线上,拿遍历的效率就低;也就是搜索二叉树不好控制,这里引进AVL树,在二叉搜索树之后,优化而成的;

在之后会在学习红黑树,那才是map set的真正的底层;

AVL的简介

  1. AVL树得名于它的发明者G. M. Adelson-Velsky和E. M. Landis是两个前苏联的科学家,他们在1962年的论⽂《An algorithm for the organization of information》中发表了它。
  2. AVL树是最先发明的是平衡⼆叉查找树,AVL是⼀颗空树,或者具备下列性质的⼆叉搜索树:它的左右⼦树都是AV树,且左右⼦树的⾼度差的绝对值不超过1。AVL树是⼀颗⾼度平衡搜索⼆叉树,通过控制⾼度差去控制平衡。
  3. AVL树实现这⾥我们引入一个平衡因⼦(balance factor)的概念,每个结点都有⼀个平衡因⼦,任何结点的平衡因⼦等于右⼦树的⾼度减去左⼦树的⾼度,也就是说任何结点的平衡因⼦等于0/1/-1,AVL树并不是必须要平衡因子,但是有了平衡因⼦可以更⽅便我们去进⾏观察和控制树是否平衡。
  4. AVL树整体结点数量和分布和完全⼆叉树类似,⾼度可以控制在 ,那么增删查改的效率也可以控制在O(logN) (虽然但是,也是接近,但是计算机的计算速度是很快的相差的拿几百几千也是可以忽略为 logN),相⽐⼆叉搜索树有了本质的提升。
  5. 思考⼀下为什么AVL树是⾼度平衡搜索⼆叉树,要求⾼度差不超过1,⽽不是⾼度差是0呢?0不是更好的平衡吗?画画图分析我们发现,不是不想这样设计,⽽是有些情况是做不到⾼度差是0的。⽐如⼀棵树是2个结点,4个结点等情况下,⾼度差最好就是1,⽆法作为⾼度差是0。

 AVL树的实现

 AVL树的结构

template<class K, class V>struct AVLTreeNode {pair<K, V> _kv;AVLTreeNode<K, V>* _left = nullptr;AVLTreeNode<K, V>* _right = nullptr;// 需要parent指针,后续更新平衡因⼦可以看到AVLTreeNode<K, V>* _parent = nullptr;int _bf = 0;     // balance factorAVLTreeNode(pair<K, V> kv):_left(),_right(),_parent(),_bf(),_kv(kv){}};
template<class K, class V>class AVLTree {typedef AVLTreeNode<K, V> Node;
……………………public:Node*_root;
}

AVL树的插⼊(平衡因子的方法)


大致过程:

  1. 平衡因子 = 右子树⾼度 - 左子树⾼度
  2. 只有子树⾼度变化才会影响当前结点平衡因子。
  3. 插⼊结点,会增加⾼度,所以新增结点在parent的右子树,parent的平衡因⼦++,新增结点parent的左子树,parent平衡因⼦--
  4. parent所在子树的⾼度是否变化决定了是否会继续往上更新

更新停止条件:
 

  1. 更新后parent的平衡因⼦等于0,更新中parent的平衡因⼦变化为-1->0 或者 1->0,说明更新前parent⼦树⼀边⾼⼀边低,新增的结点插⼊在低的那边,插⼊后parent所在的⼦树⾼度不变,不会影响parent的⽗亲结点的平衡因⼦,更新结束。
  2. 更新后parent的平衡因⼦等于1 或 -1,更新前更新中parent的平衡因⼦变化为0->1 或者 0->-1,说明更新前parent⼦树两边⼀样⾼,新增的插⼊结点后,parent所在的⼦树⼀边⾼⼀边低,parent所在的⼦树符合平衡要求,但是⾼度增加了1,会影响arent的⽗亲结点的平衡因⼦,所以要继续向上更新。
  3. 更新后parent的平衡因⼦等于2 或 -2,更新前更新中parent的平衡因⼦变化为1->2 或者 -1->-2,说明更新前parent⼦树⼀边⾼⼀边低,新增的插⼊结点在⾼的那边,parent所在的⼦树⾼的那边更⾼了,破坏了平衡,parent所在的⼦树不符合平衡要求,需要旋转处理。
  4. 旋转的⽬标有两个:1、把parent⼦树旋转平衡。2、降低parent⼦树的⾼度,恢复到插⼊结点以前的⾼度。所以旋转后也不需要继续往上更新,插⼊结束。(旋转后,parent 平衡因子是0)

过程演示

更新到10结点,平衡因⼦为2,10所在的⼦树已经不平衡,需要旋转处理
 

 更新到中间结点,3为根的⼦树⾼度不变,不会影响上⼀层,更新结束:

最坏更新到根停⽌:


插⼊结点及更新平衡因⼦的代码实现 

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.second > kv.second){parent = cur;cur = cur->_left;}else if (cur->_kv.second < kv.second){parent = cur;cur = cur->_right;}else {return false;}}cur = new Node(kv);if (parent->_kv.second > cur->_kv.second){parent->_left = cur;}else if (parent->_kv.second < cur->_kv.second){parent->_right = cur;}else {return false;}// 链接父亲cur->_parent = parent;//控制平衡//改变平衡因子while (parent){if (parent->_right == cur){parent->_bf++;}else if (parent->_left == cur){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){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;}else {assert(false);}}return true;}

会看到。插入出现了旋转,那么旋转怎么实现呢?

旋转
 

旋转的原则

  1. 保持搜索树的规则
  2. 让旋转的树从不满⾜变平衡,其次降低旋转树的⾼度
  3. 旋转总共分为四种,左单旋/右单旋/左右双旋/右左双旋。

右单旋
 

  • 下面的图片展⽰的是10为根的树,有a/b/c抽象为三棵⾼度为h的⼦树(h>=0),a/b/c均符合AVL树的要求。10可能是整棵树的根,也可能是⼀个整棵树中局部的⼦树的根。

这⾥a/b/c是⾼度为h的⼦树,是⼀种概括抽象表⽰,他代表了所有右单旋的场景,实际右单旋形态有很多种,但都属于这种形态

解读: 

在a⼦树中插⼊⼀个新结点,导致a⼦树的⾼度从h变成h+1,不断向上更新平衡因⼦,导致10的平衡因⼦从-1变成-2,10为根的树左右⾼度差超过1,违反平衡规则。10为根的树左边太⾼了,需要往右边旋转,控制两棵树的平衡。

旋转核心步骤:

  1. 因为5 < b⼦树的值 < 10,
  2. 将b变成10的左⼦树,10变成5的右⼦树,
  3. 5变成这棵树新的根,符合搜索树的规则,

控制了平衡,同时这棵的⾼度恢复到了插⼊之前的h+2,符合旋转原则。如果插⼊之前10整棵树的⼀个局部⼦树,旋转后不会再影响上⼀层,插⼊结束了。
 

右单旋代码实现
 

		void RotateR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;parent->_left = subLR;if (subLR)subLR->_parent = parent;Node* pParent = parent->_parent;subL->_right = parent;parent->_parent = subL;if (parent == _root){_root = subL;subL->_parent = nullptr;}else{if (pParent->_left == parent){pParent->_left = subL;}else{pParent->_right = subL;}subL->_parent = pParent;}subL->_bf = 0;parent->_bf = 0;}

 左单旋

  • 下面图展⽰的是10为根的树,有a/b/c抽象为三棵⾼度为h的⼦树(h>=0),a/b/c均符合AVL树的要求。10可能是整棵树的根,也可能是⼀个整棵树中局部的⼦树的根。

这⾥a/b/c是⾼度为h的⼦树,是⼀种概括抽象表⽰,他代表了所有左单旋的场景,实际左单旋形态有很多种,具体跟上⾯右旋类似这里就不在多说

 解读: 

在a⼦树中插⼊⼀个新结点,导致a⼦树的⾼度从h变成h+1,不断向上更新平衡因⼦,导致10的平衡因⼦从1变成2,10为根的树左右⾼度差超过1,违反平衡规则。10为根的树右边太⾼了,需要往左边旋转,控制两棵树的平衡。

旋转核心步骤:

  1. 因为10 < b⼦树的值 < 15,
  2. 将b变成10的右⼦树,10变成15的左⼦树,
  3. 15变成这棵树新的根,

符合搜索树的规则,控制了平衡,同时这棵的⾼度恢复到了插⼊之前的h+2,符合旋转

原则。如果插⼊之前10整棵树的⼀个局部⼦树,旋转后不会再影响上⼀层,插⼊结束了。

左单旋代码实现
 

		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;}

左右双旋
 

  • 下面的两个图可以看到,左边⾼时,如果插⼊位置不是在a⼦树,⽽是插⼊在b⼦树,b⼦树⾼度从h变成h+1,引发旋转,右单旋⽆法解决问题,右单旋后,我们的树依旧不平衡。
  • 右单旋解决的纯粹的左边⾼,但是插⼊在b⼦树中,10为跟的子树不再是单纯的左边高,对于10是左边⾼,但是对于5是右边高,需要⽤两次旋转才能解决,
  1. 以5为旋转点进⾏⼀个左单旋
  2. 以10为旋转点进⾏⼀个右单旋,这棵树这棵树就平衡了。

  • 上面两个图分别为左右双旋中h==0和h==1具体场景分析,下⾯我们将a/b/c⼦树抽象为高度h的AVL子树进行分析,另外我们需要把b的子树的细节进一步展开为8和左子树高度为h-1的e和f子树
  • 因为我们要对b的⽗亲5为旋转点进⾏左单旋,左单旋需要动b树中的左⼦树。b⼦树中新增结点的位置不同,平衡因子更新的细节也不同,通过观察上图的平衡因⼦不同,这⾥我们要分三个场景讨论。
  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 == -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);}}

右左双旋

先把左右双旋,了解明白那么右左双旋 就很好明白了 !!!!!

跟左右双旋类似,下⾯我们将a/b/c⼦树抽象为⾼度h的AVL⼦树进⾏分析,另外我们需要把b⼦树的细节进⼀步展开为12左⼦树⾼度为h-1的e和f⼦树。

因为:

  1. 我们要对b的⽗亲15为旋转点进⾏右单旋
  2. 右单旋需要动b树中的右⼦树。

b⼦树中新增结点的位置不同,平衡因子更新的细节也不同,通过观察12的平衡因⼦不同,这⾥我们要分三个场景讨论。

  1. h >= 1时,新增结点插⼊在e⼦树,e⼦树⾼度从h-1变为h并不断更新12->15->10平衡因⼦,引发旋转,其中12的平衡因⼦为-1,旋转后10和12平衡因⼦为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);}}

 AVL树的查找

那⼆叉搜索树逻辑实现即可,搜索效率为 O(logN)
 比较十分接近于完全二叉树;

		Node* Find(const K& key){Node* cur = _root;while (cur){if (cur->_kv.first > key){cur = cur->_left;}else if (cur->_kv.first < key){cur = cur->_right;}else {return cur;}}return nullptr;}

AVL树平衡检测
 

我们实现的AVL树是否合格,我们通过检查左右⼦树⾼度差的的程序进⾏反向验证,同时检查⼀下结点的平衡因⼦更新是否出现了问题。

注意:
不能检查  节点的 bf 是否 大于-2 小于 2;毕竟这和  自己该自己的卷子差不多;要用算的左子树高度减右子树高度  和 bf 对比; 

AVL树的删除
 

想要了解的同学可参考:《殷⼈昆 数据结构:⽤⾯向对象⽅法与C++语⾔描述》中讲解。

这里就不介绍了0;
 

		int Height(){return _Height(_root);}void InOrder(){_InOrder(_root);cout << endl;}bool IsBalanceTree(){return _IsBalanceTree(_root);}private:bool _IsBalanceTree(Node* root){// 空树也是AVL树if (nullptr == root)return true;// 计算pRoot结点的平衡因⼦:即pRoot左右⼦树的⾼度差int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);int dif = rightHeight - leftHeight;if (dif != root->_bf){cout << root->_kv.first << "平衡因子异常" << dif << ":" << root->_bf << endl;return false;}if (abs(dif) >= 2){cout << root->_kv.first << "高度差异常" << endl;return false;}return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);}void _InOrder(Node* root){if (root == nullptr)return;_InOrder(root->_left);cout << root->_kv.first << ":" << root->_kv.second << ":" << root->_bf << endl;_InOrder(root->_right);}int _Height(Node* root){if (root == nullptr)return 0;int HeightL = _Height(root->_left);int HeightR = _Height(root->_right);return HeightL > HeightR ? HeightL + 1 : HeightR + 1;}


http://www.mrgr.cn/news/49403.html

相关文章:

  • 020 elasticsearch7.10.2 elasticsearch-head kibana安装
  • 【优化方案】Java 将字符串中的星号替换为0-9中的数字,并返回所有可能的替换结果
  • 决策树和集成学习
  • 使用ChatGPT润色学术论文,只需4个顶级提示词指令,先人经验,直接高效使用
  • 深入探讨Python网络爬虫的实现与应用
  • ES5求职经典 JavaScript篇章
  • 优先算法1--双指针
  • 代理IP如何广告验证的效率和成功率?
  • 新品牌Sesame Street《芝麻街》商标版权双维权,尚未TRO
  • 在顺序结构和链式结构的线性表上实现顺序检索算法
  • Ubuntu20.04同时安装ROS1和ROS2,如何选择ROS1 or ROS2
  • CVESearch部署、使用与原理分析
  • 使用mnist数据集和LeakyReLU高级激活函数训练神经网络示例代码
  • Springboot 使用【过滤器】实现在请求到达 Controller 之前修改请求体参数和在结果返回之前修改响应体
  • 25.1 降低采集资源消耗的收益和无用监控指标的判定依据
  • 7-2 试试多线程
  • 探索C#编程基础:从输入验证到杨辉三角的生成
  • Java——数组的定义与使用
  • AndroidLogger 使用问题
  • 大厂面试真题-AQS中节点的入队时机有哪些