数据结构:二叉平衡树

在学数据结构与算法的时候,二叉平衡树也就是AVL树是一个很重要的数据结构。下面我将由浅入深的讲解它。

什么是二叉平衡树

二叉平衡树又叫做AVL树为啥会有这么奇怪的名字呢?原因是为了纪念前苏联的两个科学家Adelse-Velskil和Landis,取其首字母 AVL 因此叫做AVL树。
它的全名是平衡二叉查找树,这里有二叉查找树,这是不是就说明它是和BST有关的呢
实际上平衡二叉查找树,就是树如其名 二叉,代表是二叉树,查找树,说明它是拥有和BST一样的性质,左<root<右。平衡代表任意一个节点的左右子树的高度差<=1
又被称为|平衡因子|<=1。
平衡因子:某节点的左⼦树与右⼦树的⾼度(深度)差即为该节点的平衡因子。(BF,Balance Factor)
这样我们就对二叉平衡树有了大致的了解,可以把它理解为一棵二叉查找树+平衡的性质。
AVL树一定是BST树,但BST树不一定是AVL树。

为什么会有二叉平衡树

AVL 树其实就是为了防止普通二叉搜索树(BST)变成“歪脖子树”而发明的。
普通 BST 的有着致命的缺陷,
普通 BST 在理想情况下查找、插入、删除都是O(logn)
因为树的高度接近 log n。
但是会有一种比较糟糕的插入顺序,插成一棵斜树了。

1
 \
  2
   \
    3
     \
      4
       \
        5

这已经不是树了,是链表,高度 = N,查找变成O(n)
那么这和线性搜索就没什么区别了
为了弥补这一缺陷,有了AVL树
AVL 树的好处在于AVL 树在每次插入、删除时都会主动检测平衡因子并通过旋转修复。它保证:任意节点的左右子树高度差≤1,所以树的高度最多是O(logn)
无论你插入数据的顺序多糟糕,都不会退化成链表。
那么使用AVL树进行查找,不管怎么插入,最终的最坏时间复杂度都是O(logn)
这大大的加快了速度。AVL树进行插入或者删除虽然有额外的开销,但仍是常数级,仍然是O(logn)

操作普通 BST 最坏AVL 树最坏
查找O(n)O(log n)
插入O(n)O(log n)
删除O(n)O(log n)

适用场景

因为插入或者删除有额外的开销,AVL树适用于查找操作频繁、更新操作较少时(查找性能更稳定)。

如何构造一棵二叉平衡树

建 AVL 树的核心思路就是:
按 BST 规则插入新值(先不管平衡)
插入后,从当前结点向上回溯,检查高度差
如果失衡(|左高 - 右高| > 1),根据插入位置类型,旋转修复。

插入节点

插入流程
假设我们从空树开始,依次插入数字:
第一步:正常 BST 插入
如果当前结点为空 → 直接创建新结点
如果 val < root->data → 插左子树
如果 val > root->data → 插右子树
第二步:更新高度
每次插入结束后,需要更新当前结点的高度
第三步:检查平衡因子避免失衡。
如果失衡,那么就根据情况进行旋转。
有以下四种选转情况:

类型平衡因子方向插入方向旋转操作
LL左高插在左孩子的左边右旋
RR右高插在右孩子的右边左旋
LR左高插在左孩子的右边左旋左孩子 + 右旋自己
RL右高插在右孩子的左边右旋右孩子 + 左旋自己
AVLNode* insert(AVLNode*root,int k)//插入 建立一个平衡二叉树 
{
    if(root==NULL)//如果根指针为空,说明还没有创建第一个结点 
    {
    	AVLNode*s=(AVLNode*)malloc(sizeof(AVLNode));//创建结点 
		s->data=k;//赋值 
		s->l=s->r=NULL;//左右孩子为空 
		s->h=0;
		root=s; //根指针指向 
	}//说明把一个数据放在了这个结点里面了 
	else if(k<root->data)//如果插入的k小于根节点中的数
	//就让它往左孩子中插入 
	{
		root->l=insert(root->l,k);///递归写法
		if(geth(root->l)-geth(root->r)>1)//root失衡 ,如果让左子树插入结点后出现失衡 
		{
			if(k<root->l->data)//说明放在了左侧 ,要右旋 
			{//
				root=LLrotation(root); 
			 } 
			 else
			 {
			 		root=LRrotation(root);
			 	//lr
			 }
		 } 
		 
	}
	else if(k>root->data)
	{
		root->r=insert(root->r,k);
		if(geth(root->r)-geth(root->l)>1)//root失衡 
		{
			if(k>root->r->data)
			{//RR
				root=RRrotation(root);
			 } 
			 else
			 {
			 	root=RLrotation(root);
			 	//LR
			 }
		 } 
	}
	//因为在以上插入过程中root可能进行了调整,需要重新算一下高度
	
	root->h=maxx(geth(root->l),geth(root->r))+1;
	return root;
}

在这个过程中建一棵二叉排序树是容易的,难点在于旋转。
首先我们应该先判断平衡因子是否》1,然后再说是否要旋转。
举往左子树插入节点的例子,如果左子树的高度-右子树的高度大于1
那么就需要旋转,根据插入的情况,又分为左旋和右旋
如果插入的是左子树的左侧,那么就需要右旋
如果插入的是左子树的右侧,那么就需要左旋左孩子 + 右旋自己
跟上面的图中所写的是一致的。

if(geth(root->l)-geth(root->r)>1)//root失衡 ,如果让左子树插入结点后出现失衡 
{
			if(k<root->l->data)//说明放在了左侧 ,要右旋 
			{//
				root=LLrotation(root); 
			 } 
			 else
			 {
			 		root=LRrotation(root);
			 	//lr
			 }
} 

经过旋转调整后,AVL树不再失衡,也就是成为了真正意义上的AVL树。
需要注意的是AVL树的插入,是递归生成的,那么我们就需要要在插入一个节点后,不断的回溯判断,找到第一次失衡需要调整的节点,也就是插入一个节点x之后,x的可能有若干个长辈失衡,先去调整以离x最近的长辈为根的失衡子树—>最小失衡子树。
当明白大体的情况后,我们就来详细的讲一讲具体如何调整/或者说存在的四种失衡类型。
LL,RR,LR,RL
LL:失衡节点z的左孩子的左子树中插入了一个节点导致了z失衡
如图所示:
在这里插入图片描述

因为左边多了,那么我们肯定要想办法让它往右移动,也就是右旋
在这里插入图片描述
在旋转的过程中X变成了Y的右孩子,那么如果原来y有右孩子,需要把Y的右孩子放到旋转后X的左孩子的位置。
要注意在旋转的过程中指针的改变,这在写代码的时候十分重要。

AVLNode* LLrotation(AVLNode* x)
{
	AVLNode* y=x->l;
	x->l=y->r;//2
	y->r=x;//3
	x->h=maxx(geth(x->l),geth(x->r))+1;// 先求x的再求y的高度  
	y->h=maxx(geth(y->l),geth(y->r))+1;// 
	return y;//1
 } 

RR :失衡节点x的右孩子的右子树中插入了一个节点导致了x失衡
在这里插入图片描述
和LL类似无非是进行的是左旋因为右边多
经过右旋调整:
在这里插入图片描述

AVLNode* RRrotation(AVLNode* x)
{
	AVLNode* y=x->r;
	x->r=y->l;//2 
	y->l=x;//3
	x->h=maxx(geth(x->l),geth(x->r))+1;
	y->h=maxx(geth(y->l),geth(y->r))+1;
	return y;//1
 } 

LR 失衡节点x的左孩子的右子树中插入了一个节点导致了x失衡

经过旋转调整:

LR实际上,可以看成先对最小失衡子树的左孩子进行一个左旋
在这里插入图片描述
即需要先对Y进行左旋:
在这里插入图片描述
然后再对X进行右旋:
在这里插入图片描述
这样恰好使其平衡。
为啥需要这样子,为啥不能直接的左旋或右旋呢
这是因为BST 性质:中序遍历必须是递增的,平衡条件:任意节点左右子树高度差 ≤ 1,当插入新节点破坏了“平衡”,我们要通过旋转修复。但修复的时候,不能破坏 BST 的中序顺序。
拿这个场景举例:插入点在“左孩子 y 的右子树 z”

  x
 /
y
 \
  z

中序遍历:y < z < x
如果我们直接对 x 右旋:

y
 \
  x
 /
z

结果:中序顺序变成 y < x < z (z 被放错位置)
因此不能想当然的无脑进行左旋和右旋,而是要根据插入节点的位置灵活变通。
具体代码如下:

AVLNode*LRrotation(AVLNode* x)
{
   x->l=RRrotation(x->l);
   x=LLrotation(x);
   return x;
}

RL 失衡节点x的右孩子的左子树中插入了一个节点导致了x失衡
在这里插入图片描述
经过类似LR的处理得到:
在这里插入图片描述
完整代码如下:

 AVLNode*RLrotation(AVLNode* x)
{
   x->r=LLrotation(x->r);
   x=RRrotation(x);
   return x;
}

这样我们就通过不断的插入,旋转平衡得出了一棵AVL树。
这里在插入的时候也要不断的统计高度,这样才能计算是否高度差>1,是否会导致失衡。
我们对节点高度的维护是通过它的左右孩子的高度来确定,
即,一个节点的高度是等于,它的左右节点的高度中的最大高度,然后+1.
每一个节点的初始高度为0

int geth(AVLNode*x)
{
	if(x==NULL)
	{
		return 0;
	}
	else
	{
		return x->h;
	}
}
int maxx(int u,int v)
{
	if(u>v)
	{
		return u;
	}
	else{
		return v;
	}
}
AVLNode* insert(AVLNode*root,int k)//插入 建立一个平衡二叉树 
{
    if(root==NULL)//如果根指针为空,说明还没有创建第一个结点 
    {
    	AVLNode*s=(AVLNode*)malloc(sizeof(AVLNode));//创建结点 
		s->data=k;//赋值 
		s->l=s->r=NULL;//左右孩子为空 
		s->h=0;
		root=s; //根指针指向 
	}//说明把一个数据放在了这个结点里面了 
	else if(k<root->data)//如果插入的k小于根节点中的数
	//就让它往左孩子中插入 
	{
		root->l=insert(root->l,k);///递归写法
		if(geth(root->l)-geth(root->r)>1)//root失衡 ,如果让左子树插入结点后出现失衡 
		{
			if(k<root->l->data)//说明放在了左侧 ,要右旋 
			{//
				root=LLrotation(root); 
			 } 
			 else
			 {
			 		root=LRrotation(root);
			 	//lr
			 }
		 } 
		 
	}
	else if(k>root->data)
	{
		root->r=insert(root->r,k);
		if(geth(root->r)-geth(root->l)>1)//root失衡 
		{
			if(k>root->r->data)
			{//RR
				root=RRrotation(root);
			 } 
			 else
			 {
			 	root=RLrotation(root);
			 	//LR
			 }
		 } 
	}
	//因为在以上插入过程中root可能进行了调整,需要重新算一下高度
	
	root->h=maxx(geth(root->l),geth(root->r))+1;
	return root;
}

总结AVL树的插入操作代码如下:

#include <stdio.h>
#include<stdlib.h>
typedef struct AVLNode{
	int data;
	struct AVLNode*l;
	struct AVLNode*r;
	int h;//结点的高度和深度(为了维护避免插入失衡) 
}AVLNode;//一个结点 

int a[9]={1,2,3,4,5,6,7,8,9};//要插入的数据
 
int geth(AVLNode*x)
{
	if(x==NULL)
	{
		return 0;
	}
	else
	{
		return x->h;
	}
}
int maxx(int u,int v)
{
	if(u>v)
	{
		return u;
	}
	else{
		return v;
	}
}
//调整函数:
//ll
//lr
AVLNode* LLrotation(AVLNode* x)
{
	AVLNode* y=x->l;
	x->l=y->r;//2
	y->r=x;//3
	x->h=maxx(geth(x->l),geth(x->r))+1;// 先求x的再求y的高度  
	y->h=maxx(geth(y->l),geth(y->r))+1;// 
	return y;//1
 } 
AVLNode* RRrotation(AVLNode* x)
{
	AVLNode* y=x->r;
	x->r=y->l;//2 
	y->l=x;//3
	x->h=maxx(geth(x->l),geth(x->r))+1;
	y->h=maxx(geth(y->l),geth(y->r))+1;
	return y;//1
 } 
AVLNode*LRrotation(AVLNode* x)
{
   x->l=RRrotation(x->l);
   x=LLrotation(x);
   return x;
}
 AVLNode*RLrotation(AVLNode* x)
{
   x->r=LLrotation(x->r);
   x=RRrotation(x);
   return x;
}
 
 
AVLNode* insert(AVLNode*root,int k)//插入 建立一个平衡二叉树 
{
    if(root==NULL)//如果根指针为空,说明还没有创建第一个结点 
    {
    	AVLNode*s=(AVLNode*)malloc(sizeof(AVLNode));//创建结点 
		s->data=k;//赋值 
		s->l=s->r=NULL;//左右孩子为空 
		s->h=0;
		root=s; //根指针指向 
	}//说明把一个数据放在了这个结点里面了 
	else if(k<root->data)//如果插入的k小于根节点中的数
	//就让它往左孩子中插入 
	{
		root->l=insert(root->l,k);///递归写法
		if(geth(root->l)-geth(root->r)>1)//root失衡 ,如果让左子树插入结点后出现失衡 
		{
			if(k<root->l->data)//说明放在了左侧 ,要右旋 
			{//
				root=LLrotation(root); 
			 } 
			 else
			 {
			 		root=LRrotation(root);
			 	//lr
			 }
		 } 
		 
	}
	else if(k>root->data)
	{
		root->r=insert(root->r,k);
		if(geth(root->r)-geth(root->l)>1)//root失衡 
		{
			if(k>root->r->data)
			{//RR
				root=RRrotation(root);
			 } 
			 else
			 {
			 	root=RLrotation(root);
			 	//LR
			 }
		 } 
	}
	//因为在以上插入过程中root可能进行了调整,需要重新算一下高度
	
	root->h=maxx(geth(root->l),geth(root->r))+1;
	return root;
}
void inorder(AVLNode*root)//遍历树 
{
	if(root!=NULL)//如果不为空, 
	{
		inorder(root->l);//遍历左孩子因为它要保证升序序列 
		printf("%d ",root->data);//输出 
		inorder(root->r);
	}
 } 
int main()
{
	int i; 
	AVLNode*root=NULL;
	for(i=0;i<9;i++)
	{
		root=insert(root,a[i]);
	}
	inorder(root);
	printf("\n");
	printf("%d ",root->h);
	return 0; 
}

删除节点

AVL树的删除操作实际上是类似二叉排序树的删除操作的,无非是多了一个判断是否失衡。这里先不详细讲,有时间继续写

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值