完整教程:数据结构入门 (十一):“自我平衡”的艺术 —— 详解AVL树
文章目录
引言:当“秩序”走向“极端”
在上一篇文章中,我们见证了二叉搜索树(BST)的强大——它依靠“左小右大”的秩序,在平均情况下提供了 O(log n) 的高效操作。
然而,这份“秩序”是脆弱的。如果我们按顺序 (1, 2, 3, 4, 5) 插入数据,会发现了一个大大的问题:,BST会退化成一条单链表,所有操作的效率也随之退化到 O(n)。我们精心构建的“秩序”反而成了我们的枷锁。
![![BST退化成链表的示意图]](https://i-blog.csdnimg.cn/direct/7e6f1b62a9cd485fb8742b559fb52368.png)
为了让操作效率始终保持在O(logn),两位苏联数学家 Adelson-Velsky 和 Landis 在1962年提出了一种严格的自平衡结构——AVL树。
一、平衡的“标尺”:平衡因子 (BF)
AVL 树的实现,就是在二叉搜索树的基础上,增加了一条“铁律”:
树中任意一个节点的左、右子树的高度差(平衡因子),其绝对值不能超过1。
平衡因子 (BF) = 左子树高度 - 右子树高度
因此,在一个合法的 AVL 树中,每个节点的平衡因子 BF 只能是:
1:左子树比右子树高1层(左高)。0:左、右子树等高。-1:右子树比左子树高1层(右高)。
一旦某个节点的 BF 变成了 2 或 -2,就说明树“失衡”了,必须立刻进行调整。
二、“拨乱反正”:AVL树的四种旋转
AVL 树保持平衡的秘诀,就在于它在插入或删除后,会沿着路径回溯,检查路径上所有祖先节点的平衡因子。一旦发现失衡(BF 变为 ±2),就会立即启动“旋转”操作,进行局部重构,使子树恢复平衡。
旋转的本质是在保持BST“左小右大”性质(即中序遍历不变)的前提下,通过指针的挪移来降低子树的高度。
根据新节点插入的位置,有四种失衡情况:
1. LL 型(左左):右旋
- 成因:新节点插入到了“失衡节点”31的左子树的左侧。
- 表现:31 的
BF变为2,31 的左孩子 26 的BF变为1。

- 调整:对 26 进行右旋。
- 26 提拔为新的根。
- 31 降级为 26 的右孩子。
- 26 原来的右子树(28)“过继”给 31,成为 31 的左子树。

2. RR 型(右右):左旋
成因:新节点插入到了“失衡节点”56 的右子树的右侧。
表现:56 的
BF变为-2,56 的右孩子 78 的BF变为-1。
调整:对 56 进行左旋。
- 78 提拔为新的根。
- 56 降级为 78 的左孩子。
- 78 原来的左子树(66)“过继”给 56,成为 56 的右子树。

3. LR 型(左右):先左旋再右旋
成因:新节点插入到了“失衡节点”75 的左子树的右侧。
表现:75 的
BF变为2,75的左孩子 45 的BF变为-1。
调整:
- 对 45 (75的左孩子) 进行一次左旋,将其转化为 LL 型。
- 对 75 (失衡节点) 进行一次右旋。

4. RL 型(右左):先右旋再左旋
- 成因:新节点插入到了“失衡节点”45 的右子树的左侧。
- 表现:45 的
BF变为-2,A 的右孩子 75 的BF变为1。
- 调整:
- 对 75 (45的右孩子) 进行一次右旋,将其转化为 RR 型。
- 对 45 (失衡节点) 进行一次左旋。

三、AVL树的C语言实现
1. 结构设计
每个节点必须额外存储 height(高度)字段,平衡因子 BF 可以通过左右子树的 height 动态计算得出。
#include <stdio.h>
#include <stdlib.h>
typedef int Element;
// 平衡二叉树的节点结构
typedef struct _avl_Node
{
Element data;
struct _avl_Node *left, *right;
int height; // 节点高度 (以该节点为根的子树的最大高度)
} AVLNode;
// 平衡二叉树的树头结构
typedef struct
{
AVLNode *root;
int count;
} AVLTree;
2. 核心辅助函数
// 获取节点高度(NULL节点高度为0)
static int h(AVLNode *node) {
if (node == NULL) {
return 0;
}
return node->height;
}
// 比较并取更大值
static int maxNum(int a, int b) {
return (a > b) ? a : b;
}
// 计算平衡因子
static int getBalance(const AVLNode *node) {
if (node == NULL) {
return 0;
}
return h(node->left) - h(node->right);
}
// 创建一个新节点
static AVLNode *createAVLNode(Element data, AVLTree* tree) {
AVLNode *node = (AVLNode*)malloc(sizeof(AVLNode));
if (node == NULL) {
return NULL;
}
node->data = data;
node->left = node->right = NULL;
node->height = 1; // 新节点高度默认为1
tree->count++;
return node;
}
3. 旋转操作 (核心)
/* 左旋操作
* px px
* | |
* x y
* / \ ---> / \
* lx y x ry
* / \ / \
* ly ry lx ly
*/
static AVLNode *leftRotate(AVLNode *x)
{
AVLNode* y = x->right;
x->right = y->left;
y->left = x;
// 更新高度(必须先更新x,再更新y)
x->height = maxNum(h(x->left), h(x->right)) + 1;
y->height = maxNum(h(y->left), h(y->right)) + 1;
return y;
}
/* py py
* | |
* y x
* / \ ---> / \
* x ry lx y
* / \ / \
* lx rx rx ry
*/
static AVLNode *rightRotate(AVLNode *y)
{
AVLNode* x = y->left;
y->left = x->right;
x->right = y;
// 更新高度(必须先更新y,再更新x)
y->height = maxNum(h(x->left), h(x->right)) + 1;
x->height = maxNum(h(y->left), h(y->right)) + 1;
return y;
}
4. 插入操作 (带平衡调整)
AVL 树的插入,就是在 BST 插入的递归回溯过程中,增加了检查平衡并执行旋转的步骤。
// 插入的递归辅助函数
static AVLNode *insertAVLNode(AVLTree* tree, AVLNode *node, Element e) {
// 1. 递归的初始化位置
if (node == NULL) {
return createAVLNode(e, tree);
}
// 递的过程
if (e < node->data) {
node->left = insertAVLNode(tree, node->left, e);
} else if (e > node->data) {
node->right = insertAVLNode(tree, node->right, e);
} else {
return node; // 不允许插入重复值
}
// 2. 此时的代码,已经进入到归的过程,更新这条路径上节点高度,同时检测平衡因子
// 2.1 归过程中的节点高度的更新
updateHeight(node);
// 2.2 计算当前节点的平衡因子
int balance = getBalance(node);
// 3. 检查是否失衡,并执行相应旋转
if (balance > 1) {
// 左边的高度大了
if (e > node->left->data) {
// LR
node->left = leftRotate(node->left);
}
// LL
return rightRotate(node);
}
if (balance < -1){
if (e < node->right->data) {
// RL
node->right = rightRotate(node->right);
}
// RR
return leftRotate(node);
}
return node;
}
// 对外接口:插入
void insertAVLTree(AVLTree* tree, Element data) {
if (tree) {
tree->root = insertAVLNode(tree, tree->root, data);
}
}
5. 删除操作 (带平衡调整)
删除操作与插入类似,但在删除节点后(尤其是度为2的节点,替换前驱/后继后),也需要在回溯路径上检查平衡性并进行旋转。
// 删除的递归辅助函数
static AVLNode *deleteAVLNode(AVLTree *tree, AVLNode *node, Element e) {
if (node == NULL) {
return NULL; // 未找到
}
// 1. 找到要删除的节点
if (e < node->data) {
node->left = deleteAVLNode(tree, node->left, e);
} else if (e > node->data) {
node->right = deleteAVLNode(tree, node->right, e);
} else {
// 找到了,执行删除
AVLNode *tmp;
if (node->left == NULL || node->right == NULL) {
tmp = node->left ? node->left : node->right;
if (tmp == NULL) {
// 度为0,直接删除
tree->count--;
free(node);
return NULL;
}
// 度为1,将tmp的值总结替换成node
node->data = tmp->data;
node->left = tmp->left;
node->right = tmp->right;
tree->count--;
free(tmp);
} else {
// 度为2的点,找前驱节点
tmp = node->left;
while (tmp->right) {
tmp = tmp->right;
}
node->data = tmp->data;
node->left = deleteAVLNode(tree, node->left, tmp->data);
}
}
// 2.归的过程中,更新平衡因子
updateHeight(node);
// 3. 计算平衡因子
int balance = getBalance(node);
// 4. 检查并执行旋转 (逻辑与插入时类似,但判断条件略有不同)
if (balance > 1) {
if (getBalance(node->left) < 0) {
node->left = leftRotate(node->left);
}
return rightRotate(node);
}
if (balance < -1) {
if (getBalance(node->right) > 0) {
node->right = rightRotate(node->right);
}
return leftRotate(node);
}
return node;
}
// 对外接口:删除
void deleteAVLTree(AVLTree* tree, Element e) {
if (tree) {
tree->root = deleteAVLNode(tree, tree->root, e);
}
}
四、总结:严格平衡的“代价”
AVL 树以其极其严格的平衡策略(BF 绝对值不超过1),确保了无论数据如何插入,树的高度始终被“压”在 O(log n) 级别,提供了极其稳定的 O(log n) 查找效率。
但这份“严格”是有代价的:
- 优点:查找效率极高且非常稳定,非常适合查找密集型的应用。
- 缺点:为了维持这种严格平衡,插入和删除时可能需要频繁的旋转(最多
O(log n)次旋转),这使得其“写”操作的开销比 BST 更大。
至此,我们对“树”这种层级结构的探索,已经达到了一个相当的深度。我们一直在研究一个集合内部的“父子”、“兄弟”关系。
然而,在现实世界中,我们还经常遇到另一类完全不同的问题,它不关心“层级”,只关心“分组”与“归属”。
想象一下:
- 最初:我们有成千上万个居民,每个人都是一个独立的个体(n 个元素,n 个独立的集合)。
- 操作1:村长宣布,张三家和李四家“联谊”了,从此并为一个大家族(合并 Union:将两个集合合并)。
- 操作2:你需要快速判断,王五和赵六是不是“自家人”?(查找 Find:查询两个元素是否在同一个集合中)。
这种专门用来处理“动态集合合并”与“归属关系查询”问题的利器,就是我们下一篇文章将要探索的,看似简单却蕴含惊人效率的数据结构:并查集 。

浙公网安备 33010602011771号