平衡二叉树介绍
平衡二叉搜索树(AVL)树是一种自平衡的二叉搜索树,由两位前苏联数学家G.M. Adelson-Velsky和E.M. Landis于1962年发明,其名称即取自他们姓氏的首字母。它通过在每次插入或删除节点后,自动调整树的结构,来保证其始终处于一种"高度平衡"的状态,从而确保了高效的查找性能。
平衡二叉树概念和特性
1.什么是"平衡"?
AVL树的核心是引入了 平衡因子(Balance Factor, BF) 的概念。一个节点的平衡因子定义为其左子树的高度减去右子树的高度(有时定义相反,但原理相同)。
在AVL树中,每个节点的平衡因子只能是 -1、0 或 1。
这意味着任意节点的左右子树高度差绝对值不超过1。这确保了树形结构是平衡的,避免了退化成链表。
2.为什么需要平衡?
普通的二叉搜索树(BST)在极端情况下(例如插入有序数据)会退化成类似链表的结构,导致其操作时间复杂度从理想的 O(log n) 恶化到 O(n)。
AVL树通过维持平衡,保证了其查找、插入和删除操作的时间复杂度在平均和最坏情况下均为 O(log n),其中 n 是树中节点的数量。一个有 n 个节点的AVL树,其最大深度约为 1.44 * log₂(n)。
平衡二叉树核心操作
当插入或删除节点后,某些节点的平衡因子可能会变成 +2 或 -2,此时树就"失衡"了。为了恢复平衡,AVL树会执行一种名为"旋转"的局部调整操作。
旋转
右旋(Right Rotation)------ 解决"左左(LL)"失衡
场景:节点 A 失衡(平衡因子为 +2),且其左子节点 B 偏重(平衡因子为 0 或 +1)。我们需要将 A 向右旋转,让 B 成为新的根。
旋转前的结构(注意 B 的右子树 T2):
A
/ \
B T3 <- T3 是 A 的右子树,不受影响
/ \
T1 T2 <- T2 是 B 的右子树,它需要"过继"
核心操作(三步走):
1.暂存左子:B = A.left
2.过继子树:将 B 的右子树 T2 挂给 A 作为新的左子树 → A.left = B.right
3.完成旋转:将 A 挂给 B 作为新的右子树 → B.right = A
旋转后的结构:
B
/ \
T1 A
/ \
T2 T3
验证(中序遍历):
旋转前顺序:T1 -> B -> T2 -> A -> T3
旋转后顺序:T1 -> B -> T2 -> A -> T3(完全一致,证明了旋转不破坏二叉搜索树的性质)
左旋(Left Rotation)------ 解决"右右(RR)"失衡
场景:节点 A 失衡(平衡因子为 -2),且其右子节点 B 偏重。这是右旋的完美镜像。
旋转前的结构(注意 B 的左子树 T2 需要"过继"):
A
/ \
T1 B <- T1 是 A 的左子树,不受影响
/ \
T2 T3 <- T2 是 B 的左子树,它需要"过继"
核心操作(三步走):
1.暂存右子:B = A.right
2.过继子树:将 B 的左子树 T2 挂给 A 作为新的右子树 → A.right = B.left
3.完成旋转:将 A 挂给 B 作为新的左子树 → B.left = A
旋转后的结构:
B
/ \
A T3
/ \
T1 T2
验证(中序遍历):
旋转前顺序:T1 -> A -> T2 -> B -> T3
旋转后顺序:T1 -> A -> T2 -> B -> T3(完全一致)
记忆口诀(帮你快速区分)
右旋:新根是 左孩子。把左孩子的右子树(T2)过继给旧根当左子树。
左旋:新根是 右孩子。把右孩子的左子树(T2)过继给旧根当右子树。
一句话总结:旋转就是"爷爷下来当孙子,孙子上去当爷爷",关键是那棵被"过继"的子树,它永远跟随新根原来的方向(右旋时 T2 是左孩子的右子树,所以过继后挂在右边;左旋时 T2 是右孩子的左子树,过继后挂在左边)。
源码
cpp
/*
左旋
过继子树:将q的左子树挂给p作为新的右子树 → p.right = q.left
完成旋转:将p挂给q作为新的左子树 → q.left = p
*/
void AVLTree::levorotation(Node * p, Node* q, Node* s)
{
//左旋操作
q->parent = p->parent;
if (p->parent && p->isLeftTree)
{
p->parent->left = q;
q->isLeftTree = true;
}
else if(p->parent)
{
p->parent->right = q;
q->isLeftTree = false;
}
//将p挂到q的左子树
q->parent = p;
p->isLeftTree = true;
p->right = q->left;
q->left = p;
}
/*
右旋
过继子树:将 q 的右子树挂给 p 作为新的左子树 → p.left = q.right
完成旋转:将 p 挂给 q 作为新的右子树 → q.right = p
*/
void AVLTree::rightHandedRotation(Node * p, Node * q, Node * s)
{
//右旋
q->parent = p->parent;
if (p->parent && p->isLeftTree)
{
p->parent->left = q;
q->isLeftTree = true;
}
else if(p->parent)
{
p->parent->right = q;
q->isLeftTree = false;
}
//将p挂到q的右子树
q->parent = p;
q->isLeftTree = false;
p->left = q->right;
q->right = p;
}
理解了这两个基础操作,AVL树的LR和RL双旋就很容易了------它们只是连续调用两次单旋(例如LR = 先对左孩子左旋,再对自身右旋)。
根据失衡节点的位置和形态,主要有四种旋转方式:
失衡类型 描述 解决方法
1.LL (左左) 在节点A的左子树的左子树上插入节点导致失衡 对节点A执行一次右旋 (Right Rotation)
2.RR (右右) 在节点A的右子树的右子树上插入节点导致失衡 对节点A执行一次左旋 (Left Rotation)
3.LR (左右) 在节点A的左子树的右子树上插入节点导致失衡 先对A的左子节点左旋,再对A右旋
4.RL (右左) 在节点A的右子树的左子树上插入节点导致失衡 先对A的右子节点右旋,再对A左旋
通过这四种旋转操作,AVL树能高效地恢复平衡。
平衡二叉树优缺点与应用场景
优点:查找操作非常高效,因为它是一种严格的平衡二叉树。
缺点:为了维护这种严格的平衡,在插入和删除节点时可能需要进行多次旋转操作,维护成本相对较高。
因此,AVL树适用于查找操作远多于插入和删除操作的场景,例如:
数据库索引:需要快速查找数据;需要频繁查找的内存数据结构。早期曾被用于 Linux 内核中。
相比之下,红黑树也是一种自平衡二叉搜索树,但它是一种"弱平衡"树,插入和删除操作更快,因此在需要频繁增删改查的综合性场景(如C++ STL的map和set)中应用更广。下一篇讲解红黑树。
平衡二叉树的操作
AVL树的操作核心是在标准二叉搜索树(BST)操作的基础上,增加"平衡维护"的步骤。具体来说,查找操作和BST完全一致,而插入和删除操作则在BST操作之后,需要沿路径向上回溯检查平衡因子,并在失衡时执行旋转。
1.查找(Search)
与普通BST完全相同,时间复杂度稳定为 O(log n)。
从根节点开始,若目标值等于当前节点则返回;小于则去左子树,大于则去右子树。由于AVL树严格平衡,查找过程不存在最坏情况链表退化的问题。
源码见二叉搜索树(BST Tree)讲解
2.插入(Insert)
插入分三步走,重点在于回溯和旋转:
第一步:标准BST插入。从根节点出发,按照BST规则将新节点插入为叶子节点。
源码见二叉搜索树(BST Tree)讲解
第二步:更新平衡因子。从新插入的节点开始,自底向上(向根节点方向)回溯,更新沿途所有祖先节点的高度和平衡因子。
第三步:检查并修复失衡。在回溯过程中,如果遇到某个节点A的平衡因子变为 +2 或 -2,说明以A为根的子树失衡。此时根据插入位置,执行四种旋转之一:
插入在A的左子树的左子树(LL):对A执行右旋。
插入在A的右子树的右子树(RR):对A执行左旋。
插入在A的左子树的右子树(LR):先对A的左子节点左旋,再对A右旋。
插入在A的右子树的左子树(RL):先对A的右子节点右旋,再对A左旋。
关键特性:在插入操作中,一旦执行了旋转修复,失衡点以上的祖先节点的平衡因子会自动恢复,无需继续向上回溯,因此插入操作最多只需要两次旋转(LR或RL情况)即可完成。
插入源码
cpp
//插入操作
void AVLTree::insert(int val)
{
BinarySearchTree::insert(val);
Node* p = nullptr;
//判断是否平衡
if (isBalance(p) || p == nullptr)
{
//是平衡二叉树,不需要调整
return;
}
//需要调整
//(p,q,s),左左型,右右型,右左型和左右型的旋转调整
if (p->right == nullptr)
{
//左左型和左右型
Node* q = p->left;
if (q->right == nullptr)
{
//左左型 右旋
Node *s = q->left;
rightHandedRotation(p, q, s);
}
else
{
//左右型
Node *s = q->right;
//先左旋
levorotation(q, s, nullptr);
//后右旋
rightHandedRotation(p, s, q);
}
}
else
{
//右右型和右左型
Node* q = p->right;
if (q->left == nullptr)
{
//右右型 左旋
Node *s = q->left;
levorotation(p, q, s);
}
else
{
//右左型
Node *s = q->right;
//先右旋
rightHandedRotation(q, s, nullptr);
//后左旋
levorotation(p, s, q);
}
}
}
3.删除(Delete)
删除是AVL树最复杂的操作,因为它可能导致多个祖先节点连续失衡,修复可能一直传播到根节点。
第一步:标准BST删除。分三种情况处理待删节点:
1.无子节点:直接删除。
2.只有一个子节点:用子节点顶替自己。
3.有两个子节点:找到中序后继(右子树的最左节点)或中序前驱(左子树的最右节点)来替换值,然后递归删除那个后继/前驱节点(该节点最多只有一个子节点)。
源码见二叉搜索树(BST Tree)讲解
第二步:更新平衡因子。从实际被删除节点的父节点开始,自底向上回溯到根节点,更新所有经过节点的平衡因子。
第三步:检查并修复失衡。在回溯过程中,每遇到一个失衡节点,就执行旋转修复。与插入不同,删除后的旋转会降低子树的高度,这可能导致其祖先节点再次失衡。因此,删除操作在修复一个节点后,必须继续向上回溯检查,直到根节点。
关键特性:删除操作可能触发 O(log n) 次旋转,因为修复了一个失衡点,高度降低后,上面的节点可能又失衡了。
复杂度总结对比
操作 时间复杂度 旋转次数
查找 O(log n) 0
插入 O(log n) 最多 2 次(常数级)
删除 O(log n) 最坏情况 O(log n) 次