1. 为什么我们需要AVL树?从二叉排序树的痛点说起
很多朋友在学数据结构时,都接触过二叉排序树(BST)。它用起来挺直观的:比根节点小的放左边,大的放右边,查找、插入好像都挺快。我自己刚开始用的时候也觉得这结构真不错,写个简单的字典或者索引,代码清晰又好懂。但后来在实际项目里,特别是处理一些有序但可能“偏斜”的数据时,我就踩坑了。
想象一下,你按顺序插入一组数据:1, 2, 3, 4, 5。你的二叉排序树会变成什么样子?它会退化成一个长长的“链条”,也就是我们常说的“斜树”。这时候,树的高度变成了5,查找5这个节点需要从根节点1一路比较到最底下。它的时间复杂度从理想的 O(log n) 退化到了 O(n),这和遍历一个链表几乎没区别了。我当时就遇到了类似的问题,一个本该很快的查询操作,因为数据输入顺序不理想,性能急剧下降,调试了半天才发现是树结构“瘸腿”了。
这就是二叉排序树最大的软肋:它的平衡性完全依赖于输入数据的顺序。而AVL树,就是为了解决这个“平衡性”问题而生的。它是一种自平衡的二叉排序树,由两位苏联数学家 Adelson-Velsky 和 Landis 在1962年提出。它的核心思想很简单:在每次插入或删除节点后,都检查一下树是否“失衡”,如果失衡了,就通过一系列优雅的“旋转”操作,把树重新调整平衡,保证整棵树的高度始终维持在 O(log n) 的水平。这意味着,无论你以什么顺序插入数据,查找、插入、删除操作的时间复杂度都能稳定在 O(log n)。对于需要高性能和稳定响应时间的场景,比如数据库索引、内存中的高速缓存,这个特性至关重要。
所以,学习AVL树,不仅仅是多学一种数据结构,更是掌握一种保证性能下限的工程化思维。接下来,我们就抛开那些枯燥的理论,用C++从零开始,一步步构建一棵真正能用的AVL树,我会把我在实现过程中遇到的“坑”和优化技巧都分享出来。
2. 打好地基:AVL树节点的设计艺术
万事开头难,而设计一个好的节点结构,就是构建稳健AVL树的第一步。这个结构体看似简单,里面却藏着不少影响后续实现便利性和性能的小心思。我们先来看一个最基础的版本:
struct AVLNode {
int key; // 节点存储的关键字
AVLNode* left; // 左孩子指针
AVLNode* right; // 右孩子指针
int height; // 节点的高度
};
这个结构体清晰明了,但用起来会有点别扭。最大的问题是,当我们进行旋转调整时,需要知道当前节点的父节点是谁。如果节点里没有指向父节点的指针,我们就得从根节点开始一层层找下来,或者在递归函数里额外传递父节点信息,代码会变得复杂且低效。所以,我强烈建议加上父指针:
struct AVLNode {
int key;
AVLNode* left;
AVLNode* right;
AVLNode* parent; // 指向父节点的指针
int height;
};
加了parent指针后,旋转操作中对节点关系的重新链接会直观很多。接下来,我们聊聊height这个字段。为什么存高度,而不是平衡因子?平衡因子(左子树高度减右子树高度)确实是判断是否失衡的直接依据。但是,存储高度比存储平衡因子更通用、更安全。
如果你只存平衡因子(比如-1,0,1),当插入或删除后,你需要更新从插入点到根节点路径上所有节点的平衡因子。这个更新过程容易出错,特别是删除操作,情况比较复杂。而存储高度,我们可以通过一个简单的递归函数,在需要时计算任何节点的高度,平衡因子只是两个高度的一次减法。在旋转后,我们也只需要更新局部几个节点的高度即可,逻辑更清晰。计算高度的函数很简单:
// 获取节点的高度,空节点高度定义为0
int getHeight(AVLNode* node) {
return node ? node->height : 0;
}
// 更新节点的高度
void updateHeight(AVLNode* node) {
if (node) {
node->height = 1 + std::max(getHeight(node->left), getHeight(node->right));
}
}
最后,别忘了构造函数。一个好的构造函数能避免很多指针未初始化的野指针问题。
struct AVLNode {
int key;
AVLNode* left;
AVLNode* right;
AVLNode* parent;
int height;
// 构


1624

被折叠的 条评论
为什么被折叠?



