目录
前情提要
上一篇我们学习的二叉搜索树是一种常用的高效查找数据结构,其增删查改操作的时间复杂度可达到O(logn),但是如果插入的节点呈现有序状态(如1、2、3、4、5),普通BST就会退化为单链表,此时所有操作的时间复杂度会骤降为O(n),失去其优势。于是在这种情况下,AVL树出现了。
AVL树在插入和删除节点后,自动调整树的结构,保证任意节点的左右子树高度差不超过1,从而稳定维持O(logn)的时间复杂度。这篇文章我们一同开始迭代实现AVL树
一、AVL基础概念
1、什么是AVL树
AVL树本质上是一种「自平衡二叉搜索树」,它继承了二叉搜索树的所有性质,同时增加了一个关键约束:任意节点的左右子树高度差(平衡因子)的绝对值不超过1 。自平衡的核心:插入或删除节点后,树的结构可能会破坏平衡因子绝对值≤1的约束,此时AVL树会通过「旋转」操作,调整节点的父子关系,恢复树的平衡
2、平衡因子(BF)的定义
AVL树的核心约束:任意节点的平衡因子只能是 -1、0、1 中的一个。
当节点的平衡因子为 2 或 -2 时,说明该节点所在的子树已经失衡,需要通过旋转操作恢复平衡;若平衡因子为3、-3或其他值,则说明代码存在了明显逻辑错误
二、AVL的节点结构与类框架
1、节点结构体
cpp
#pragma once
#include<iostream>
#include<cassert>
using namespace std;
template<class K, class V>
struct AVLTreeNode
{
pair<K, V> _kv; // 存储键值对(和STL map一致,键K唯一,通过K查找V)
AVLTreeNode<K, V>* _left; // 左孩子指针
AVLTreeNode<K, V>* _right; // 右孩子指针
AVLTreeNode<K, V>* _parent;// 父节点指针
int _bf;
AVLTreeNode(const pair<K, V>& kv)
:_kv(kv) // 初始化键值对
,_left(nullptr) // 左孩子指针置空
,_right(nullptr) // 右孩子指针置空
,_parent(nullptr)// 父节点指针置空
,_bf(0) // 平衡因子初始化为0
{}
};
AVLTreeNode<K, V>* _parent:父节点指针,迭代版AVL树的核心成员,也是和递归版的核心区别。插入节点后,通过这个指针可以一步步向上找到所有祖先节点,从而更新平衡因子;旋转操作时,也需要通过父节点指针调整上层节点的链接关系。
二、类框架
cpp
template<class K, class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
bool Insert(const pair<K, V>& kv); // 插入键值对
void InOrder(); // 中序遍历
private:
Node* _root = nullptr;
void RotateL(Node* parent);
void RotateR(Node* parent);
void FixBF(Node* P, Node* C, Node* G);
};
三、AVL树的插入
插入操作是AVL树最核心的操作,也是理解平衡因子更新和旋转操作的基础。迭代版AVL树的插入操作分为5个步骤,全程用while循环实现
先按二叉搜索树(BST)的规则找到插入位置,创建新节点并建立双向链接(孩子→父、父→孩子),再通过父指针向上回溯更新平衡因子,判断是否失衡,若失衡则通过旋转恢复平衡,最后返回插入结果。
cpp
template<class K, class V>
bool AVLTree<K, V>::Insert(const pair<K, V>& kv)
{
if (_root == nullptr)
{
_root = new Node(kv);
return true;
}
// 遍历找插入位置
Node* cur = _root;
Node* parent = nullptr;
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)
{
if (parent->_bf == 2 && cur->_bf == 1)
{
RotateL(parent); // 调用左旋函数,修复RR失衡
}
else if (parent->_bf == -2 && cur->_bf == -1)
{
RotateR(parent);
}
else if (parent->_bf == 2 && cur->_bf == -1)
{
Node* grandSon = cur->_left; // 记录cur的左孩子(新根节点G)
int bf = grandSon->_bf; // 保存G的bf,用于后续更新
RotateR(cur); // 先对cur做右旋,将RL失衡转化为RR失衡
RotateL(parent); // 再对parent做左旋,修复RR失衡
FixBF(parent, cur, grandSon); // 调用FixBF,更新三个节点的bf
}
else if (parent->_bf == -2 && cur->_bf == 1)
{
Node* grandSon = cur->_right; // 记录cur的右孩子(新根节点G)
int bf = grandSon->_bf; // 保存G的bf,用于后续更新
RotateL(cur); // 先对cur做左旋,将LR失衡转化为LL失衡
RotateR(parent); // 再对parent做右旋,修复LL失衡
FixBF(parent, cur, grandSon); // 调用FixBF,更新三个节点的bf
}
break;
}
else // 逻辑错误(如平衡因子更新遗漏),断言终止程序
{
assert(false);
}
}
return true;
}
1:空树处理(插入第一个节点)
当根节点_root为nullptr时,说明当前是一棵空树,插入的第一个节点直接作为根节点,无需更新平衡因子(根节点无父节点,没有祖先节点需要回溯),插入成功后直接返回true。
2:非空树,查找插入位置(BST规则)
非空树时,需要按照二叉搜索树的 左小右大 规则,迭代查找插入位置(插入位置一定是叶子节点的空孩子处),这一步的核心是「找到插入节点的父节点parent」,因为新节点需要挂在parent的左/右孩子位置
关键变量说明:
cur:从根节点开始遍历,用于寻找插入位置,初始值为_root;
parent:用于记录cur的父节点,初始值为nullptr,cur移动时,parent同步更新,最终parent就是插入节点的父节点
3:创建新节点,建立双向链接
找到插入位置后,创建新节点(传入插入的键值对),然后将新节点挂在parent的左/右孩子位置,同时设置新节点的父指针指向parent,建立双向链接 ------这是迭代版AVL树的关键,后续回溯更新平衡因子、旋转操作都依赖这个双向链接,若只建立单向链接(如只设置parent->_right=cur,不设置cur->_parent=parent),会导致后续无法通过cur找到parent,回溯失败
cur = new Node(kv):创建新节点,此时cur指向新节点,新节点的左右孩子、父节点都已通过构造函数置为nullptr,平衡因子为0;
判断parent的键与插入键的大小:
若parent->_kv.first < kv.first:新节点是parent的右孩子,设置parent->_right=cur;
若parent->_kv.first > kv.first:新节点是parent的左孩子,设置parent->_left=cur;
- cur->_parent = parent:新节点的父指针指向parent,完成双向链接。
4:向上回溯更新平衡因子(核心中的核心)
新节点插入后,会导致其所有祖先节点的子树高度发生变化,进而影响祖先节点的平衡因子。这一步通过while(parent)循环,从新节点的父节点(parent)开始,一步步向上回溯,依次更新每个祖先节点的平衡因子,直到出现以下三种情况之一,循环终止:
某个节点的平衡因子变为0(子树高度不变,上层祖先无需更新);
某个节点的平衡因子变为2或-2(子树失衡,需要旋转修复,旋转后终止);
到达根节点(parent为nullptr,循环终止)。
四、LR失衡平衡因子更新规则
FixBF函数的参数的是P(原失衡节点)、C(原子节点)、G(新根节点),更新规则如下
cpp
// 处理LR/RL失衡后的bf赋值,P=原失衡节点,C=原子节点,G=新根
template<class K, class V>
void AVLTree<K, V>::FixBF(Node* P, Node* C, Node* G)
{
if (G->_bf == 1) // 场景1:插入节点在G的右子树
{
P->_bf = -1;
C->_bf = 0;
}
else if (G->_bf == -1) // 场景2:插入节点在G的左子树
{
P->_bf = 0;
C->_bf = 1;
}
else // 场景3:G本身就是插入节点
{
P->_bf = 0;
C->_bf = 0;
}
G->_bf = 0; // 新根G的bf永远为0
}
五、AVL树中序遍历
前文已完成AVL树的核心实现为了验证代码的正确性,我们补充中序遍历函数的完整解析,并编写测试用例,覆盖四种失衡场景
cpp
template<class K, class V>
void AVLTree<K, V>::InOrder()
{
if (_root == nullptr)
{
cout << "树为空!" << endl;
return;
}
Node* cur = _root;
while (cur != nullptr)
{
while (cur->_left != nullptr)
{
cur = cur->_left;
}
cout << "key:" << cur->_kv.first << " val:" << cur->_kv.second << " ";
while (cur != nullptr && cur->_right == nullptr)
{
Node* prev = cur;
cur = cur->_parent;
if (cur != nullptr && cur->_right == prev)
{
break;
}
}
if (cur != nullptr)
{
cur = cur->_right;
}
}
cout << endl;
}
迭代版中序遍历的核心是找最左节点→打印→回溯→访问右子树,循环往复,直到cur为nullptr,全程通过父指针回溯,无需额外栈空间,逻辑与插入操作的回溯思想一致。
找最左节点:中序遍历先访问左子树,因此每次都先移动到当前子树的最左节点(无左孩子为止);
打印节点:访问根节点,打印键值对,验证是否按升序排列;
回溯:如果当前节点无右孩子,回溯到父节点,直到找到有右孩子的节点(避免重复打印已访问的节点);
访问右子树:移动到右孩子,重复上述步骤,完成右子树的遍历。