1. AVL的概念
- AVL 树的定义:AVL 树是最先发明的自平衡二叉查找树。它要么是一棵空树,要么是具备下列性质的二叉搜索树:它的左右子树都是 AVL 树,且左右子树的高度差的绝对值不超过 1 。换句话说,AVL 树是一棵高度平衡的搜索二叉树,它通过严格控制每棵子树的高度差来控制整棵树的平衡,从而避免普通二叉搜索树在有序插入时退化成单支链表的问题。
- 平衡因子(balance factor) :为了更方便地观察和控制树是否平衡,这里引入平衡因子的概念。每个结点都有一个平衡因子,任何结点的平衡因子等于右子树的高度减去左子树的高度。由 AVL 树的性质可知,任何结点的平衡因子只可能是 0、1、-1 这三个值。需要强调的是,AVL 树并不是必须要存储平衡因子,平衡因子只是一种辅助手段,有了它可以更直观、更高效地判断某棵子树是否失衡,以及失衡的方向。
- 效率分析:AVL 树整体的结点数量和分布与完全二叉树类似,因此树的高度可以控制在 logN 级别。高度被控制住了,增、删、查、改这些操作的时间复杂度自然也就能稳定在 O(logN),这正是 AVL 树相对于普通二叉搜索树的核心优势所在。
2.AVL树的实现
2.1AVL树的结构
本节先给出 AVL 树的结点定义与整体框架,这是后续所有插入、旋转、查找操作的基础。重点有两个:
- 三叉链结构 :
AVLTreeNode中除了_left、_right两个左右孩子指针外,还额外保存了_parent父指针。之所以需要父指针,是因为插入新结点后要沿着父指针一路向上回溯,更新祖先结点的平衡因子,并在失衡时定位到需要旋转的子树根。没有父指针,向上回溯将非常困难。 - 平衡因子
_bf:每个结点用一个int记录平衡因子,初始值为 0,表示左右子树等高。它让"是否失衡、往哪边失衡"的判断变成一次简单的整数比较,是 AVL 树高效维护平衡的关键。
AVLTree 类则用 _root 保存整棵树的根,对外只暴露增删查改接口,内部细节全部封装。
cpp
template<class K, class V>
struct AVLTreeNode
{
//
需要
parent
指针,后续更新平衡因⼦可以看到
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)
{}
};
template<class K, class V>
class AVLTree
{
typedef AVLTreeNode<K, V> Node;
public:
//...
private:
Node* _root = nullptr;
};
2.2 AVL树的插⼊
2.2.1 AVL树插⼊⼀个值的⼤概过程
- 按二叉搜索树规则插入:先像普通二叉搜索树一样,从根结点开始比较 key 值,一路向下找到合适的空位置,把新结点挂上去。
- 向上更新平衡因子:新增结点以后,只会影响它祖先结点的高度,也就是可能会影响部分祖先结点的平衡因子。因此需要从新增结点出发,沿着到根结点的路径自底向上更新平衡因子。实际中最坏情况下要一直更新到根,但有些情况更新到中间就可以提前停止,具体情况在下一小节详细分析。
- 更新过程无异常则插入结束:如果在更新平衡因子的过程中,所有祖先结点都仍然满足平衡条件,说明这次插入没有破坏 AVL 树的性质,插入直接结束。
- 出现不平衡则旋转:如果更新平衡因子的过程中出现了不平衡(平衡因子变成 2 或 -2),就对这棵不平衡的子树进行旋转。旋转在调整平衡的同时,本质上还把这棵子树的高度降回了插入之前的高度,因此不会再影响上一层结点,旋转完成后插入即可结束。
2.2.2 平衡因⼦更新
更新原则:
-
平衡因子的定义:平衡因子 = 右子树高度 - 左子树高度,这是整个更新逻辑的基准。
-
只有子树高度变化才会影响平衡因子:如果某棵子树的高度没有变化,那么它父结点的平衡因子也不会改变,这是判断是否需要继续向上更新的根本依据。
-
插入方向决定加减:插入结点会增加所在子树的高度。所以新增结点在 parent 的右子树时,parent 的平衡因子 ++;新增结点在 parent 的左子树时,parent 的平衡因子 --。
-
高度是否变化决定是否继续 :parent 所在子树的高度是否发生变化,决定了是否需要继续向上更新祖先结点的平衡因子。
更新停⽌条件:
-
情况一:更新后 parent 的平衡因子等于 0。更新过程中 parent 的平衡因子变化为 -1 -> 0 或者 1 -> 0,说明更新前 parent 子树一边高一边低,而新增的结点恰好插入在低的那一边,插入后 parent 所在的子树两边变得一样高,高度没有变化,因此不会影响 parent 的父亲结点的平衡因子,更新到此结束。
-
情况二:更新后 parent 的平衡因子等于 1 或 -1。更新过程中 parent 的平衡因子变化为 0 -> 1 或者 0 -> -1,说明更新前 parent 子树两边一样高,新增结点插入后,parent 所在的子树变成一边高一边低。此时 parent 子树仍然符合平衡要求,但它的高度增加了 1,会影响 parent 的父亲结点的平衡因子,所以必须继续向上更新。
-
情况三:更新后 parent 的平衡因子等于 2 或 -2。更新过程中 parent 的平衡因子变化为 1 -> 2 或者 -1 -> -2,说明更新前 parent 子树就已经一边高一边低,而新增的结点又插在了高的那一边,使得高的那边更高,彻底破坏了平衡。此时 parent 所在的子树不符合平衡要求,必须进行旋转处理。旋转的目标有两个:第一,把 parent 子树旋转平衡;第二,降低 parent 子树的高度,使其恢复到插入结点以前的高度。正因为高度被恢复了,所以旋转后不需要继续往上更新,插入直接结束。
-
情况四:一路更新到根。如果不断向上更新,一直更新到根结点,此时根的平衡因子是 1 或 -1,也已经没有更上层的结点需要更新,更新同样停止。
2.2.3插⼊结点及更新平衡因⼦的代码实现
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;
}
}
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)
{
// 不平衡了,旋转处理
break;
}
else
{
assert(false);
}
}
return true;
}
2.3 旋转
2.3.1 旋转的原则
- 保持搜索树的规则:旋转只是调整结点的层次关系,不能破坏二叉搜索树中序遍历有序的性质,即左子树所有值 < 根 < 右子树所有值。
- 让旋转的树从不平衡变平衡,并降低旋转树的高度:旋转不仅要消除失衡,还要把这棵子树的高度降回插入之前的高度,从而保证旋转后不再影响上层结点。
按照失衡的方向和插入位置的不同,旋转总共分为四种:左单旋、右单旋、左右双旋、右左双旋。其中单旋用于处理"直线型"失衡,双旋用于处理"折线型"失衡。
2.3.2 右单旋
- 本图1展⽰的是10为根的树,有a/b/c抽象为三棵⾼度为h的⼦树(h>=0),a/b/c均符合AVL树的要求。10可能是整棵树的根,也可能是⼀个整棵树中局部的⼦树的根。这⾥a/b/c是⾼度为h的⼦树,是⼀种概括抽象表⽰,他代表了所有右单旋的场景,实际右单旋形态有很多种。
- 在a⼦树中插⼊⼀个新结点,导致a⼦树的⾼度从h变成h+1,不断向上更新平衡因⼦,导致10的平衡因⼦从-1变成-2,10为根的树左右⾼度差超过1,违反平衡规则。10为根的树左边太⾼了,需要往右边旋转,控制两棵树的平衡。
- 旋转核⼼步骤,因为5<b⼦树的值<10,将b变成10的左⼦树,10变成5的右⼦树,5变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的⾼度恢复到了插⼊之前的h+2,符合旋转原则。如果插⼊之前10整棵树的⼀个局部⼦树,旋转后不会再影响上⼀层,插⼊结束了。

右单旋的代码实现要点如下,理解这几步就掌握了单旋的通用套路:
- 先接好"内子树" :
subL是 parent 的左孩子,subLR是subL的右孩子。旋转后subLR要挂到 parent 的左边,所以先执行parent->_left = subLR,并注意subLR可能为空,必须判空后再改它的父指针。 - 再完成主体旋转 :让
subL->_right = parent、parent->_parent = subL,此时subL成为新的子树根。 - 最后处理上层链接 :用
parentParent暂存 parent 原来的父亲。如果parentParent为空,说明 parent 原本就是整棵树的根,需要把_root更新为subL;否则要判断 parent 原本是父结点的左孩子还是右孩子,把对应指针改指向subL,并维护好subL的父指针。 - 更新平衡因子 :旋转完成后,parent 与
subL的平衡因子都归零,因为它们各自的左右子树已经等高。
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;
// parent
有可能是整棵树的根,也可能是局部的⼦树
//
如果是整棵树的根,要修改
_root
//
如果是局部的指针要跟上⼀层链接
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;
}
2.3.4左单旋
- 本图展⽰的是10为根的树,有a/b/c抽象为三棵⾼度为h的⼦树(h>=0),a/b/c均符合AVL树的要求。10可能是整棵树的根,也可能是⼀个整棵树中局部的⼦树的根。
- 在a⼦树中插⼊⼀个新结点,导致a⼦树的⾼度从h变成h+1,不断向上更新平衡因⼦,导致10的平衡因⼦从1变成2,10为根的树左右⾼度差超过1,违反平衡规则。10为根的树右边太⾼了,需要往左边旋转,控制两棵树的平衡。
- 旋转核⼼步骤,因为10<b⼦树的值<15,将b变成10的右⼦树,10变成15的左⼦树,15变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的⾼度恢复到了插⼊之前的h+2,符合旋转原则。如果插⼊之前10整棵树的⼀个局部⼦树,旋转后不会再影响上⼀层,插⼊结束了。
2.3.5左单旋代码实现
左单旋与右单旋完全对称,只是方向相反,实现要点同样分四步:
- 接好内子树 :
subR是 parent 的右孩子,subRL是subR的左孩子。旋转后subRL挂到 parent 的右边,即parent->_right = subRL,同样要判空后再改父指针。 - 完成主体旋转 :
subR->_left = parent、parent->_parent = subR,subR成为新的子树根。 - 处理上层链接 :若
parentParent为空,则_root = subR;否则根据 parent 是左孩子还是右孩子,把父结点对应指针改指向subR,并维护subR的父指针。 - 更新平衡因子 :parent 与
subR的平衡因子都置 0。
对比右单旋可以看出,两者代码结构一一对应,只是把 _left 与 _right 互换,掌握一个即可推出另一个。
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;
}
2.3.6左右双旋
左边⾼时,如果插⼊位置不是在a⼦树,⽽是插⼊在b⼦树,b⼦树⾼度从h变成h+1,引发旋转,右单旋⽆法解决问题,右单旋后,我们的树依旧不平衡。右单旋解决的纯粹的左边⾼,但是插⼊在b⼦树中,10为跟的⼦树不再是单纯的左边⾼,对于10是左边⾼,但是对于5是右边⾼,需要⽤两次旋转才能解决,以5为旋转点进⾏⼀个左单旋,以10为旋转点进⾏⼀个右单旋,这棵树这棵树就平衡了。
2.3.7 左右双旋代码实现
左右双旋的代码实现有一个关键技巧:先记录 subLR 的平衡因子 bf,再执行两次单旋,最后根据 bf 的取值修正三个结点的平衡因子。
- 旋转动作本身很简单:先对
parent->_left做一次左单旋,再对parent做一次右单旋,即RotateL(parent->_left); RotateR(parent);。 - 难点在于旋转后平衡因子的修正,它取决于插入发生在
subLR的哪一侧:bf == 0:说明subLR就是新插入的结点,三个结点平衡因子全为 0。bf == -1:新结点插在subLR的左子树,旋转后parent->_bf = 1,其余为 0。bf == 1:新结点插在subLR的右子树,旋转后subL->_bf = -1,其余为 0。
- 最后的
assert(false)用于兜底,正常情况下bf只可能是 -1、0、1。
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);
}
}
2.3.8 右左双旋
右左双旋是左右双旋的镜像,思路完全一致:先记录 subRL 的平衡因子 bf,再对 parent->_right 做右单旋、对 parent 做左单旋,最后按 bf 修正平衡因子。
- 旋转动作:
RotateR(parent->_right); RotateL(parent);。 - 平衡因子修正按
bf分三种情况:bf == 0:subRL即新插入结点,三个结点平衡因子全为 0。bf == 1:新结点插在subRL的右子树,旋转后parent->_bf = -1,其余为 0。bf == -1:新结点插在subRL的左子树,旋转后subR->_bf = 1,其余为 0。
- 同样用
assert(false)兜底非法取值。
至此四种旋转全部介绍完毕:单旋处理直线型失衡,双旋处理折线型失衡,而双旋的平衡因子修正正是整个 AVL 插入中最容易出错、也最需要重点掌握的地方。
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);
}
}
2.4AVL树的查找
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;
}
2.5AVL树平衡检测
我们实现的AVL树是否合格,我们通过检查左右⼦树⾼度差的的程序进⾏反向验证,同时检查⼀下结点的平衡因⼦更新是否出现了问题。
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)
{
// 空树也是AVL树
if (nullptr == root)
return true;
// 计算pRoot结点的平衡因⼦:即pRoot左右⼦树的⾼度差
int leftHeight = _Height(root->_left);
int rightHeight = _Height(root->_right);
int diff = rightHeight - leftHeight;
// 如果计算出的平衡因⼦与pRoot的平衡因⼦不相等,或者
// pRoot平衡因⼦的绝对值超过1,则⼀定不是AVL树
if (abs(diff) >= 2)
{
cout << root->_kv.first << "⾼度差异常" << endl;
return false;
}
if (root->_bf != diff)
{
cout << root->_kv.first << "平衡因⼦异常" << endl;
return false;
}
// pRoot的左和右如果都是AVL树,则该树⼀定是AVL树
return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}