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

从零开始的C++学习生活 12:AVL树全面解析

 

个人主页:Yupureki-CSDN博客

C++专栏:C++_Yupureki的博客-CSDN博客

 

目录

前言

1. AVL树的概念

2. AVL树的实现

2.1 AVL树的结构

2.2 AVL树的插入

2.2.1 AVL树插入值的大概过程

2.2.2 平衡因子更新

2.2.3 插入结点及更新平衡因子的代码实现

2.3 旋转

2.3.1 旋转的原则

2.3.2 左单旋

2.3.3 右单旋

2.3.4 左右双旋

2.3.5 右左双旋

2.4 AVL树的查找

2.5 AVL树平衡检测

2.6 AVL树的删除

总结


 

 

 

 

上一篇:从零开始的C++学习生活 11:二叉搜索树全面解析-CSDN博客

 

前言

在前面,我们学习了二叉搜索树。二叉搜索树(BST)作为一种常见的数据结构,虽然提供了快速的查找、插入和删除操作,但在最坏情况下(如插入有序数据)会退化成链表,导致时间复杂度从理想的O(log N)恶化到O(N)。为了解决这个问题,两位前苏联科学家G. M. Adelson-Velsky和E. M. Landis在1962年的论文《An algorithm for the organization of information》中提出了AVL树——一种自平衡二叉搜索树。

AVL树通过严格控制树的平衡性,确保在任何情况下树的高度都保持在O(log N)级别,从而保证了所有操作的时间复杂度都为O(log N)。我将详细解析AVL树的原理、实现方法以及平衡维护机制。

 

1. AVL树的概念

AVL树是最先发明的自平衡二叉查找树

其具有以下特性:

它的左右子树都是AVL树,且左右子树的高度差的绝对值不超过1。AVL树是一颗高度平衡搜索二叉树,通过控制高度差去控制平衡。

AVL树实现这里我们引入一个平衡因子(balance factor)的概念,每个结点都有一个平衡因子,任何结点的平衡因子等于右子树的高度减去左子树的高度,也就是说任何结点的平衡因子等于0/1/-1

 

AVL树并不是必须要平衡因子,但是有了平衡因子可以更方便我们去进行观察和控制树是否平衡,就像一个风向标一样。

思考一下为什么AVL树是高度平衡搜索二叉树,要求高度差不超过1,而不是高度差是0呢?因为很多情况无法保证高度差一定为0。比如一棵树是2个结点,4个结点等情况下,高度差最好就是1,无法做到高度差是0。

AVL树整体结点数量和分布和完全二叉树类似,高度可以控制在 log⁡NlogN ,那么增删查改的效率也可以控制在 O(log⁡N)O(logN) ,相比二叉搜索树有了本质的提升。

 

2. AVL树的实现

 

2.1 AVL树的结构

一般AVL树我们使用前面将的key/value组合,而在这里我们使用一个叫pair的类封装key和value(pair为库文件中的类)。这是一个专门处理两个值映射关系的类

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

 

2.2 AVL树的插入

2.2.1 AVL树插入值的大概过程

  1. 插入一个值按二叉搜索树规则进行插入。

  2. 新增结点以后,只会影响祖先结点的高度,也就是可能会影响部分祖先结点的平衡因子,所以更新从新增结点->根结点路径上的平衡因子,实际中最坏情况下要更新到根,有些情况更新到中间就可以停止了。

  3. 更新平衡因子过程中没有出现问题,则插入结束。

  4. 更新平衡因子过程中出现不平衡,对不平衡子树旋转,旋转后本质调平衡的同时,本质降低了子树的高度,不会再影响上一层,所以插入结束。

2.2.2 平衡因子更新

更新原则:

  • 平衡因子=右子树高度-左子树高度

  • 只有子树高度变化才会影响当前结点平衡因子。

  • 插入结点,会增加高度,所以新增结点在parent的右子树,parent的平衡因子++,新增结点在parent的左子树,parent平衡因子--

  • parent所在子树的高度是否变化决定了是否会继续往上更新

我们插入3后,其父节点4,6,3,8都发生了平衡因子变化,而其余的节点不会变化,因为只有平衡因子等于右子树高度-左子树高度,只有子树高度变化才会引起平衡因子的变化

如果平衡因子变为-1或者1,说明是从0变过去的,也就是增加了高度,所以还得对上面进行调整

如果是0,说明是从-1或者1变过去的,就相当于原来一个节点只有一个孩子,现在补了一个,从-1或者1变成了0,那高度变化了吗?没有,所以不用向上调整

当为2或者-2时,树不平衡,需要进行旋转调整

 

2.2.3 插入结点及更新平衡因子的代码实现

bool Insert(const 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 (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 旋转

在上面平衡因子的更新中,我们发现有的节点已经不满足平衡因子的绝对值小于2的条件了,变成了2或者-2,因此我们需要对树进行一系列的操作,使得平衡因子满足规则

2.3.1 旋转的原则

  1. 保持搜索树的规则

  2. 让旋转的树从不满足变平衡,其次降低旋转树的高度

旋转总共分为四种:左单旋、右单旋、左右双旋、右左双旋。

 

2.3.2 左单旋

当某个节点的右子树比左子树高2,且右子树的右子树比左子树高时,需要进行左单旋。

平衡因子更新到2,那么一定是从1更新到2,即右子树的高度+1

在这里这种情况左单旋还无法解决,我们把3和1删掉,假设4是新插入的节点

我们拆分这个树单独查看

 

我们可以把左单旋看作往左边"压",因为3节点翘得太高了,以至于导致平衡因子失衡。所以把3给压下去,换6上来,同时让3指向6的左孩子4。不要忘记让8之前指向的是3,现在要改为6

 

左单旋代码实现:

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

2.3.3 右单旋

右单旋其实和左单旋十分相似,只是旋的方向相反而已

(左图4的平衡因子是0)

我们这次把6压下去,换4上来,然后6指向4的右孩子7,8指向4

右单旋代码实现:

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

 

2.3.4 左右双旋

我们先看下面两个树

 

看起来区别就是4的左右孩子不一样

然后6肯定是失衡了的,我们都进行右旋看看

 

我们会发现上面的情形进行右单旋后,4上去后平衡因子却成了2

因为右旋后6要指向5,6本来就下去了,6下面再接个5,不就造成了平衡因子再次失衡吗

所以发生这种情况的条件是,4的右子树高度高于左子树,即4的平衡因子是1

因为当平衡因子是1的时候,就会发生错误,所以我们只需要改变4的平衡因子即可,就是对4进行左单旋

让4和5是往左下斜,而不是往右下斜,这样再次进行右旋即可

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

 

2.3.5 右左双旋

当某个节点的右子树比左子树高2,且右子树的左子树比右子树高时,需要进行右左双旋:先对右子树进行右旋,再对当前节点进行左旋。

与左右双旋的案例十分相似,我们不再做过多的讨论

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.4 AVL树的查找

AVL树的查找与普通二叉搜索树相同,但由于AVL树是平衡的,查找效率始终为O(log N)。

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

 

 

2.5 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)
{if (nullptr == root)return true;int leftHeight = Height(root->_left);int rightHeight = Height(root->_right);int diff = rightHeight - leftHeight;if (abs(diff) >= 2){cout << root->_kv.first << "高度差异常" << endl;return false;}if (root->_bf != diff){cout << root->_kv.first << "平衡因子异常" << endl;return false;}return IsBalanceTree(root->_left) && IsBalanceTree(root->_right);
}

 

2.6 AVL树的删除

删除操作我们可以进行与二叉搜索树类似的操作:

首先查找元素是否在二叉搜索树中,如果不存在,则返回false。

如果查找元素存在则分以下四种情况分别处理:(假设要删除的结点为N)

  1. 要删除结点N左右孩子均为空
  2. 要删除的结点N左孩子位空,右孩子结点不为空,那么删除节点的父节点则应该指向删除节点的右孩子
  3. 要删除的结点N右孩子位空,左孩子结点不为空,那么删除节点的父节点则应该指向删除节点的左孩子
  4. 要删除的结点N左右孩子结点均不为空

也就是替换法,替换完成后就完了吗?当然不是,对节点的删除必然会导致平衡因子的改变,因此我们需要调整

由于我们使用替代法,所以实际真删除的是被替代的那个节点,因此得从该节点的父节点开始调整

如果是0,说明从-1或者1调整过去,高度减少了1,需要向上调整

如果是-1或者1,最大高度不变,就相当于有两根绳子,一根不变,另一根少了一节,高度差从原来的0变成了-1或者1,最大长度变吗?不变,平衡因子看的是最大高度,所以不需要调整

如果是-2或者2,不平衡,直接旋转

bool Erase(const K& key)
{// 空树情况if (_root == nullptr)return false;Node* parent = nullptr;Node* cur = _root;// 查找要删除的节点while (cur){if (cur->_kv.first < key){parent = cur;cur = cur->_right;}else if (cur->_kv.first > key){parent = cur;cur = cur->_left;}else{// 找到要删除的节点break;}}// 没有找到要删除的节点if (cur == nullptr)return false;// 情况1:要删除的节点有两个子节点if (cur->_left && cur->_right){// 找到右子树的最小节点(后继)Node* minParent = cur;Node* minRight = cur->_right;while (minRight->_left){minParent = minRight;minRight = minRight->_left;}// 用后继节点的值替换当前节点的值cur->_kv = minRight->_kv;// 转换为删除后继节点(后继节点最多只有一个子节点)cur = minRight;parent = minParent;}// 情况2和3:要删除的节点是叶子节点或只有一个子节点Node* child = nullptr;if (cur->_left)child = cur->_left;elsechild = cur->_right;// 更新父节点指针if (child)child->_parent = parent;// 更新根节点或父节点的子节点指针if (parent == nullptr){_root = child;}else{if (cur == parent->_left)parent->_left = child;elseparent->_right = child;// 更新平衡因子UpdateBalanceFactorAfterErase(parent, child);}delete cur;return true;
}void UpdateBalanceFactorAfterErase(Node* parent, Node* child)
{Node* cur = parent;while (cur){// 更新平衡因子if (child == cur->_left)cur->_bf++;elsecur->_bf--;// 根据平衡因子决定下一步操作if (cur->_bf == 1 || cur->_bf == -1){// 高度不变,停止更新break;}else if (cur->_bf == 0){// 高度变化,继续向上更新child = cur;cur = cur->_parent;}else if (cur->_bf == 2 || cur->_bf == -2){// 需要旋转Node* higherChild = nullptr;if (cur->_bf == 2){higherChild = cur->_right;if (higherChild->_bf == 0){RotateL(cur);cur->_bf = 1;higherChild->_bf = -1;break;}else if (higherChild->_bf == 1){RotateL(cur);}else // higherChild->_bf == -1{RotateRL(cur);}}else // cur->_bf == -2{higherChild = cur->_left;if (higherChild->_bf == 0){RotateR(cur);cur->_bf = -1;higherChild->_bf = 1;break;}else if (higherChild->_bf == -1){RotateR(cur);}else // higherChild->_bf == 1{RotateLR(cur);}}// 旋转后继续向上更新child = cur;cur = cur->_parent;}}
}

 

总结

AVL树通过引入平衡因子和旋转操作,确保了二叉搜索树在任何情况下都能保持近似平衡的状态,从而保证了所有操作的时间复杂度都为O(log N)。虽然维护平衡需要额外的开销,但在需要频繁查找的场景下,AVL树提供了稳定的高性能保证。

插入操作的特点

  • 插入后最多只需要一次旋转即可恢复平衡

  • 旋转后子树高度恢复原状,不会影响更高层节点

删除操作的特点

  • 删除后可能需要多次旋转

  • 旋转后子树高度可能变化,需要继续向上更新平衡因子

  • 实现比插入操作更复杂

AVL树的实现虽然相对复杂,但它为我们理解更高级的数据结构(如红黑树)奠定了基础。在实际应用中,我们可以根据具体需求选择是否使用AVL树——当查找操作远多于插入和删除操作时,AVL树是一个优秀的选择。

 

 

 

 

 

 

http://www.dtcms.com/a/511700.html

相关文章:

  • Spring Boot 启动慢?启动过程深度解析与优化策略
  • telnet工具使用详解
  • YOLOv4:目标检测界的 “集大成者”
  • 从零开始的C++学习生活 11:二叉搜索树全面解析
  • 【QT常用技术讲解】控件随窗口自适应变化大小或者移动位置
  • Kafka面试精讲 Day 30:Kafka面试真题解析与答题技巧
  • 江苏建设准考证打印在哪个网站医疗网站 seo怎么做
  • 数据结构9:队列
  • 逆向分析星星充电APP:从签名生成到数据深度解析
  • Vue + WebApi 实现上传下载功能
  • 建设门户网站预算做旅游网站多少钱
  • 【Rust创作】Rust 错误处理:从 panic 到优雅控制
  • 常见激活函数的Lipschitz连续证明
  • 专做皮具的网站网站建设公司排行榜
  • 第三次面试:C++实习开发
  • 公司网站内容更新该怎么做wordpress显示目录
  • 边界扫描测试原理 2 -- 边界扫描测试设备的构成
  • 如何入侵网站后台晴天影视
  • Linux top 命令使用说明
  • 研发图文档管理的革新:从无序到智能协同
  • springboot点餐系统的设计与实现(代码+数据库+LW)
  • ArcoDesignVue Select组件分离问题
  • Python开发:接口场景设计
  • 汽车网站flash模板定制高端网站建设
  • 【Ubuntu18.04 D435i RGB相机与IMU标定详细版(三)】
  • 单肩包自定义页面设计模板seo关键词优化软件app
  • 朊病毒检测市场:技术突破与公共卫生需求驱动下的全球增长
  • 思维清晰的基石:概念和命题解析
  • ubuntu中替换python版本
  • mybatis请求重试工具