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

【C++游记】AVL树

  

枫の个人主页

你不能改变过去,但你可以改变未来

算法/C++/数据结构/C

Hello,这里是小枫。C语言与数据结构和算法初阶两个板块都更新完毕,我们继续来学习C++的内容呀。C++是接近底层有比较经典的语言,因此学习起来注定枯燥无味,西游记大家都看过吧~,我希望能带着大家一起跨过九九八十一难,降伏各类难题,学会C++,我会尽我所能,以通俗易懂、幽默风趣的方式带给大家形象生动的知识,也希望大家遇到困难不退缩,遇到难题不放弃,学习师徒四人的精神!!!故此得名【C++游记

 话不多说,让我们一起进入今天的学习吧~~~  

目录

一、AVL 树的概念

为什么 AVL 树高度差不要求为 0?

二、AVL 树的实现

2.1 AVL 树的结构

2.2 AVL 树的插入

2.2.1 插入过程概述

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 树平衡检测

 三、结语


一、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?

可能有人会疑惑,为什么 AVL 树要求高度差不超过 1 而不是 0 呢?0 不是更平衡吗?

其实并非不想这样设计,而是在某些情况下无法做到高度差为 0。例如,当树有 2 个结点、4 个结点等情况时,高度差最好就是 1,无法做到高度差为 0。

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

二、AVL 树的实现

2.1 AVL 树的结构

AVL 树的结点结构需要包含键值对、左右孩子指针、父节点指针以及平衡因子。具体定义如下:

template<class K, class V>
struct AVLTreeNode
{// 需要parent指针,后续更新平衡因子会用到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
{typedef AVLTreeNode<K, V> Node;
public:// 各种成员函数实现...
private:Node* _root = nullptr;
};

2.2 AVL 树的插入

2.2.1 插入过程概述

AVL 树插入一个值的过程大致如下:

  1. 按二叉搜索树规则插入新结点
  2. 从新增结点到根结点的路径上更新祖先结点的平衡因子(最坏情况要更新到根,有些情况更新到中间即可停止)
  3. 若更新平衡因子过程中没有出现问题,则插入结束
  4. 若更新过程中出现不平衡,对不平衡子树进行旋转处理。旋转后会降低子树高度,不会再影响上一层,插入结束
2.2.2 平衡因子更新规则

更新原则

  • 平衡因子 = 右子树高度 - 左子树高度
  • 只有子树高度变化才会影响当前结点平衡因子
  • 若新增结点在 parent 的右子树,parent 的平衡因子 ++;若在左子树,parent 的平衡因子 --
  • parent 所在子树的高度是否变化决定了是否继续往上更新

更新停止条件

  1. 若更新后 parent 的平衡因子等于 0(变化为 - 1->0 或 1->0):说明更新前 parent 子树一边高一边低,新增结点插入在低的那边,插入后 parent 所在子树高度不变,不会影响 parent 的父亲结点的平衡因子,更新结束
  2. 若更新后 parent 的平衡因子等于 1 或 - 1(变化为 0->1 或 0->-1):说明更新前 parent 子树两边一样高,插入后一边高一边低,子树符合平衡要求但高度增加了 1,会影响 parent 的父亲结点的平衡因子,需要继续向上更新
  3. 若更新后 parent 的平衡因子等于 2 或 - 2(变化为 1->2 或 - 1->-2):说明更新前 parent 子树一边高一边低,新增结点插入在高的那边,破坏了平衡,需要旋转处理。旋转后子树高度恢复,不需要继续往上更新
  4. 更新到根结点,若根的平衡因子是 1 或 - 1 也停止
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){// 更新当前parent的平衡因子if (cur == parent->_left)parent->_bf--;elseparent->_bf++;// 根据平衡因子判断后续操作if (parent->_bf == 0){// 平衡因子为0,子树高度不变,更新结束break;}else if (parent->_bf == 1 || parent->_bf == -1){// 平衡因子为1或-1,子树高度增加,继续向上更新cur = parent;parent = parent->_parent;}else if (parent->_bf == 2 || parent->_bf == -2){// 平衡因子为2或-2,子树不平衡,需要旋转处理break;}else{// 平衡因子异常,程序出错assert(false);}}// 若需要旋转处理if (parent){if (parent->_bf == 2){if (parent->_right->_bf == 1){// 右右情况,左单旋RotateL(parent);}else if (parent->_right->_bf == -1){// 右左情况,右左双旋RotateRL(parent);}else{assert(false);}}else if (parent->_bf == -2){if (parent->_left->_bf == -1){// 左左情况,右单旋RotateR(parent);}else if (parent->_left->_bf == 1){// 左右情况,左右双旋RotateLR(parent);}else{assert(false);}}}return true;
}

2.3 旋转操作

2.3.1 旋转原则

旋转操作需要遵循两个原则:

  1. 保持搜索树的规则(即旋转后仍满足二叉搜索树的性质)
  2. 让旋转的树从不平衡变为平衡,同时降低旋转树的高度

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

2.3.2 右单旋

右单旋适用于 "左左" 情况,即 parent 的平衡因子为 - 2,且其左孩子的平衡因子为 - 1。

旋转核心步骤

  1. 将 parent 的左孩子(subL)的右子树(subLR)变为 parent 的左子树
  2. 将 parent 变为 subL 的右子树
  3. 更新相关结点的父指针
  4. 若 parent 是根结点,则更新根指针为 subL;否则将 subL 与 parent 的父结点连接
  5. 重置 parent 和 subL 的平衡因子为 0

右单旋代码实现

void RotateR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;// 将subL的右子树变为parent的左子树parent->_left = subLR;if (subLR)subLR->_parent = parent;// 保存parent的父节点Node* parentParent = parent->_parent;// 将parent变为subL的右子树subL->_right = parent;parent->_parent = subL;// 处理与parent的父节点的连接if (parentParent == nullptr){// parent是根节点_root = subL;subL->_parent = nullptr;}else{if (parent == parentParent->_left)parentParent->_left = subL;elseparentParent->_right = subL;subL->_parent = parentParent;}// 重置平衡因子parent->_bf = subL->_bf = 0;
}
2.3.3 左单旋

左单旋适用于 "右右" 情况,即 parent 的平衡因子为 2,且其右孩子的平衡因子为 1。

旋转核心步骤

  1. 将 parent 的右孩子(subR)的左子树(subRL)变为 parent 的右子树
  2. 将 parent 变为 subR 的左子树
  3. 更新相关结点的父指针
  4. 若 parent 是根结点,则更新根指针为 subR;否则将 subR 与 parent 的父结点连接
  5. 重置 parent 和 subR 的平衡因子为 0

左单旋代码实现

void RotateL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;// 将subR的左子树变为parent的右子树parent->_right = subRL;if (subRL)subRL->_parent = parent;// 保存parent的父节点Node* parentParent = parent->_parent;// 将parent变为subR的左子树subR->_left = parent;parent->_parent = subR;// 处理与parent的父节点的连接if (parentParent == nullptr){// parent是根节点_root = subR;subR->_parent = nullptr;}else{if (parent == parentParent->_left)parentParent->_left = subR;elseparentParent->_right = subR;subR->_parent = parentParent;}// 重置平衡因子parent->_bf = subR->_bf = 0;
}
2.3.4 左右双旋

左右双旋适用于 "左右" 情况,即 parent 的平衡因子为 - 2,且其左孩子的平衡因子为 1。

旋转核心步骤

  1. 先以 parent 的左孩子(subL)为旋转点进行左单旋
  2. 再以 parent 为旋转点进行右单旋
  3. 根据 subL 的右孩子(subLR)的平衡因子,调整相关结点的平衡因子

左右双旋代码实现

void RotateLR(Node* parent)
{Node* subL = parent->_left;Node* subLR = subL->_right;int bf = subLR->_bf; // 保存subLR的平衡因子,用于后续调整// 先对subL进行左单旋RotateL(parent->_left);// 再对parent进行右单旋RotateR(parent);// 根据subLR的平衡因子调整各节点的平衡因子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 右左双旋

右左双旋适用于 "右左" 情况,即 parent 的平衡因子为 2,且其右孩子的平衡因子为 - 1。

旋转核心步骤

  1. 先以 parent 的右孩子(subR)为旋转点进行右单旋
  2. 再以 parent 为旋转点进行左单旋
  3. 根据 subR 的左孩子(subRL)的平衡因子,调整相关结点的平衡因子

右左双旋代码实现

void RotateRL(Node* parent)
{Node* subR = parent->_right;Node* subRL = subR->_left;int bf = subRL->_bf; // 保存subRL的平衡因子,用于后续调整// 先对subR进行右单旋RotateR(parent->_right);// 再对parent进行左单旋RotateL(parent);// 根据subRL的平衡因子调整各节点的平衡因子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(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 树平衡检测

为了验证实现的 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;// 计算当前节点的平衡因子(右子树高度 - 左子树高度)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);
}

测试代码

void TestAVLTree1()
{
AVLTree<int, int> t;
// 常规的测试⽤例
//int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
// 特殊的带有双旋场景的测试⽤例
int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
for (auto e : a)
{
t.Insert({ e, e });
}
t.InOrder();
cout << t.IsBalanceTree() << endl;
}

 三、结语

今日C++到这里就结束啦,如果觉得文章还不错的话,可以三连支持一下。感兴趣的宝子们欢迎持续订阅小枫,小枫在这里谢谢宝子们啦~小枫の主页还有更多生动有趣的文章,欢迎宝子们去点评鸭~C++的学习很陡,时而巨难时而巨简单,希望宝子们和小枫一起坚持下去~你们的三连就是小枫的动力,感谢支持~

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

相关文章:

  • 通过 select into outfile / load data infile 进行数据导入导出学习笔记
  • 【网络安全入门基础教程】网络安全就业方向(非常详细)零基础入门到精通,收藏这篇就够了
  • three.js+WebGL踩坑经验合集(10.2):镜像问题又一坑——THREE.InstancedMesh的正反面向光问题
  • 亥姆霍兹线圈和放载流线圈
  • 【SpreadJS V18.2 新特性】Table 与 DataTable 双向转换功能详解
  • SD卡自动检测与挂载脚本
  • React 第七十一节 Router中generatePath的使用详解及注意事项
  • table表格字段明细展示
  • 【前端教程】ES6 Promise 实战教程:从基础到游戏案例
  • django的URL路由配置常用方式
  • C# Task 入门:让你的程序告别卡顿
  • 基于STM32单片机的无线鼠标设计
  • 【ComfyUI】图像反推描述词总结
  • 杰理ac791无法控制io脚原因
  • 【算法】算法题核心类别与通用解题思路
  • 时序数据库IoTDB:为何成为工业数据管理新宠?
  • 【frontend】w3c的发展历史ToDo
  • accelerate、trainer、lightning还是pytorch?
  • SpringBoot 分库分表 - 实现、配置与优化
  • 雅思听力第四课:配对题核心技巧与词汇深化
  • CLion编译基于WSL平台Ubuntu系统的ros项目
  • 1.人工智能——概述
  • 测试开发的角色
  • 动态规划:硬币兑换II
  • 异常类分析
  • HTML应用指南:利用GET请求获取全国招商银行网点位置信息
  • 软件测试面试技巧-面试问题大全
  • 盟接之桥说制造:守正出奇:在能力圈内稳健前行,以需求导向赢得市场
  • 综合实验:DHCP、VLAN、NAT、BDF、策略路由等
  • 数据库主键选择策略分析