一、AVL 树基础概念
1.1 为什么需要 AVL 树?
普通的二叉搜索树(BST)在理想情况下(完全平衡),查找、插入、删除的时间复杂度都是O(log n) 。但如果插入的数据是有序的(如 1,2,3,4,5),二叉搜索树会退化成链表 ,此时时间复杂度会恶化到O(n),这在数据量大时是不可接受的。
AVL 树 就是为了解决这个问题而诞生的。它是一种自平衡的二叉搜索树 ,通过在插入和删除操作后进行旋转调整,保证树的任意节点的左右子树高度差不超过 1,从而始终维持 O (log n) 的时间复杂度。
1.2 AVL 树的定义
AVL 树满足以下两个条件:
- 它首先是一棵二叉搜索树(左子树所有节点值 < 根节点值 < 右子树所有节点值)
- 任意节点的平衡因子(左子树高度 - 右子树高度)的绝对值不超过 1
1.3 核心术语
- 节点高度:从该节点到其最远叶子节点的路径上的边数(空节点高度定义为 - 1,叶子节点高度为 0)
- 平衡因子:左子树高度 - 右子树高度,取值范围为 {-1, 0, 1}
- 失衡:当某个节点的平衡因子绝对值等于 2 时,该节点所在的子树失衡,需要进行旋转调整
二、AVL 树的四种失衡情况与旋转操作
AVL 树的失衡只有四种情况,对应四种旋转操作:
表格
| 失衡类型 | 平衡因子情况 | 旋转方式 |
|---|---|---|
| LL 型 | 节点平衡因子 = 2,左孩子平衡因子≥0 | 单右旋 |
| RR 型 | 节点平衡因子 =-2,右孩子平衡因子≤0 | 单左旋 |
| LR 型 | 节点平衡因子 = 2,左孩子平衡因子 =-1 | 先左旋左孩子,再右旋当前节点 |
| RL 型 | 节点平衡因子 =-2,右孩子平衡因子 = 1 | 先右旋右孩子,再左旋当前节点 |
三、完整代码实现与逐行讲解
3.1 头文件与结构体定义
cpp
#include<stdio.h>
#include<iostream>
#include<cassert>
#include<string.h>
#include<stack>
using namespace std;
// AVL树节点结构体
typedef struct Avlnode
{
int val; // 节点存储的值
int height; // 节点的高度
struct Avlnode* rightchild; // 右孩子指针
struct Avlnode* leftchild; // 左孩子指针
}Avlnode;
// AVL树结构体(封装根节点,方便操作)
typedef struct AvlTree
{
struct Avlnode* root; // 树的根节点
}AvlTree;
讲解:
- 每个节点除了存储值和左右孩子指针外,还额外存储了高度,避免每次计算平衡因子时都递归遍历子树
- 用
AvlTree结构体封装根节点,而不是直接用Avlnode*,这样对外接口更统一,也方便后续扩展
3.2 基础工具函数
3.2.1 购买节点
cpp
// 1. 购买节点(创建新节点)
Avlnode* BuyNode(int val)
{
// 动态分配内存
Avlnode* node = (Avlnode*)malloc(sizeof(Avlnode));
if (node == nullptr)
{
printf("内存分配失败!\n");
return nullptr;
}
// 初始化节点属性
node->height = 0; // 新节点是叶子节点,高度为0
node->leftchild = nullptr;
node->rightchild = nullptr;
node->val = val;
return node;
}
3.2.2 获取节点高度
cpp
// 2. 获取节点的高度
int Get_height(Avlnode* node)
{
// 空节点高度定义为-1(这是AVL树的标准定义)
if (node == nullptr)
{
return -1;
}
return node->height;
}
3.2.3 更新节点高度
cpp
// 3. 更新当前节点的高度
int Updata_Height(Avlnode* node)
{
if (node == nullptr)
{
return -1;
}
// 节点高度 = 左右子树高度的最大值 + 1
int height_right = Get_height(node->rightchild);
int height_left = Get_height(node->leftchild);
node->height = (height_right > height_left ? height_right + 1 : height_left + 1);
return node->height;
}
注意 :原代码中这里有个 bug------ 只计算了高度但没有赋值给node->height,导致后续平衡因子计算错误。
3.2.4 获取平衡因子
cpp
// 4. 获取当前节点的平衡因子
int Get_Balance_Factor(Avlnode* node)
{
if (node == nullptr)
{
return 0;
}
// 平衡因子 = 左子树高度 - 右子树高度
return Get_height(node->leftchild) - Get_height(node->rightchild);
}
3.3 核心旋转操作
3.3.1 单左旋(处理 RR 型失衡)
cpp
// 5. 单左旋(处理RR型失衡)
Avlnode* Left_Rotate(Avlnode* node)
{
assert(node != nullptr);
assert(node->rightchild != nullptr); // 左旋必须有右孩子
Avlnode* child = node->rightchild; // 右孩子成为新的根
Avlnode* grandchild = child->leftchild; // 右孩子的左孩子
// 旋转操作
node->rightchild = grandchild; // 原根的右孩子指向孙子节点
child->leftchild = node; // 新根的左孩子指向原根
// 更新高度(先更新原根,再更新新根)
Updata_Height(node);
Updata_Height(child);
return child; // 返回新的根节点
}
左旋示意图:
cpp
node child
/ \ / \
A child => node C
/ \ / \
B C A B
3.3.2 单右旋(处理 LL 型失衡)
cpp
// 6. 单右旋(处理LL型失衡)
Avlnode* right_Rotate(Avlnode* node)
{
assert(node != nullptr);
assert(node->leftchild != nullptr); // 右旋必须有左孩子
Avlnode* child = node->leftchild; // 左孩子成为新的根
Avlnode* grandchild = child->rightchild; // 左孩子的右孩子
// 旋转操作
node->leftchild = grandchild; // 原根的左孩子指向孙子节点
child->rightchild = node; // 新根的右孩子指向原根
// 更新高度(先更新原根,再更新新根)
Updata_Height(node);
Updata_Height(child);
return child; // 返回新的根节点
}
右旋示意图:
cpp
node child
/ \ / \
child C => A node
/ \ / \
A B B C
3.3.3 通用旋转函数(自动判断旋转类型)
cpp
// 7. 通用旋转函数(根据平衡因子自动判断旋转类型)
Avlnode* Rotate(Avlnode* node)
{
assert(node != nullptr);
int balance = Get_Balance_Factor(node);
// 左子树更高(平衡因子=2)
if (balance == 2)
{
if (Get_Balance_Factor(node->leftchild) >= 0)
{
// LL型:单右旋
return right_Rotate(node);
}
else
{
// LR型:先左旋左孩子,再右旋当前节点
node->leftchild = Left_Rotate(node->leftchild);
return right_Rotate(node);
}
}
// 右子树更高(平衡因子=-2)
if (balance == -2)
{
if (Get_Balance_Factor(node->rightchild) <= 0)
{
// RR型:单左旋
return Left_Rotate(node);
}
else
{
// RL型:先右旋右孩子,再左旋当前节点
node->rightchild = right_Rotate(node->rightchild);
return Left_Rotate(node);
}
}
// 如果没有失衡,直接返回原节点
return node;
}
注意:原代码中 RL 旋转有个严重 bug------ 错误地旋转了左孩子而不是右孩子,这会导致程序崩溃或树结构错误。
3.4 AVL 树初始化与销毁
cpp
// 8. 初始化AVL树
void Init_AVLTree(AvlTree* pTree)
{
assert(pTree != nullptr);
pTree->root = nullptr;
}
// 9. 递归销毁节点
void Destory(Avlnode* root)
{
if (root == NULL)
return;
// 后序遍历销毁(先销毁左右子树,再销毁根节点)
Destory(root->leftchild);
Destory(root->rightchild);
free(root);
}
// 10. 销毁整个AVL树
void Destory_AVLTree(AvlTree* pTree)
{
assert(pTree != nullptr);
Destory(pTree->root);
pTree->root = nullptr; // 根节点置空,防止野指针
}
3.5 插入操作
cpp
// 11. 插入帮助函数(递归实现)
Avlnode* Insert_Helper(Avlnode* node, int val)
{
// 1. 找到插入位置,创建新节点
if (node == nullptr)
{
return BuyNode(val);
}
// 2. 二叉搜索树的插入逻辑
if (val < node->val)
{
node->leftchild = Insert_Helper(node->leftchild, val);
}
else if (val > node->val)
{
node->rightchild = Insert_Helper(node->rightchild, val);
}
else
{
// 3. 值已存在,不插入
printf("值%d已存在,无需插入!\n", val);
return node;
}
// 4. 回溯时更新当前节点的高度
Updata_Height(node);
// 5. 检查并修复失衡
return Rotate(node);
}
// 12. 插入节点(对外接口)
bool Insert(AvlTree* pTree, int val)
{
assert(pTree != nullptr);
pTree->root = Insert_Helper(pTree->root, val);
return true;
}
插入操作流程:
- 按照二叉搜索树的规则找到插入位置
- 创建新节点并插入
- 从插入位置向上回溯,更新每个节点的高度
- 检查每个节点是否失衡,如果失衡则进行相应的旋转调整
3.6 删除操作
cpp
// 13. 删除帮助函数(递归实现)
Avlnode* Delete_help(Avlnode* node, int val)
{
// 1. 节点不存在,返回空
if (node == nullptr)
{
printf("值%d不存在,无法删除!\n", val);
return nullptr;
}
// 2. 二叉搜索树的删除逻辑
if (val < node->val)
{
node->leftchild = Delete_help(node->leftchild, val);
}
else if (val > node->val)
{
node->rightchild = Delete_help(node->rightchild, val);
}
else // 3. 找到要删除的节点
{
// 情况1:节点有两个孩子
if (node->leftchild != nullptr && node->rightchild != nullptr)
{
// 找到右子树的最小节点(中序后继)
Avlnode* successor = node->rightchild;
while (successor->leftchild != nullptr)
{
successor = successor->leftchild;
}
// 用后继节点的值替换当前节点的值
node->val = successor->val;
// 删除后继节点
node->rightchild = Delete_help(node->rightchild, successor->val);
}
// 情况2:节点有0个或1个孩子
else
{
Avlnode* child = node->leftchild != nullptr ? node->leftchild : node->rightchild;
free(node); // 释放当前节点内存
return child; // 返回孩子节点,让父节点指向它
}
}
// 4. 回溯时更新当前节点的高度
Updata_Height(node);
// 5. 检查并修复失衡
return Rotate(node);
}
// 14. 删除节点(对外接口)
bool Delete(AvlTree* pTree, int val)
{
assert(pTree != nullptr);
pTree->root = Delete_help(pTree->root, val);
return true;
}
删除操作注意点:
- 删除有两个孩子的节点时,我们选择用 ** 右子树的最小节点(中序后继)** 来替换它,这样可以保持二叉搜索树的性质
- 删除操作比插入操作更复杂,因为删除可能导致祖先节点失衡,需要从删除位置向上回溯检查所有节点
3.7 查找、判空与遍历
cpp
// 15. 判断AVL树是否为空
bool IsEmpty(AvlTree* pTree)
{
assert(pTree != nullptr);
return pTree->root == nullptr;
}
// 16. 查找节点
Avlnode* Search_AVLTree(AvlTree* pTree, int val)
{
if (IsEmpty(pTree))
return nullptr;
Avlnode* p = pTree->root;
while (p != nullptr)
{
if (p->val == val)
{
return p; // 找到节点
}
else if (val > p->val)
{
p = p->rightchild; // 去右子树查找
}
else
{
p = p->leftchild; // 去左子树查找
}
}
return nullptr; // 未找到
}
// 17. 中序遍历打印AVL树(非递归实现)
void Print_AvlTree(AvlTree* pTree)
{
if (IsEmpty(pTree))
{
printf("AVL树为空!\n");
return;
}
stack<Avlnode*> s;
Avlnode* cur = pTree->root;
while (cur != nullptr || !s.empty())
{
// 一直向左走,将所有左节点入栈
while (cur != nullptr)
{
s.push(cur);
cur = cur->leftchild;
}
// 弹出栈顶节点并访问
cur = s.top();
s.pop();
cout << cur->val << " ";
// 处理右子树
cur = cur->rightchild;
}
cout << endl;
}
注意:原代码中的中序遍历实现有严重 bug,会导致死循环和遍历不完整。这里重写了标准的非递归中序遍历实现。
四、测试代码与运行结果
cpp
int main()
{
AvlTree tree;
Init_AVLTree(&tree);
// 测试插入操作(插入有序序列,验证AVL树的自平衡能力)
printf("插入10,20,30,40,50后,中序遍历结果:\n");
Insert(&tree, 10);
Insert(&tree, 20);
Insert(&tree, 30);
Insert(&tree, 40);
Insert(&tree, 50);
Print_AvlTree(&tree); // 预期输出:10 20 30 40 50
// 测试删除操作
printf("\n删除40后,中序遍历结果:\n");
Delete(&tree, 40);
Print_AvlTree(&tree); // 预期输出:10 20 30 50
printf("\n删除20后,中序遍历结果:\n");
Delete(&tree, 20);
Print_AvlTree(&tree); // 预期输出:10 30 50
// 测试查找操作
Avlnode* node = Search_AVLTree(&tree, 30);
if (node != nullptr)
{
printf("\n找到节点30,其高度为:%d\n", node->height);
}
else
{
printf("\n未找到节点30\n");
}
// 测试销毁操作
Destory_AVLTree(&tree);
printf("\n销毁AVL树后,树是否为空:%s\n", IsEmpty(&tree) ? "是" : "否");
return 0;
}
运行结果:
插入10,20,30,40,50后,中序遍历结果:
10 20 30 40 50
删除40后,中序遍历结果:
10 20 30 50
删除20后,中序遍历结果:
10 30 50
找到节点30,其高度为:1
销毁AVL树后,树是否为空:是