引言
二叉搜索树在数据有序插入时会退化为单支树,导致查找、插入、删除的时间复杂度从预期的 O(log N)恶化至 O(N)。为了解决这一问题,1962 年苏联科学家 G.M. Adelson-Velsky 和 E.M. Landis 在论文《An algorithm for the organization of information》中首次提出了自平衡二叉搜索树 ,后以其名字首字母命名为 AVL 树。
AVL 树通过引入平衡因子 (balance factor)来监控每个节点的左右子树高度差,并在插入或删除导致不平衡时通过旋转操作使树恢复平衡,从而保证所有操作的时间复杂度稳定在 O(\\log N)。本文将从 AVL 树的基本概念出发,详细讲解其节点结构、插入算法、平衡因子更新规则,以及四种旋转(左单旋、右单旋、左右双旋、右左双旋)的具体实现与平衡因子修正细节,最后给出平衡检测和查找的实现。
目录
[一、AVL 树的概念与性质](#一、AVL 树的概念与性质)
[1.1 定义](#1.1 定义)
[1.2 为什么不能要求高度差为 0?](#1.2 为什么不能要求高度差为 0?)
[1.3 AVL 树的性能](#1.3 AVL 树的性能)
[二、AVL 树的节点结构](#二、AVL 树的节点结构)
[三、AVL 树的插入](#三、AVL 树的插入)
[3.1 平衡因子的更新规则](#3.1 平衡因子的更新规则)
[3.2 插入代码框架](#3.2 插入代码框架)
[4.1 右单旋(RotateR)](#4.1 右单旋(RotateR))
[4.2 左单旋(RotateL)](#4.2 左单旋(RotateL))
[4.3 左右双旋(RotateLR)](#4.3 左右双旋(RotateLR))
[4.4 右左双旋(RotateRL)](#4.4 右左双旋(RotateRL))
[五、AVL 树的查找](#五、AVL 树的查找)
[六、AVL 树的平衡检测](#六、AVL 树的平衡检测)
一、AVL 树的概念与性质
1.1 定义
一棵 AVL 树要么是空树,要么满足以下两个条件:
-
其左子树和右子树都是 AVL 树;
-
左子树和右子树的高度之差的绝对值不超过 1。
这里的高度差通常记为 平衡因子(balance factor):
bf=右子树高度−左子树高度bf=右子树高度−左子树高度
因此,任意节点的平衡因子只能取 -1、0 或 1。
1.2 为什么不能要求高度差为 0?
高度差为 0(即完全平衡)是一种理想状态,但在某些节点数量下无法实现。例如:
-
树中只有 2 个节点时,必然一个为根、另一个为左或右孩子,高度差为 1;
-
树中有 4 个节点时,无论如何构造,高度差至少为 1。
因此 AVL 树退而求其次,允许高度差为 1,仍能保证树的高度接近 \\lceil \\log_2(N+1) \\rceil,从而获得对数级的操作效率。
1.3 AVL 树的性能
AVL 树是高度平衡的二叉搜索树,其高度严格控制在 O(\\log N),因此增删查改的时间复杂度均为 O(\\log N),相比普通二叉搜索树的最坏 O(N) 有了本质提升。
二、AVL 树的节点结构
为了实现平衡因子的更新和旋转操作,每个节点需要存储指向父节点的指针。节点中同时存储键值对(pair<K, V>)以及左右孩子指针和平衡因子。
cpp
template<class K, class V>
struct AVLTreeNode {
pair<K, V> _kv;
AVLTreeNode<K, V>* _left;
AVLTreeNode<K, V>* _right;
AVLTreeNode<K, V>* _parent;
int _bf; // balance factor
AVLTreeNode(const pair<K, V>& kv)
: _kv(kv)
, _left(nullptr)
, _right(nullptr)
, _parent(nullptr)
, _bf(0)
{}
};
树的封装类 AVLTree 仅需维护一个根节点指针:
cpp
template<class K, class V>
class AVLTree {
typedef AVLTreeNode<K, V> Node;
public:
// 公有接口(插入、查找、平衡检测等)
private:
Node* _root = nullptr;
};
三、AVL 树的插入
AVL 树的插入分为三个步骤:
-
按照二叉搜索树的规则插入新节点;
-
更新从新节点到根节点路径上所有祖先的平衡因子;
-
若某祖先的平衡因子绝对值变为 2,则通过旋转使其恢复平衡,并结束更新(因为旋转后子树高度恢复原状,不再影响上层)。
3.1 平衡因子的更新规则
设新插入节点为 cur,其父节点为 parent。插入后:
-
若
cur是parent的左孩子,则parent->_bf--; -
若
cur是parent的右孩子,则parent->_bf++。
更新后检查 parent 的平衡因子:
-
parent->_bf == 0:说明更新前parent的平衡因子为 ±1(一边高),插入节点在较矮的一侧,使子树高度不变。停止向上更新。 -
parent->_bf == ±1:说明更新前parent的平衡因子为 0(左右等高),插入后一侧变高,子树高度增加 1,需要继续向上更新。 -
parent->_bf == ±2:说明更新前parent的平衡因子为 ±1,插入节点在已经较高的一侧,导致不平衡。需要进行旋转,旋转后子树高度恢复原状,停止向上更新。
3.2 插入代码框架
cpp
bool Insert(const pair<K, V>& kv) {
if (_root == nullptr) {
_root = new Node(kv);
return true;
}
Node* parent = nullptr;
Node* cur = _root;
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; // key 已存在
}
}
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)
RotateR(parent); // 右单旋
else if (parent->_bf == 2 && cur->_bf == 1)
RotateL(parent); // 左单旋
else if (parent->_bf == -2 && cur->_bf == 1)
RotateLR(parent); // 左右双旋
else if (parent->_bf == 2 && cur->_bf == -1)
RotateRL(parent); // 右左双旋
break;
} else {
assert(false); // 平衡因子异常
}
}
return true;
}
四、旋转操作
旋转的目标:
-
保持二叉搜索树的规则(左 < 根 < 右);
-
使不平衡的子树恢复平衡;
-
降低子树高度,使其恢复到插入前的高度。
4.1 右单旋(RotateR)
适用场景 :parent->_bf == -2 且 cur->_bf == -1,即左边过高且左孩子的左边也过高(纯左边高)。
抽象模型 :设 parent 为 10,其左孩子 subL 为 5,subL 的右子树为 subLR(高度为 h)。在 subL 的左子树(a)中插入新节点导致 a 高度变为 h+1,引发不平衡。
旋转步骤:
-
将
subLR变为parent的左孩子; -
将
parent变为subL的右孩子; -
subL成为新的子树根; -
更新父指针连接;
-
最后将
parent和subL的平衡因子设为 0。
代码实现:
cpp
void RotateR(Node* parent) {
Node* subL = parent->_left;
Node* subLR = subL->_right;
parent->_left = subLR;
if (subLR)
subLR->_parent = parent;
Node* parentParent = parent->_parent;
subL->_right = parent;
parent->_parent = subL;
if (parentParent == nullptr) {
_root = subL;
subL->_parent = nullptr;
} else {
if (parent == parentParent->_left)
parentParent->_left = subL;
else
parentParent->_right = subL;
subL->_parent = parentParent;
}
parent->_bf = subL->_bf = 0;
}
4.2 左单旋(RotateL)
适用场景 :parent->_bf == 2 且 cur->_bf == 1,即右边过高且右孩子的右边也过高(纯右边高)。
对称步骤:
-
将
subRL变为parent的右孩子; -
将
parent变为subR的左孩子; -
subR成为新根; -
平衡因子清零。
代码实现:
cpp
void RotateL(Node* parent) {
Node* subR = parent->_right;
Node* subRL = subR->_left;
parent->_right = subRL;
if (subRL)
subRL->_parent = parent;
Node* parentParent = parent->_parent;
subR->_left = parent;
parent->_parent = subR;
if (parentParent == nullptr) {
_root = subR;
subR->_parent = nullptr;
} else {
if (parent == parentParent->_left)
parentParent->_left = subR;
else
parentParent->_right = subR;
subR->_parent = parentParent;
}
parent->_bf = subR->_bf = 0;
}
4.3 左右双旋(RotateLR)
适用场景 :parent->_bf == -2 且 cur->_bf == 1,即左边过高,但左孩子的右边过高(非纯左边高)。此时单次右旋无法解决,需要先对左孩子左旋,再对 parent 右旋。
抽象分析 :设 parent = 10,subL = 5,subLR = 8(即 subL 的右孩子)。插入位置可能在 subLR 的左子树(e)或右子树(f),也可能 subLR 本身就是新增节点(h = 0)。三种情况下旋转后的平衡因子不同。
旋转步骤:
-
对
subL执行左单旋(RotateL(parent->_left)); -
对
parent执行右单旋(RotateR(parent)); -
根据
subLR原来的平衡因子(bf)调整subL、parent和subLR的平衡因子。
平衡因子修正规则 (记 bf = subLR->_bf):
-
若
bf == 0:所有节点平衡因子均为 0。 -
若
bf == -1:说明插入在 e 子树,旋转后parent->_bf = 1,subL->_bf = 0,subLR->_bf = 0。 -
若
bf == 1:说明插入在 f 子树,旋转后subL->_bf = -1,parent->_bf = 0,subLR->_bf = 0。
代码实现:
cpp
void RotateLR(Node* parent) {
Node* subL = parent->_left;
Node* subLR = subL->_right;
int bf = subLR->_bf;
RotateL(parent->_left);
RotateR(parent);
if (bf == 0) {
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 0;
} else if (bf == -1) {
subL->_bf = 0;
subLR->_bf = 0;
parent->_bf = 1;
} else if (bf == 1) {
subL->_bf = -1;
subLR->_bf = 0;
parent->_bf = 0;
} else {
assert(false);
}
}
4.4 右左双旋(RotateRL)
适用场景 :parent->_bf == 2 且 cur->_bf == -1,即右边过高,但右孩子的左边过高。
旋转步骤:
-
对
subR执行右单旋(RotateR(parent->_right)); -
对
parent执行左单旋(RotateL(parent)); -
根据
subRL原来的平衡因子调整。
平衡因子修正 (记 bf = subRL->_bf):
-
bf == 0:全为 0。 -
bf == 1:parent->_bf = -1,subR->_bf = 0,subRL->_bf = 0。 -
bf == -1:subR->_bf = 1,parent->_bf = 0,subRL->_bf = 0。
代码实现:
cpp
void RotateRL(Node* parent) {
Node* subR = parent->_right;
Node* subRL = subR->_left;
int bf = subRL->_bf;
RotateR(parent->_right);
RotateL(parent);
if (bf == 0) {
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = 0;
} else if (bf == 1) {
subR->_bf = 0;
subRL->_bf = 0;
parent->_bf = -1;
} else if (bf == -1) {
subR->_bf = 1;
subRL->_bf = 0;
parent->_bf = 0;
} else {
assert(false);
}
}
五、AVL 树的查找
查找逻辑与普通二叉搜索树完全相同,时间复杂度 O(\\log N)。
cpp
Node* Find(const K& key) {
Node* cur = _root;
while (cur) {
if (cur->_kv.first < key)
cur = cur->_right;
else if (cur->_kv.first > key)
cur = cur->_left;
else
return cur;
}
return nullptr;
}
六、AVL 树的平衡检测
为了验证实现的 AVL 树是否正确,需要编写递归函数计算每个节点的左右子树高度,并检查:
-
平衡因子绝对值是否小于 2;
-
节点存储的平衡因子是否等于实际右高减左高。
cpp
int _Height(Node* root) {
if (root == nullptr) return 0;
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}
bool _IsBalanceTree(Node* root) {
if (root == nullptr) return true;
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
int diff = rightHeight - leftHeight;
if (abs(diff) >= 2) {
cout << root->_kv.first << "高度差异常" << endl;
return false;
}
if (root->_bf != diff) {
cout << root->_kv.first << "平衡因子异常" << endl;
return false;
}
return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}
测试时可用随机数据大量插入,验证平衡性和高度是否接近 \\log N。
七、关于删除的说明
AVL 树的删除操作同样需要更新平衡因子并进行旋转,其复杂度与插入相当,但实现更为繁琐。由于篇幅和常见教学安排,本文未给出删除的实现,有兴趣的读者可参考经典教材如《殷人昆 数据结构:用面向对象方法与 C++ 语言描述》。
总结
AVL 树是历史上第一种自平衡二叉搜索树,通过在每个节点上维护平衡因子(左右子树高度差)并在插入导致不平衡时执行四种旋转(左单旋、右单旋、左右双旋、右左双旋),使树始终保持高度平衡。其插入、查找、删除的时间复杂度稳定为 O(\\log N),相比普通二叉搜索树具有决定性的性能优势。
理解 AVL 树的旋转机制和平衡因子更新规则,是掌握更复杂的平衡树(如红黑树)的基础。在实际工程中,C++ 标准库的 std::map 和 std::set 通常使用红黑树而非 AVL 树(因为红黑树插入删除旋转次数更少,常数更小),但 AVL 树在查找密集的场景下仍有一定优势。无论如何,AVL 树的设计思想------通过局部旋转保持全局平衡------是数据结构领域的重要里程碑。