文章目录
- C++ AVL 树:平衡因子更新逻辑与四种旋转的完整实现
- 一、AVL 树的核心概念
- 1.1 平衡因子:AVL 的灵魂
- 1.2 节点结构:为什么要三叉链
- 二、插入:分两步走
- 2.1 整体流程
- 2.2 平衡因子更新的三条规则
- 2.3 插入代码
- 三、旋转:四种情况的完整实现
- 3.1 旋转的两条原则
- 3.2 右单旋(RotateR):左左失衡
- 3.3 左单旋(RotateL):右右失衡
- 3.4 左右双旋(RotateLR):左孩子的右边插入了
- 3.5 右左双旋(RotateRL):右孩子的左边插入了
- 四、查找:和 BST 完全一样
- 五、验证:怎么确认你的 AVL 树是对的
- 六、总结
C++ AVL 树:平衡因子更新逻辑与四种旋转的完整实现
二叉搜索树(BST)有个致命的缺陷:插入顺序一旦是"有序的"(比如 1、2、3、4、5),树就退化成一根链表,查找复杂度从 O(logN) 崩到 O(N)。AVL 树就是为了解决这个问题诞生的——它是第一种自平衡二叉搜索树,通过严格控制"左右子树高度差不超过 1",保证任何情况下查找都稳定在 O(logN)。
这篇文章不废话,直接把 AVL 树的平衡因子更新逻辑、四种旋转(左单旋、右单旋、左右双旋、右左双旋)的完整代码和最容易出错的双旋平衡因子细节讲透。红黑树、B+ 树的旋转思路和它一脉相承,吃透 AVL 的旋转,后面全通。
一、AVL 树的核心概念
1.1 平衡因子:AVL 的灵魂
AVL 树同时满足两个条件:
- 是一棵合法的二叉搜索树(左小右大);
- 任意节点的左右子树高度差绝对值不超过 1。
为了量化"高度差",给每个节点引入平衡因子(balance factor,简称 bf):
平衡因子 = 右子树高度 - 左子树高度AVL 树铁律:所有节点的平衡因子只能是 -1、0、1。
- bf = 1:右子树偏高
- bf = 0:左右等高
- bf = -1:左子树偏高
只要某个节点的 |bf| 达到 2(即 2 或 -2),树就失衡了,必须通过旋转修复。
注意不同教材对平衡因子的定义可能相反(有的用"左-右",有的用"右-左"),本文统一用「右 - 左」,后面所有代码和讲解都基于这个约定,别混。
1.2 节点结构:为什么要三叉链
template<classK,classV>structAVLTreeNode{pair<K,V>_kv;AVLTreeNode<K,V>*_left;AVLTreeNode<K,V>*_right;AVLTreeNode<K,V>*_parent;// 关键:父指针int_bf;// 平衡因子AVLTreeNode(constpair<K,V>&kv):_kv(kv),_left(nullptr),_right(nullptr),_parent(nullptr),_bf(0){}};template<classK,classV>classAVLTree{typedefAVLTreeNode<K,V>Node;public:// ...private:Node*_root=nullptr;};和普通 BST 节点比,AVL 节点多了两个东西:
- _parent 父指针:插入后需要从新节点一路向上回溯更新祖先的平衡因子,没有父指针就回不去了。这是三叉链存在的唯一理由。
- _bf 平衡因子:记录当前节点左右子树高度差,判断是否需要旋转。
二、插入:分两步走
2.1 整体流程
AVL 插入分四步:
- 按二叉搜索树规则插入新节点;
- 从新节点一路向上更新祖先的平衡因子;
- 更新过程中若没出现失衡,插入结束;
- 若出现失衡(|bf| == 2),对失衡子树旋转,旋转会同时降低子树高度,不会再影响更上层,插入结束。
2.2 平衡因子更新的三条规则
这是 AVL 的核心逻辑,理解了它,旋转就顺理成章。插入一个节点后,它只会影响祖先节点的高度,所以从新节点开始向上更新平衡因子:
- 新节点在 parent 的右子树:parent 的右子树高度 +1,所以
parent->_bf++; - 新节点在 parent 的左子树:parent 的左子树高度 +1,所以
parent->_bf--。
更新后,根据 parent 的 bf 值,有三种走向:
| 更新后 bf | 更新前变化 | 含义 | 处理 |
|---|---|---|---|
| 0 | -1→0 或 1→0 | 原来一边高一边低,插到了低的一边,子树高度不变 | 停止更新 |
| 1 或 -1 | 0→1 或 0→-1 | 原来两边一样高,插入后一边高一,子树高度 +1 | 继续向上更新 |
| 2 或 -2 | 1→2 或 -1→-2 | 原来就一边高,又插到了高的那边,失衡了 | 旋转处理,然后停止 |
用大白话解释这三条:
- bf 变成 0:说明这颗子树"补平"了,整体高度没变,不会影响再往上的祖先,所以到此为止。
- bf 变成 ±1:这颗子树自己还平衡,但高度确实涨了 1,会波及上面的父节点,所以要继续往上更新。
- bf 变成 ±2:这颗子树已经不平衡了,必须旋转。旋转的本质是把这颗子树调平衡的同时把高度降回插入前,所以旋转后也不会影响更上层,到此为止。
2.3 插入代码
boolInsert(constpair<K,V>&kv){if(_root==nullptr){_root=newNode(kv);returntrue;}// 1. 按 BST 规则找到插入位置Node*parent=nullptr;Node*cur=_root;while(cur){if(cur->_kv.first<kv.first){parent=cur;cur=cur->_right;}elseif(cur->_kv.first>kv.first){parent=cur;cur=cur->_left;}else{returnfalse;// key 已存在,插入失败}}// 2. 链接新节点cur=newNode(kv);if(parent->_kv.first<kv.first)parent->_right=cur;elseparent->_left=cur;cur->_parent=parent;// 3. 向上更新平衡因子while(parent){// 先更新 parent 的平衡因子if(cur==parent->_left)parent->_bf--;// 新节点在左,左树变高,bf 减elseparent->_bf++;// 新节点在右,右树变高,bf 加if(parent->_bf==0){break;// 补平了,高度不变,停止}elseif(parent->_bf==1||parent->_bf==-1){cur=parent;// 继续往上更新parent=parent->_parent;}elseif(parent->_bf==2||parent->_bf==-2){// 失衡了,旋转处理break;}else{assert(false);// 理论上不可能走到这里}}returntrue;}注意最后那段旋转逻辑,我这里break占位了,真正的旋转要等到插入完成后根据失衡位置单独调用对应旋转函数。实际工程里会把旋转的调用合并进这个 while 循环,这里拆开是为了把"更新平衡因子"和"旋转"两件事讲清楚。
三、旋转:四种情况的完整实现
3.1 旋转的两条原则
所有旋转都遵循两条原则:
- 保持搜索树规则(左小右大不能破坏);
- 让旋转的树从失衡变平衡,同时降低高度。
旋转共四种:右单旋、左单旋、左右双旋、右左双旋。下面用抽象子树 a/b/c 表示高度为 h 的 AVL 子树(h >= 0),这样一套图就能覆盖所有情况。
3.2 右单旋(RotateR):左左失衡
触发场景:某个节点的左子树的左子树插入节点,导致该节点 bf 变成 -2。也就是"左边太高,且插在了左孩子的左边"。
核心步骤:因为5 < b子树的值 < 10,把 b 子树变成 10 的左子树,10 变成 5 的右子树,5 变成新的根。
voidRotateR(Node*parent){Node*subL=parent->_left;// 5Node*subLR=subL->_right;// b 子树// 把 b 子树挂到 parent 的左孩子parent->_left=subLR;if(subLR)subLR->_parent=parent;// 保存 parent 的父节点,因为旋转后要重新链接上层Node*parentParent=parent->_parent;// 让 subL 成为新根,parent 成为 subL 的右孩子subL->_right=parent;parent->_parent=subL;// parent 可能是整棵树的根,也可能是局部子树if(parentParent==nullptr){_root=subL;// 是根,更新 _rootsubL->_parent=nullptr;}else{// 是局部子树,把 subL 链到 parentParent 的正确位置if(parent==parentParent->_left)parentParent->_left=subL;elseparentParent->_right=subL;subL->_parent=parentParent;}// 旋转后 parent 和 subL 的平衡因子都归 0parent->_bf=subL->_bf=0;}最容易漏的点:旋转不只是改两个节点的指向,还要:
- 处理
subLR(可能为空的中间子树)的挂载; - 判断 parent 是根还是局部子树,分别更新
_root或父节点的孩子指针; - 旋转完更新平衡因子。
漏掉任何一个,树就断了或 bf 就错了。这是手写 AVL 时 bug 的重灾区。
3.3 左单旋(RotateL):右右失衡
和右单旋完全镜像。触发场景:右子树的右子树插入节点,bf 变成 2。因为10 < b子树的值 < 15,把 b 变成 10 的右子树,10 变成 15 的左子树,15 成为新根。
voidRotateL(Node*parent){Node*subR=parent->_right;// 15Node*subRL=subR->_left;// b 子树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;elseparentParent->_right=subR;subR->_parent=parentParent;}parent->_bf=subR->_bf=0;}左单旋和右单旋完全对称,理解了右单旋,左单旋就是把 left/right 全部对调。
3.4 左右双旋(RotateLR):左孩子的右边插入了
触发场景:左边高,但插入位置不在左孩子的左子树(a),而在左孩子的右子树(b)。这种情况下单纯右单旋解决不了问题——因为对 10 来说是左边高,但对 5 来说却是右边高,是"拐弯"的失衡,需要两次旋转:先以 5 为旋转点做左单旋,再以 10 为旋转点做右单旋。
这里是最容易出错的地方:双旋后三个节点的平衡因子不是简单归零,要根据插入的具体位置分三种情况。我们把 b 子树进一步展开——b 的根是 8,它有两个高度为 h-1 的子树 e 和 f。新节点插在 e、插在 f、还是 b 本身就是新节点,旋转后的 bf 完全不同:
| 场景 | 条件 | 8 的 bf | 旋转后 5 | 旋转后 8 | 旋转后 10 |
|---|---|---|---|---|---|
| 场景1 | h>=1,插在 e 子树 | -1 | 0 | 0 | 1 |
| 场景2 | h>=1,插在 f 子树 | 1 | -1 | 0 | 0 |
| 场景3 | h==0,b 就是新节点 | 0 | 0 | 0 | 0 |
为什么不同?因为双旋的本质是:第一次左旋把 8 提上来,8 的左子树(e)挂到 5 的右孩子;第二次右旋把 8 提到最顶,8 的右子树(f)挂到 10 的左孩子。所以插入位置在 e 还是 f,直接决定了 e 归 5 还是 f 归 10,进而决定了 5 和 10 谁变高。这就是三种场景的根源。
代码实现的关键:先记录 subLR 的 bf,再旋转(旋转会改 bf),最后根据记录的 bf 还原三个节点的平衡因子:
voidRotateLR(Node*parent){Node*subL=parent->_left;// 5Node*subLR=subL->_right;// 8intbf=subLR->_bf;// 先记录 8 的平衡因子RotateL(parent->_left);// 先对 5 左单旋RotateR(parent);// 再对 10 右单旋// 根据记录的 bf 还原三个节点的平衡因子if(bf==0)// 场景3:b 就是新节点{subL->_bf=0;subLR->_bf=0;parent->_bf=0;}elseif(bf==-1)// 场景1:插在 e 子树{subL->_bf=0;subLR->_bf=0;parent->_bf=1;}elseif(bf==1)// 场景2:插在 f 子树{subL->_bf=-1;subLR->_bf=0;parent->_bf=0;}else{assert(false);}}这是整个 AVL 实现里最坑的一处:很多人旋转完直接给三个节点 bf 都归零,结果在某些插入序列下 bf 算错,导致后续插入时旋转判断失灵,最终树结构错误却查不出来。必须先存 bf、旋转、再按 bf 分支还原,这个顺序不能乱。
3.5 右左双旋(RotateRL):右孩子的左边插入了
和左右双旋完全镜像。触发场景:右边高,但插入位置在右孩子的左子树。先以右孩子为旋转点做右单旋,再以 parent 做左单旋。
同样,把 b 子树展开,b 的根是 12,e/f 是两个高度 h-1 的子树,也分三种场景:
| 场景 | 条件 | 12 的 bf | 旋转后 10 | 旋转后 12 | 旋转后 15 |
|---|---|---|---|---|---|
| 场景1 | h>=1,插在 e 子树 | -1 | 0 | 0 | 1 |
| 场景2 | h>=1,插在 f 子树 | 1 | -1 | 0 | 0 |
| 场景3 | h==0,b 就是新节点 | 0 | 0 | 0 | 0 |
voidRotateRL(Node*parent){Node*subR=parent->_right;// 15Node*subRL=subR->_left;// 12intbf=subRL->_bf;// 先记录 12 的平衡因子RotateR(parent->_right);// 先对 15 右单旋RotateL(parent);// 再对 10 左单旋if(bf==0){subR->_bf=0;subRL->_bf=0;parent->_bf=0;}elseif(bf==1)// 插在 f 子树{subR->_bf=0;subRL->_bf=0;parent->_bf=-1;}elseif(bf==-1)// 插在 e 子树{subR->_bf=1;subRL->_bf=0;parent->_bf=0;}else{assert(false);}}注意右左双旋的 bf 分支和左右双旋是镜像的,+1和-1的位置对调了,别照抄左右双旋的条件。
四、查找:和 BST 完全一样
Node*Find(constK&key){Node*cur=_root;while(cur){if(cur->_kv.first<key)cur=cur->_right;elseif(cur->_kv.first>key)cur=cur->_left;elsereturncur;}returnnullptr;}AVL 树的查找就是标准的二叉搜索树查找,因为树始终平衡,复杂度稳定 O(logN)。
五、验证:怎么确认你的 AVL 树是对的
手写 AVL 最容易出的问题就是"看着能跑,但 bf 其实算错了"。光靠肉眼看不出来,必须写个校验函数反向验证:
int_Height(Node*root){if(root==nullptr)return0;intleftHeight=_Height(root->_left);intrightHeight=_Height(root->_right);returnleftHeight>rightHeight?leftHeight+1:rightHeight+1;}bool_IsBalanceTree(Node*root){if(nullptr==root)returntrue;// 重新计算左右子树高度差,和节点存储的 bf 对比intleftHeight=_Height(root->_left);intrightHeight=_Height(root->_right);intdiff=rightHeight-leftHeight;if(abs(diff)>=2){cout<<root->_kv.first<<"高度差异常"<<endl;returnfalse;}if(root->_bf!=diff){cout<<root->_kv.first<<"平衡因子异常"<<endl;returnfalse;}return_IsBalanceTree(root->_left)&&_IsBalanceTree(root->_right);}核心思路:不信任节点里存的 bf,而是现场重新算一遍真实的高度差,和存的 bf 对比。如果两者不一致,说明你更新平衡因子的逻辑有 bug。这是发现 bf 错误的唯一可靠手段。
测试时插入一堆随机值,再调用校验函数:
voidTestAVLTree(){constintN=100000;vector<int>v;v.reserve(N);srand(time(0));for(size_t i=0;i<N;i++)v.push_back(rand()+i);AVLTree<int,int>t;for(autoe:v)t.Insert(make_pair(e,e));cout<<"是否平衡:"<<t.IsBalanceTree()<<endl;// 应为 1cout<<"树高:"<<t.Height()<<endl;// 10万节点约 17 层,验证 O(logN)}10 万个随机节点的 AVL 树高度约 17 层(log2(100000) ≈ 17),如果你测出来的高度是几十上百,说明旋转没生效、树退化了,立刻检查旋转代码。
六、总结
- AVL 用平衡因子(右-左)严格控制高度差 ≤ 1,换来 O(logN) 的稳定查找,代价是插入删除时要频繁旋转。
- 平衡因子更新三条规则:bf 变 0 停止、变 ±1 继续向上、变 ±2 旋转后停止。
- 四种旋转:LL 右单旋、RR 左单旋、LR 左右双旋、RL 右左双旋,单旋 bf 归零,双旋必须按插入位置分三种场景还原 bf——这是 AVL 最核心也最易错的点。
- 旋转时别忘了处理中间子树挂载、根/局部子树判断、bf 更新三件事。
- 写完必须写校验函数,插入随机值反向验证 bf 是否正确,这是发现隐藏 bug 的唯一手段。
AVL 的旋转思路是后续红黑树、B+ 树的基础,把这四种旋转和 bf 更新逻辑吃透,后面学红黑树只需要理解"颜色约束替代了高度约束"这个思想转变,旋转本身你已经会了。