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

【c++】AVL树模拟实现

简介

AVL树是最先被发明出来的自平衡二叉查找树,在1962由前苏联科学家G. M. Adelson-Velsky和E. M. Landis在论文中发表。AVL树中引入了平衡因子,每一个节点都有一个平衡因子(一般是右子树高度 - 左子树高度);AVL树要求左右子树高度相差不能超过1,即平衡因子只能是0,1或-1。AVL树的高度始终是log(n),n为节点数量。

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){}
};template<class K,class V>
class AVLTree
{typdef AVLTreeNode<K, V> Node;
public:private:Node* _root = nullptr;};

_kv是AVL树储存的键值对,既能存储键值key,每一个key也能对应储存一个value值。

为什么要有_parent?

AVL树通过平衡因子来控制树的平衡,插入或删除数据后,需要回溯到父节点计算平衡因子,所以我们需要_parent来储存每一个节点父节点的值。

AVL树的插入 

插入的基本流程

寻找插入的位置

按照二叉树遍历的顺序找到要插入的位置。

更新平衡因子

插入一个新的节点之后,如果父节点的平衡因子发生改变,则需更新从根节点到父节点这条路径的所有节点的平衡因子。

判断是否产生不平衡

如果更新平衡因子之后,没有出现平衡因子不为0,-1,1的,则说明树是平衡的,插入结束。

如果出现了不平衡,对不平衡⼦树旋转,旋转后本质调平衡的同时,本质降低了⼦树的⾼度,不会再影响上⼀层,所以插⼊结束。

平衡因子的更新

更新的原则

平衡因⼦ = 右⼦树⾼度-左⼦树⾼度。
只有⼦树⾼度变化才会影响当前结点平衡因⼦。
插⼊结点,会增加⾼度,所以新增结点在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所在的⼦树不符合平衡要求,需要旋转处理,旋转的⽬标有两个:1、把
parent⼦树旋转平衡。2、降低parent⼦树的⾼度,恢复到插⼊结点以前的⾼度。所以旋转后也不
需要继续往上更新,插⼊结束。
不断更新,更新到根,跟的平衡因⼦是1或-1也停⽌了。

插入的整体框架:
 

bool insert(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 (cur->_kv.first < parent->_kv.first){parent->_left = cur;}else if (cur->_kv.first > parent->_kv.first){parent->_right = 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;}

旋转

旋转的原则

1.保持搜索树的规则

2.让树变平衡,降低旋转树的高度

根据插入的位置不同,旋转一共分为四种,左单旋/右单旋/左右双旋/右左双旋

右单旋

右单旋适合处理下图这种情况,插入前,左子树高度刚好比右子树高1,且刚好插入值为在左子树最左边(比原来树中的所有值都小)。

普遍情况:

为了方便理解我们这里取特例: 

插入节点-3之后,树不再平衡。这时候就需要通过旋转来使树再次平衡。怎样让树再次平衡呢?

我们只需要操作最深的那一条路径就好了,让它高度减一,在移到右子树上,这样平衡因子就为0了。

虽然不符合AVL树的规则了,但是这颗树还是符合二叉搜索树的规则的。还是可以中序遍历。

这棵树中序遍历的结果是:-3,1,5,8,10,15。把10拿下来让左子树高度减一,再向办法把10放到右子树中,5的右子树绝对比它的父节点(10)小,所以可以把10变成5的右节点,同时让8接到10的左节点。这样即保证中序遍历的结果又使树恢复了平衡。

代码实现:

//右单旋void RotateR(Node* parent){Node* subL = parent->_left;Node* subLR = subL->_right;// 需要注意除了要修改孩⼦指针指向,还是修改⽗亲parent->_left = subLR;if (subLR)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;}

要处理的三个节点

注意:parent不一定是根节点,所以要创建一个临时变量parentParent储存parent的父节点,处理好子树之后,再让subL的父节点指向parentParent(parentParent为空则,subL变为根节点)。

左单旋

左单旋适合一下情况

树中的左旋和右旋互为对称操作,左旋和右旋是可以相互抵消的,即经历一次左旋和一次右旋之后会恢复成原树。

 代码实现:

void RoateL(Node* parent){Node* subR = parent->_right;parent->_right = subR;Node* subRL = subR->_left;if (subRL){subRL->_parent = subR;}Node* parentParent = parent->_parent;subR->_left = parent;parent->_parent = subR;parent->_right = subRL;if (subRL){subRL->_parent = parent;}if (parentParent){subR->_parent = parentParent;if (parentParent->_left == parent){subR->_parent = parentParent;parentParent->_left = subR;}else if(parentParent->_right == parent){subR->_parent = parentParent;parentParent->_right = subR;}}else if (parentParent == nullptr){_root = subR;}parent->_bf = subL->_bf = 0;}

左右双旋

观察下面这种情况,它并不是纯粹的左边高,对于节点10是左边高,对于节点5来说是右边高。

这时候单纯的右单旋或者左单旋都不能解决问题, 这时候两次单旋就可以解决问题。以节点5为旋转点,进行一次左单旋,这时候就是一个完全左边高的树结构,完全符合右单旋的应用场景,以节点10为旋转点进行一次右单旋,树恢复平衡。

要处理的节点:

 

 代码实现:

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

右左双旋 


右左双旋于左右双旋类似,接下来我们以普适情况抽象出a,b,c三棵子树来讨论。另外我们需要把b⼦树的 细节进⼀步展开为12和左⼦树⾼度为h-1的e和f⼦树,因为我们要对b的⽗亲15为旋转点进⾏右单旋,右单旋需要动b树中的右⼦树。

b⼦树中新增结点的位置不同,平衡因⼦更新的细节也不同,通过观察12的平衡因⼦不同,这⾥我们要分三个场景讨论。
场景一: h >= 1时,新增结点插⼊在e⼦树,e⼦树⾼度从h-1变为h并不断更新12->15->10平衡因
⼦,引发旋转,其中12的平衡因⼦为-1,旋转后10和12平衡因⼦为0,15平衡因⼦为1。

 场景二:h >= 1时,新增结点插⼊在f⼦树,f⼦树⾼度从h-1变为h并不断更新12->15->10平衡因⼦, 引发旋转,其中12的平衡因⼦为1,旋转后15和12平衡因⼦为0,10平衡因⼦为-1。

场景三: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);}}

 查找

按照遍历二叉树的逻辑查找即可。

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

AVL树平衡检测

对于实现的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);}

相关文章:

  • Comfy UI 笔记
  • 文章记单词 | 第47篇(六级)
  • 面试记录1-春招补录0427
  • 基础学习:(9)vit -- vision transformer 和其变体调研
  • 《大型网站技术架构-核心原理与案例分析》笔记
  • UV工具的安装与使用
  • Leetcode:283. 移动零
  • Scala 函数柯里化及闭包
  • 343. 整数拆分
  • Tailwind CSS 实战:基于 Kooboo 构建企业官网页面(二)
  • 【SF】在 Android 显示系统中,图层合成方式 Device 和 Client 的区别
  • 信创系统资产清单采集脚本:主机名+IP+MAC 一键生成 CSV
  • 汽车产业链主表及类别表设计
  • 2、Linux操作系统下,ubuntu22.04版本安装搜狗输入法
  • ACM会议模板设置单排作者数量
  • 低压电工常见知识点
  • Java——琐碎知识点一
  • 国家与省市县 标准地图服务网站 审图号地图下载
  • 【愚公系列】《Manus极简入门》005-DeepSeek与Manus的创新之处
  • camera知识学习
  • 上海数学教育及数学科普专家陈永明去世,享年85岁
  • QFII一季度现身超300家公司:持有南京银行市值最高,5家青睐立航科技
  • 王庆成:儒家、墨家和洪秀全的“上帝”
  • 洗冤录·巴县档案|道咸年间一起家暴案
  • 戴昕谈隐私、数据、声誉与法律现实主义
  • 乌方称泽连斯基与特朗普进行简短会谈