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

数据结构:AVL 树

目录

二叉搜索树的缺点 (Drawbacks of BST)

第一性原理回顾:BST 的性能承诺是什么?

核心缺点:性能承诺的脆弱性——树的退化 (Degeneracy)

如何定义和衡量“平衡”?

AVL 树的诞生 

如何把这个概念落实到代码结构上?

总结


二叉搜索树的缺点 (Drawbacks of BST)

到目前为止,我们一直在赞美 BST。但它并非完美,存在一些致命的缺陷,理解这些缺陷是通往更高级数据结构(如 AVL 树、红黑树)的必经之路。

第一性原理回顾:BST 的性能承诺是什么?

我们当初为什么选择 BST?因为我们期望它的查找、插入、删除操作都能达到 O(logn) 的时间复杂度。

这个期望是建立在一个隐含的假设之上的:

这棵树是“平衡”的,或者说是“枝繁叶茂”的。

“平衡”意味着树的高度 h 大约是 logn。因为每次操作最多走过树的高度,所以性能就是 O(h)approxO(logn)。

核心缺点:性能承诺的脆弱性——树的退化 (Degeneracy)

BST 的最大缺点在于,它无法保证自己永远处于“平衡”状态。树的最终形态完全取决于数据的插入顺序。

最坏情况推导: 如果我们插入一个已经排好序的数组,比如 [10, 20, 30, 40, 50],会发生什么?

  1. 插入 10,成为根。

  2. 插入 2020 > 10,成为 10 的右孩子。

  3. 插入 3030 > 10, 30 > 20,成为 20 的右孩子。

  4. ...以此类推。

最终形成的“树”是这样的:

10\20\30\40\50

这棵所谓的“树”已经退化 (degenerate) 成了一个链表!

在这种退化的树上执行操作:

  • 查找 50:需要从 10 开始,一路向下,访问 5 个节点。

  • 插入 60:需要从 10 开始,一路向下,访问 5 个节点,最后挂在 50 后面。

此时,树的高度 h 等于节点数量 n。所有操作的时间复杂度都从期望的 O(logn) 劣化到了 O(n)。我们费尽心机构建的 BST,其性能又回到了普通数组的水平,优势荡然无存。这就是 BST 最致命的缺点。

普通的搜索二叉树(BST)的性能严重依赖于其“形态”或“高度”。一棵“倾斜”的、不平衡的 BST 会使其性能优势丧失殆尽。

其他缺点

  1. 没有自平衡机制:这是导致核心缺点的原因。标准的 BST 只有一套“左小右大”的构建规则,但没有任何规则来监控和修正树的形态。它对坏的输入顺序(如有序数据)毫无抵抗力。

  2. 删除操作复杂:相比插入和查找,删除一个拥有两个子节点的节点的逻辑要复杂得多,需要“寻找后继”和“值替换”等操作,实现起来更容易出错。

  3. 递归深度问题:对于一棵退化的、深度为 n 的树,如果我们使用递归进行操作,递归调用的深度也会是 n。当 n 非常大时,可能会导致栈溢出 (stack overflow)

总结与展望

特性理想情况 (平衡树)最坏情况 (退化树)
高度O(logn)O(n)
查找O(logn)O(n)
插入O(logn)O(n)
删除O(logn)O(n)

BST 的缺点告诉我们,仅仅依赖“左小右大”的规则是不够的。我们需要一种更强大的数据结构,它能在保持 BST 性质的基础上,增加自平衡 (self-balancing) 的能力。无论我们按什么顺序插入或删除节点,它都能通过一些调整(如旋转操作)来确保树的高度始终维持在 O(logn) 级别。

这就是AVL 树红黑树等高级数据结构被发明出来的原因。它们是 BST 的“加强版”,解决了性能不稳定的核心痛点。


如何定义和衡量“平衡”?

既然问题出在“不平衡”上,那我们的核心目标就是维持树的平衡。

要维持平衡,首先要能衡量平衡。我们怎么用一个具体的、可量化的指标来描述一个节点,甚至一整棵树,是否平衡呢?

想法 A:比较左右子树的节点数量?

这看起来很直观。如果一个节点的左子树有 50 个节点,右子树有 50 个节点,那它应该很平衡吧?

不一定。看这个例子:

      (50)/  \(25)  (75)/       \
(10)      (80)
/           \
(5)         (90)

在根节点 (50) 看来,左子树有 3 个节点,右子树也有 3 个节点。数量上是完美的。但左子树和右子树本身都是严重“倾斜”的。这棵树的整体性能依然不好。

所以,仅仅比较节点数量是不够的,因为它无法真实反映树的高度。

想法 B:比较左右子树的高度?

这个想法更加靠谱。因为我们所有性能分析都和“高度”直接挂钩。如果我们能确保在任何一个节点上,它的左子树和右子树的高度差都不会太大,那么整棵树的高度一定不会太离谱。

一个衡量树平衡的有效指标,是其任意节点的左右子树高度之差。

基于这个结论,我们来定义一个概念:

平衡因子 (Balance Factor, BF)

对于任意一个节点 node,我们定义它的平衡因子为:

BF(node) = height(node->left) - height(node->right)

其中,height(subtree) 代表该子树的高度。我们做一个约定:

  • 如果子树是 NULL (空),那么它的高度为 -1。

  • 如果子树只有一个节点(叶子节点),那么它的高度为 0。

根据这个定义:

  • BF = 0 时,该节点的左右子树高度完全相等。

  • BF = 1 时,该节点的左子树比右子树高 1。

  • BF = -1 时,该节点的右子树比左子树高 1。


AVL 树的诞生 

有了平衡因子这个强大的工具,我们就可以给“平衡的搜索二叉树”下一个非常精确的定义了。

这个定义由两位苏联数学家 G. M. Adelson-Velsky 和 E. M. Landis 在 1962 年提出,所以这种树被称为 AVL 树。

AVL 树的定义:

一棵 AVL 树是满足以下两个条件的搜索二叉树:

  1. 它首先是一棵搜索二叉树 (BST)

  2. 对于树中的每一个节点,其平衡因子的绝对值必须小于等于 1

用公式表达就是,对于任意节点 node| BF(node) | <= 1

这等价于 BF(node) 的取值只能是 -1, 0, 1 中的一个。

一旦某个节点的 BF 变成了 2-2,我们就称这个节点“失衡”(unbalanced),这棵树就不再是 AVL 树了,需要进行调整(我们下次再讲如何调整)。


如何把这个概念落实到代码结构上?

现在我们知道了 AVL 树的定义,那么在 C/C++ 的代码里,我们应该如何表示一个 AVL 树的节点呢?

我们先回顾一下普通 BST 的节点结构:

// 普通搜索二叉树的节点
struct Node {int data;      // 节点存储的数据Node* left;    // 指向左子节点的指针Node* right;   // 指向右子节点的指针
};

这个结构只包含了数据和左右孩子的链接,没有包含任何与“平衡”有关的信息。

根据我们上面的推导,为了判断一个节点是否平衡,我们需要计算它的平衡因子 BF。而要计算 BF,我们又需要知道它左右子树的高度。

那么,最直接的方式,就是在节点结构中,额外增加一个字段来存储以该节点为根的子树的高度

让我们来改进这个结构:

// AVL 树的节点结构
struct AVLTreeNode {int data;                 // 节点存储的数据AVLTreeNode* left;        // 指向左子节点的指针AVLTreeNode* right;       // 指向右子节点的指针int height;               // 关键!我们增加一个字段来存储该节点的高度
};

为什么是存储 height 而不是直接存储 BF

  • height 是更基础的信息。知道了左右子节点的 height,我们可以非常容易地计算出当前节点的 heightBF

    • height(currentNode) = 1 + max(height(currentNode->left), height(currentNode->right))

    • BF(currentNode) = height(currentNode->left) - height(currentNode->right)

  • 如果我们只存 BF,当树的结构发生变化时,BF 的传递和更新会非常复杂。而 height 的更新则相对简单和直观。

所以,我们选择在节点中存储 height 字段。每当创建一个新节点(比如插入时),它作为一个叶子节点,它的 height 就被初始化为 0。


总结

到目前为止,我们已经完成了对 AVL 树的介绍,全程没有涉及任何插入、删除、旋转等操作。让我们回顾一下我们的推导路径:

  1. 发现问题:普通 BST 在极端情况下会退化成链表,性能急剧下降。

  2. 分析根源:问题的根源在于树的“不平衡”,即高度过大。

  3. 寻找指标:为了解决问题,我们需要一个量化指标。我们排除了“节点数量”,选择了“左右子树高度差”作为核心指标,并定义了平衡因子 (BF)

  4. 提出方案:基于平衡因子,我们给出了 AVL 树的严格定义——它是一棵任意节点的 |BF| <= 1 的 BST。

  5. 落实结构:为了在代码中实现这个定义,我们在原有的 BST 节点结构中,增加了一个 height 字段,用于存储每个节点的高度,为后续计算 BF 和维持平衡提供基础。

现在,我们对 AVL 树“是什么”以及“为什么是这样”有了清晰的、基于第一性原理的理解。

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

相关文章:

  • RHCA05-文件系统调优
  • Spark学习
  • 游戏本不插电源适配器不卡设置教程
  • 技术半衰期悖论:AI时代“不可替代领域“的深耕地图
  • 30.Linux cobbler自动化部署
  • 生物信息学深度学习模型比较与学习框架
  • chrome插件开发(一)
  • 23TaskExecutor初始化
  • Windows 命令行:dir 命令
  • MyBatis 动态查询语句详解:让 SQL 更灵活可控
  • 前端调用阿里云接口语音合成演示
  • 20人团队文件共享选哪款?群晖DS925+ 和 DS1525+深度对比
  • 反射基础知识初入(up晚上回家再补完剩下的)
  • Anomalib:在Linux服务器上安装使用Anomalib 2.1.0
  • 生意参谋-市场竞争分析-提升商品成长效率
  • PostgreSQL 中的金钱计算处理
  • C语言第十章内存函数
  • 《SQLAlchemy 2 In Practice》读后感
  • win与ubuntu双系统安装笔记
  • 小波函数多尺度变换的 Curvelet 变换
  • vue3项目,使用vue2方式来写,可以吗
  • 【嵌入式】CAN通信
  • 基于XGBoost算法的数据回归预测 极限梯度提升算法 XGBoost
  • 虚拟机部署HDFS集群
  • JDK 工具
  • IDEA(十四) IntelliJ Idea 常用快捷键(Mac)
  • 会计人员职业发展框架:核心能力构建与进阶路径
  • ROADS落地的架构蓝图
  • Java 通过 m3u8 链接下载所有 ts 视频切片并合并转换为 mp4 格式
  • Odoo 18 通用图片导入工具:从零到一的企业级开发实战