深入理解AVL树:结构、旋转及C++实现

1. AVL树的概念

什么是AVL树?

AVL树是一种自平衡的二叉搜索树,其发明者是Adelson-Velsky和Landis,因此得名“AVL”。AVL树是首个自平衡二叉搜索树,通过对树的平衡因子进行控制,确保任何节点的左右子树高度差最多为1,从而保证树的高度为对数级别,即 O(log⁡N)O(\log N)O(logN)。

在AVL树中,插入、删除和查找的时间复杂度都可以保持在 O(log⁡N)O(\log N)O(logN),这使得AVL树在需要频繁查询数据的应用场景中非常高效。

AVL树的平衡因子

每个节点有一个平衡因子(Balance Factor),定义如下:

  • 平衡因子 = 右子树高度 - 左子树高度
  • 对于任何节点,平衡因子的可能取值为 -1、0、1。

AVL树通过保持所有节点的平衡因子在上述范围内,确保了树的平衡。这个平衡性减少了树的高度,从而提高了数据查找效率。

为什么要平衡?

普通的二叉搜索树(BST)在最坏情况下会退化成链表。例如,按照递增顺序插入节点会使树的所有节点都集中在右边,导致高度等于节点数,查找复杂度为 O(N)O(N)O(N)。AVL树通过自动维护平衡性,避免了这种情况,确保所有操作的复杂度始终为对数级别。


2. AVL树的实现

2.1 AVL树的结构

AVL树的节点结构非常类似于普通的二叉树节点,不过增加了一个字段用于存储平衡因子。以下是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 factor)AVLTreeNode(const pair<K, V>& kv): _kv(kv), _left(nullptr), _right(nullptr), _parent(nullptr), _bf(0) {}
};

2.2 AVL树的插入

AVL树的插入操作和普通二叉搜索树的插入类似,但需要额外的步骤来确保树的平衡。如果插入新节点后某些节点失衡,我们需要通过旋转来恢复平衡。

插入步骤概述
  1. 按普通BST的规则插入:首先将节点按照二叉搜索树的规则插入。
  2. 更新平衡因子:从插入节点开始,向上遍历其祖先节点,更新每个节点的平衡因子。
  3. 检查并旋转平衡树:如果某节点的平衡因子变为2或-2,说明该节点失衡。此时需要通过旋转操作恢复平衡。
插入代码实现

以下是AVL树插入节点的代码实现:

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--;} else {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) {// 失衡,需要进行旋转操作Balance(parent);break;} else {assert(false); // 不应存在其他情况}}return true;
}

2.3 旋转操作

当AVL树失去平衡时,通过旋转操作来恢复平衡。旋转的类型分为四种:右单旋左单旋左右双旋右左双旋。每种旋转操作都有其适用场景和具体的实现。

旋转的类型
  1. 右单旋(Right Rotation):用于修正左子树过高的情况。
  2. 左单旋(Left Rotation):用于修正右子树过高的情况。
  3. 左右双旋(Left-Right Rotation):用于修正节点插入在左子树的右侧,导致子树高度增加的情况。
  4. 右左双旋(Right-Left Rotation):用于修正节点插入在右子树的左侧,导致子树高度增加的情况。
右单旋实现

右单旋用于修正某个节点的左子树过高的情况。以下是右单旋的代码:

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;
}
左单旋实现

左单旋用于修正某个节点的右子树过高的情况。以下是左单旋的代码实现:

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;
}
左右双旋和右左双旋实现

当子树的高度增加发生在左右子树中的某一侧时,单次旋转无法恢复平衡,需要进行双次旋转。

  • 左右双旋(Left-Right Rotation):首先对左子树进行左旋,再对祖先节点进行右旋。
  • 右左双旋(Right-Left Rotation):首先对右子树进行右旋,再对祖先节点进行左旋。
void RotateLR(Node* parent) {RotateL(parent->_left);RotateR(parent);
}void RotateRL(Node* parent) {RotateR(parent->_right);RotateL(parent);
}

2.4 AVL树的查找操作

AVL树的查找操作和普通二叉搜索树类似,由于AVL树保持平衡,查找操作的时间复杂度始终为 O(log⁡N)O(\log N)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树的平衡性检测旨在验证每个节点是否满足AVL树的平衡要求,即每个节点的左右子树高度差绝对值不超过1,同时保证每个节点的平衡因子正确反映左右子树的高度差。

为了验证AVL树的平衡性,检测的目标有两个:

  1. 验证左右子树的高度差是否满足AVL条件
  2. 验证每个节点的平衡因子是否正确反映了其子树的高度差

平衡性检测的挑战

在进行平衡性检测时,我们面临两个主要挑战:

  1. 递归遍历的深度与效率问题:对树的每个节点,我们需要递归计算其子树的高度,较高的递归深度可能导致性能下降。
  2. 同步验证平衡因子:在递归计算高度的过程中,我们需要同时验证每个节点的平衡因子是否正确。

平衡性检测的代码实现

以下是用于检测AVL树平衡性和正确性的代码:

int _Height(Node* root) {if (root == nullptr) return 0;int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);return max(leftHeight, rightHeight) + 1;
}bool _IsBalanceTree(Node* root) {if (root == nullptr) return true;// 计算左右子树的高度int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);int diff = rightHeight - leftHeight;// 检查高度差是否符合AVL条件if (abs(diff) > 1) {cout << root->_kv.first << " 高度差异常: 左高=" << leftHeight << ", 右高=" << rightHeight << endl;return false;}// 检查平衡因子是否正确if (root->_bf != diff) {cout << root->_kv.first << " 平衡因子异常: 期望=" << diff << ", 实际=" << root->_bf << endl;return false;}// 递归检测左右子树是否平衡return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}
代码说明
  1. 高度计算_Height 函数用于计算节点的高度,递归遍历每个节点的左右子树,返回最大的高度加1。
  2. 平衡性验证_IsBalanceTree 函数用于检测树的平衡性。它首先计算每个节点的左右子树高度差,然后检查其平衡因子是否符合AVL树的定义。
  3. 详细输出:为了帮助调试,函数在检测到异常时输出详细的信息,包括节点的键值、高度差和不匹配的平衡因子值。

平衡性检测的改进:优化高度计算

在上述实现中,_Height 函数被重复调用多次,可能会导致效率低下。特别是在平衡性检测过程中,每次都要重新递归计算子树的高度。我们可以通过以下优化来提升效率。

同时计算高度与检测平衡性

我们可以通过一次递归同时计算高度和检测平衡性,避免重复的高度计算。以下是改进后的代码:

// 新的平衡检测函数,返回子树的高度并同时检测是否平衡
int _CheckBalance(Node* root, bool& isBalanced) {if (root == nullptr) return 0;int leftHeight = _CheckBalance(root->_left, isBalanced);int rightHeight = _CheckBalance(root->_right, isBalanced);// 如果在递归过程中已经发现不平衡,直接返回if (!isBalanced) return 0;// 计算当前节点的高度差int diff = rightHeight - leftHeight;// 检查高度差是否符合AVL条件if (abs(diff) > 1) {cout << "节点 " << root->_kv.first << " 高度差异常: 左高=" << leftHeight << ", 右高=" << rightHeight << endl;isBalanced = false;}// 检查平衡因子是否正确if (root->_bf != diff) {cout << "节点 " << root->_kv.first << " 平衡因子异常: 期望=" << diff << ", 实际=" << root->_bf << endl;isBalanced = false;}// 返回子树的高度return max(leftHeight, rightHeight) + 1;
}// 检测整棵树是否平衡的入口函数
bool IsBalanceTree() {bool isBalanced = true;_CheckBalance(_root, isBalanced);return isBalanced;
}
代码改进点
  1. 高度计算与平衡检测结合

    • 在新的实现中,_CheckBalance 函数同时执行高度计算和平衡性检测。
    • 递归调用返回子树的高度,同时检查每个节点的平衡性,从而减少了重复的递归操作。
  2. 标志位

    • 通过 bool& isBalanced 参数作为标志位,当发现树中有不平衡的节点时,将其设为 false,并立即终止后续的递归。
    • 这样可以避免不必要的计算,提高检测的整体效率。
  3. 减少重复计算

    • 与之前的版本相比,新的实现避免了重复的高度计算,每个节点只需一次遍历即可完成高度计算和平衡性检测。

进阶:检查树的平衡因子及其更新的正确性

除了检测平衡性之外,还可以扩展检测模块,进一步确保AVL树中的每个节点的平衡因子在插入和旋转操作之后都得到了正确更新。以下是检测平衡因子的代码。

验证每个节点的平衡因子

在进行插入、删除等操作后,平衡因子必须保持正确更新。我们可以通过递归遍历整棵树来验证每个节点的平衡因子是否准确反映其子树的高度差。

bool ValidateBalanceFactors(Node* root) {if (root == nullptr) return true;// 递归获取左右子树高度int leftHeight = _Height(root->_left);int rightHeight = _Height(root->_right);int expectedBF = rightHeight - leftHeight;// 检查平衡因子是否正确if (root->_bf != expectedBF) {cout << "节点 " << root->_kv.first << " 的平衡因子不正确。应为 " << expectedBF << ",实际为 " << root->_bf << endl;return false;}// 递归验证左右子树的平衡因子return ValidateBalanceFactors(root->_left) && ValidateBalanceFactors(root->_right);
}
代码说明
  • ValidateBalanceFactors 函数遍历整个树,检查每个节点的平衡因子是否与其左右子树的高度差匹配。
  • 这种检测可以在每次插入、删除或旋转之后调用,以确保树在操作后没有出现错误的平衡因子。
  • 如果发现平衡因子不正确,程序会输出详细的错误信息,包括节点的键值、应有的平衡因子和当前存储的平衡因子。

平衡性检测的应用场景

  • 单元测试:在开发AVL树时,可以在每次插入、删除或旋转操作后调用平衡性检测函数,作为单元测试的一部分。
  • 调试与验证:通过详细的错误信息输出,开发者可以快速定位到问题节点,从而帮助调试和验证代码的正确性。
  • 性能优化:平衡性检测不仅可以帮助验证算法的正确性,还可以用于评估在不同数据分布和操作顺序下,AVL树的性能表现是否达到了预期。

3. AVL树的应用场景及优势

AVL树适合应用于需要高效查找的场景中,例如数据库中的索引结构、缓存系统中的快速查找等。相比于普通的二叉搜索树,AVL树保证了每次操作的时间复杂度为 O(log⁡N)O(\log N)O(logN),特别适合频繁插入和删除的应用。


4. C++实现的完整代码示例

以下是一个AVL树完整的C++实现代码示例,结合了插入、旋转、查找和检测的实现。

template<class K, class V>
class AVLTree {typedef AVLTreeNode<K, V> Node;public:bool Insert(const pair<K, V>& kv);Node* Find(const K& key);void InOrder() const;bool IsBalanceTree();private:Node* _root = nullptr;void RotateR(Node* parent);void RotateL(Node* parent);void RotateLR(Node* parent);void RotateRL(Node* parent);
};

在实际编程中,使用AVL树可以保证数据的有序性,同时保证在最坏情况下依然具有高效的时间复杂度,非常适合需要高频率动态数据维护的场景。


5. 结论

AVL树是一种经典的自平衡二叉搜索树,通过引入平衡因子和旋转操作,保持了树的平衡性,确保了插入、删除和查找操作的高效性。通过学习AVL树,我们可以深入理解数据结构的自平衡机制,以及如何在二叉树中保持最优的性能。

希望通过这篇博客,大家对AVL树的概念、实现和用途有更深的了解。如果你有任何疑问或者想了解更多相关内容,欢迎随时交流。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/62971.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Css动画:旋转相册动画效果实现

​&#x1f308;个人主页&#xff1a;前端青山 &#x1f525;系列专栏&#xff1a;Css篇 &#x1f516;人终将被年少不可得之物困其一生 依旧青山,本期给大家带来Css篇专栏内容:Css动画:旋转相册动画效果实现 前言 随着Web技术的发展&#xff0c;网页不再局限于静态展示&#…

ElasticSearch 与向量数据库的结合实践:突破亿级大表查询瓶颈20241204

&#x1f4a1; ElasticSearch 与向量数据库的结合实践&#xff1a;突破亿级大表查询瓶颈 &#x1f4da; 引言 随着业务规模的不断扩大&#xff0c;传统关系型数据库在处理 亿级大表 时&#xff0c;性能瓶颈愈加凸显。关键词检索、模糊查询、多条件筛选等需求逐步升级&#xff…

解决stable-diffusion-webui时的问题:No module ‘xformers‘. Proceeding without it

p.s 被另一篇文章坑了&#xff0c;装个xformers把我原先的pytorch降智了&%$^# 注意&#xff1a;&#xff01;&#xff01;&#xff01;xformers非强制安装&#xff1b;可优化显存&#xff0c;提高性能和出图速率&#xff0c;对于GPU能力有限的用户很有用&#xff1b;安装过…

如何加强游戏安全,防止定制外挂影响游戏公平性

在现如今的游戏环境中&#xff0c;外挂始终是一个困扰玩家和开发者的问题。尤其是定制挂&#xff08;Customized Cheats&#xff09;&#xff0c;它不仅复杂且隐蔽&#xff0c;更能针对性地绕过传统的反作弊系统&#xff0c;对游戏安全带来极大威胁。定制挂通常是根据玩家的需求…

【Python系列】使用 `psycopg2` 连接 PostgreSQL 数据库

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

Linux 权限管理:用户分类、权限解读与常见问题剖析

&#x1f31f; 快来参与讨论&#x1f4ac;&#xff0c;点赞&#x1f44d;、收藏⭐、分享&#x1f4e4;&#xff0c;共创活力社区。&#x1f31f; &#x1f6a9;用通俗易懂且不失专业性的文字&#xff0c;讲解计算机领域那些看似枯燥的知识点&#x1f6a9; 目录 &#x1f4af;L…

LeetCode 力扣 热题 100道(十四)二叉树的中序遍历(C++)

给定一个二叉树的根节点 root &#xff0c;返回 它的 中序 遍历 。 如下为代码&#xff1a; /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullpt…

运费微服务和redis存热点数据

目录 运费模板微服务 接收前端发送的模板实体类 插入数据时使用的entity类对象 BaseEntity类 查询运费模板服务 新增和修改运费模块 整体流程 代码实现 运费计算 整体流程 总的代码 查找运费模板方法 计算重量方法 Redis存入热点数据 1.从nacos导入共享redis配置…

【C++】—— set 与 multiset

【C】—— map 与 set 1 序列式容器和关联式容器2 set 系列的使用2.1 set 和 multiset 参考文档2.2 set 类的介绍2.3 set 的迭代器和构造2.4 set的增删查2.4.1 insert2.4.2 find 与 erase2.4.3 count 2.5 lower_bound 与 upper_bound2.6 multiset 与 set 的差异2.6.1 不再去重2…

Jupyter Notebook认识、安装和启动以及使用

Jupyter Notebook认识、安装和启动以及使用 Jupyter Notebook认识、安装和启动以及使用 Jupyter Notebook认识、安装和启动以及使用一、认识Jupyter Notebook1.1 Jupyter Notebook概述1.2 Jupyter Notebook 重要特性(1)交互式代码执行(2)支持多种编程语言(3)富文本编辑(4)代码高…

算法第一弹-----双指针

目录 1.移动零 2.复写零 3.快乐数 4.盛水最多的容器 5.有效三角形的个数 6.查找总价值为目标值的两个商品 7.三数之和 8.四数之和 双指针通常是指在解决问题时&#xff0c;同时使用两个指针&#xff08;变量&#xff0c;常用来指向数组、链表等数据结构中的元素位置&am…

【后端面试总结】golang channel深入理解

在Go语言中&#xff0c;Channel是一种用于在goroutine之间进行通信和同步的重要机制。它提供了一种安全、类型安全的方式来传递数据&#xff0c;使得并发编程变得更加直观和简单。本文将详细介绍Golang中Channel的基本概念、创建与关闭、发送与接收操作&#xff0c;以及相关的使…

华为、华三交换机纯Web下如何创关键VLANIF、操作STP参数

华为交换机WEB操作 使用的是真机S5735&#xff0c;目前主流的版本都适用&#xff08;V1R5~V2R1的就不在列了&#xff0c;版本太老了&#xff0c;界面完全不一样&#xff0c;这里调试线接的console口&#xff0c;电脑的网络接在ETH口&#xff09; 「模拟器、工具合集」复制整段内…

详解Java数据库编程之JDBC

目录 首先创建一个Java项目 在Maven中央仓库下载mysql connector的jar包 针对MySQL版本5 针对MySQL版本8 下载之后&#xff0c;在IDEA中创建的项目中建立一个lib目录&#xff0c;然后把刚刚下载好的jar包拷贝进去&#xff0c;然后右键刚刚添加的jar包&#xff0c;点击‘添…

网络(TCP)

目录 TCP socket API 详解 套接字有哪些类型&#xff1f;socket有哪些类型&#xff1f; 图解TCP四次握手断开连接 图解TCP数据报结构以及三次握手&#xff08;非常详细&#xff09; socket缓冲区以及阻塞模式详解 再谈UDP和TCP bind(): 我们的程序中对myaddr参数是这样…

【笔记】离散数学 1-3 章

1. 数理逻辑 1.1 命题逻辑的基本概念 1.1.1 命题的概念 命题&#xff08;Proposition&#xff09;&#xff1a;是一个陈述句&#xff0c;它要么是真的&#xff08;true&#xff09;&#xff0c;要么是假的&#xff08;false&#xff09;&#xff0c;但不能同时为真和假。例如…

【Linux篇】权限管理 - 用户与组权限详解

一. 什么是权限&#xff1f; 首先权限是限制人的。人 真实的人 身份角色 权限 角色 事物属性 二. 认识人–用户 Linux下的用户分为超级用户和普通用户 root :超级管理员&#xff0c;几乎不受权限的约束普通用户 :受权限的约束超级用户的命令提示符是#&#xff0c;普通用…

【机器学习】机器学习的基本分类-监督学习-决策树-C4.5 算法

C4.5 是由 Ross Quinlan 提出的决策树算法&#xff0c;是对 ID3 算法的改进版本。它在 ID3 的基础上&#xff0c;解决了以下问题&#xff1a; 处理连续型数据&#xff1a;支持连续型特征&#xff0c;能够通过划分点将连续特征离散化。处理缺失值&#xff1a;能够在特征值缺失的…

2023年MathorCup高校数学建模挑战赛—大数据竞赛B题电商零售商家需求预测及库存优化问题求解全过程文档及程序

2023年MathorCup高校数学建模挑战赛—大数据竞赛 B题 电商零售商家需求预测及库存优化问题 原题再现&#xff1a; 电商平台存在着上千个商家&#xff0c;他们会将商品货物放在电商配套的仓库&#xff0c;电商平台会对这些货物进行统一管理。通过科学的管理手段和智能决策&…

cocotb pytest

打印python中的print &#xff0c; 应该使用 pytest -s pytest --junitxmltest_report.xml --htmlreport.html