平衡二叉搜索树讲解

平衡二叉树介绍

平衡二叉搜索树(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) 次

删除源码见下篇文章

相关推荐
星星.72219 分钟前
【图论】最小生成树|Prim+Kruskal算法
数据结构·c++·算法·图论
charlie11451419123 分钟前
Cinux · musl 静态移植:对齐 Linux ABI、铺初始栈,以及一个被 SMAP 拦下的潜伏 bug
linux·开发语言·c++·操作系统·开源项目
_Narcissus_33 分钟前
单调栈笔记及例题详解
数据结构·c++·笔记·算法·力扣·单调栈·洛谷
流浪00138 分钟前
C/C++后端筑基系列(二):C++ 从基础到进阶
开发语言·c++
kyle~40 分钟前
x86 汇编LOCK前缀 --- 硬件架构向软件提供的核心同步原语
汇编·c++·性能优化·硬件架构·实时系统
额,不知道写啥。1 小时前
从区间加一次函数到区间加多次函数最值-----区间数据结构与函数的一些东西(《浅谈函数最值的动态维护》的学习笔记)
数据结构·笔记·学习
LuminousCPP1 小时前
数据结构-二叉树(五):查找、销毁与前序序列建树
c语言·数据结构·笔记·二叉树
Escalating_xu2 小时前
【C++入门基础(下)】默认参数、函数重载、引用、inline 与 nullptr
android·c++·redis