平衡二叉树(AVL树)

前提是你 提交于 2020-02-24 14:22:50

AVL树的提出背景

我们知道在创建二叉查找树的时候,我们都是使用一个已知的序列,通过从左到右读取序列进行创建。比如对于序列{1,2,3,4,5},我们从左到右读取序列,可以得到如下图的二叉查找树:
在这里插入图片描述
显然,这棵二叉查找树是链式的。那么,一旦需要对有10510^5级别个递增元素的序列构建二叉查找树,也将会得到一棵长长的链条式的树,此时对这棵树的结点进行查找的复杂度就会达到O(n)O(n),这就根本起不到二叉查找树来进行查询优化的目的

综上所述,我们需要对树的结构进行调整,使得树的高度在每次插入元素后仍然能保持 O(logn)O(logn) 的级别,这样能使查询操作仍然是O(logn)的时间复杂度,于是就产生了平衡二叉树。

AVL树的定义

AVL树的定义

AVL树仍然是一棵二叉查找树,只是在二叉查找树的基础上增加了“平衡”的要求。

  • 平衡:对AVL树的任意结点来说,其左子树与右子树的高度之差的绝对值不超过1,其中左子树与右子树的高度之差称为该结点的平衡因子

例如图9-25中前两棵树就是AVL树,第三棵树不是AVL树,因为存在结点的平衡因子的绝对值大于1。(节点上标注的数值是平衡因子,默认是左子树的高度减去右子树的高度)
在这里插入图片描述

  • 定理1:只要能随时保证(其实一般就是插入结点删除结点的时候)树的每个结点的平衡因子的绝对值不超过1,AVL的高度就始终能保持O(logn)O(logn)(这里的原理没搞太清楚,反正它是可以证明会保持O(logn)O(logn)的复杂度的…)

AVL树的结点定义

由于需要对每个结点都得到平衡因子,因此需要在树的结构中加入一个变量height,用来记录以当前结点为根结点的子树的高度

struct node{
	int v;
	int height;
	node* lchild;
	node* rchild;
}; 
  • 注意:至于为什么不在树的结点里存储该结点的平衡因子,请思考。

AVL树的简单操作

在这种定义下,如果新建一个结点,就可以使用如下写法:

node* newNode(int v){
	node* Node = new node;
	Node->v = v;
	Node->height = 1;
	Node->lchild = Node->rchild = NULL;
	return Node;
}
  • 注意,结点的初始高度为1

显然,可以通过下面的函数获取结点root所在子树的当前高度:

int getHeight(node* root){
	if(root == NULL){
		return 0;
	}else{
		return root->height;
	}
}

于是根据定义,可以通过下面的函数计算平衡因子:

int getBalanceFactor(node* root){
	return getHeight(root->lchild) - getHeight(root->rchild);
}
  • 注意:为什么使用getHeight()函数而不直接使用root->lchild->height呢?因为一个节点的左右孩子可能为空。

  • 为什么不直接记录结点的平衡因子,而是记录高度?
    答:因为没有办法通过当前结点的子树的平衡因子计算得到该结点的平衡因子,而需要借助子树的高度间接求得。

显然,结点root的高度等于其左孩子与右孩子的高度的较大者加1,因此可以通过下面的函数来更新height:

void updateHeight(node* root){
	root->height = max(getHeight(root->lchild),getHeight(root->rchild))+1;
} 

AVL树的基本操作

下面介绍AVL树的基本操作,为了讲解方便,以下假设每个结点的权值都不相同。
和二叉查找树相同,AVL树的基本操作有:

  1. 查找
  2. 插入
  3. 建树
  4. 删除

由于删除操作较为复杂,因此主要介绍AVL树的查找,插入和建树。同时需要注意的是插入操作以查找操作为基础,建树又以插入操作为基础。

查找操作

由于AVL树是一棵二叉查找树,因此其查找操作的做法与二叉查找树相同。由于我们在前面提到AVL树的高度为O(logn)O(logn)级别,因此AVL树的查找操作的时间复杂度为O(logn)O(logn)

void search(node* root,int x){
	if(root == NULL){
		printf("search failed\n");
		return;
	}
	if(x == root->data){
		printf("%d\n",root->data);
	}else if(x < root->data){
		search(root->lchild,x);
	}else{
		search(root->rchild,x);
	}
}

插入操作

左旋和右旋

先抛开AVL树的插入问题,考虑下面的情况:
在这里插入图片描述
这个调整过程称为左旋(Left Rotation)。假设指针root指向结点A,指针temp(是一个临时指针)指向结点B,于是调整过程可以分为三个步骤,请结合图9-27理解。
在这里插入图片描述

//左旋(Left Rotation)
void L(node* &root){//注意是传地址的
	node* temp = root->rchild;
	root->rchild = temp->lchild;	//步骤1 
	temp->lchild = root;			//步骤2 
	updateHeight(root);				//更新高度 
	updateHeight(temp);				//更新高度 
	root = temp; 					//步骤3 
} 

上述代码,需要注意其他的两点:

  1. 要传root的地址;
  2. 要更新temp和root的高度。而且需要注意的是:要先更新root的高度,再更新temp,不能写反!!!

既然有左旋,一定有右旋(Right Rotation)。事实上,右旋和左旋是对称的过程,如图9-28所示:
在这里插入图片描述在这里插入图片描述在这里插入图片描述

//右旋(Right Rotation)
void R(node* &root){
	node* temp = root->lchild;
	root->lchild = temp->rchild;
	temp->rchild = root;
	updateHeight(temp);
	updateHeight(root);
	root = temp;
} 

在这里插入图片描述

插入类型

关于旋转的讨论到此为止了,接下来开始讨论AVL树的插入操作。

假设现在有一棵平衡二叉树,那么可以预见到,在往其中插入一个结点时,一定会有结点的平衡因子发生变化,此时可能会有结点的平衡因子的绝对值大于1(这些平衡因子只可能是2或者-2,想想为什么?),这样以该结点为根结点的子树就是失衡的,需要进行调整。显然,只有在从根结点到该插入结点的路径上的结点才可能发生平衡因子变化,因此只需对这条路径上失衡的结点进行调整。

  • 定理:只要把最靠近插入结点的失衡结点调整到正常,路径上的所有结点就都会失衡。

假设最靠近插入结点的失衡结点是A,显然它的平衡因子只可能是2或者-2。很容易发现这两种情况完全对称,因此主要讨论结点A的平衡因子是2的情形(也就是插入到A的左子树中)。

在这里插入图片描述在这里插入图片描述
(如果是0的话,那么说明它的左右在插入之前就已经不是一个平衡二叉树了,因为插入这个点并没改变高度。)在这里插入图片描述(上面的话要理解)

LL型

现在考虑怎样调整这两种树型,才能使树平衡?
先考虑LL型,可以把以C为根结点的子树看作一个整体,然后以结点A作为root进行右旋,便可以达到平衡,如图9-32所示。在这里插入图片描述

LR型

然后考虑LR型,可以先忽略结点A,以结点C为root进行左旋,就可以把情况转化为LL型,然后按上面LL型的做法进行一次右旋即可,如图9-33所示。在这里插入图片描述
至此,结点A的平衡因子是2的情况已经讨论清楚了,下面简要说明平衡因子是-2的情况,显然这两种情况是完全对称的。

由于结点A的平衡因子为-2,因此右子树的高度比左子树大2,于是以结点A为根结点的子树一定是图9-34的两种形态RR型和RL型之一。

注意,由于和上面讨论的LL型和LR型对称,此处结点A、B、C的权值满足A<B<C。可以发现,当结点A的右孩子的平衡因子是-1时为RR型,是1时为RL型。在这里插入图片描述

RR型

对RR型来说,可以把以C为根结点的子树看作一个整体,然后以结点A作为root进行右旋,便可以达到平衡,如图9-35所示。在这里插入图片描述

RL型

对RL型来说,可以先忽略结点A,以结点C为root进行右旋,就可以把情况转化为RR型,然后按上面RR型的做法进行一次左旋即可,如图9-36所示。在这里插入图片描述
至此,对LL型、LR型、RR型、RL型的调整方法都已经讨论清楚了,下面做个小小的汇总表,见表9-1。

小结

表9-1 AVL树插入情况汇总(BF表示平衡因子,not boyfriend~~)

树型 判定条件 调整方法
LL BF(root) =2,BF(root->lchild)=1 对root进行右旋
LR BF(root) =2,BF(root->lchild)=-1 对root->lchild进行左旋,再对root进行右旋
RR BF(root) =-2,BF(root->rchild)=-1 对root进行左旋
RL BF(root) =-2,BF(root->rchild)=1 对root->rchid进行右旋,再对root进行左旋

代码

现在考虑如何书写插入代码。

首先,AVL树的插入代码是在二叉查找树的插入代码的基础上增加平衡操作的,因此,如果不考虑平衡操作,代码是下面这样的:

//插入权值为v的结点
void insert(node* &root,int v){
	if(root == NULL){//到达空结点 
		root = newNode(v);
		return; 
	}
	if(v < root->v){
		insert(root->lchild,v);
	}else{
		insert(root->rchild,v);
	}
} 

(注意:root传的是地址&root。)
在这个基础上,由于需要从插入的结点开始从下往上判断结点是否失衡,因此需要在每个insert函数之后更新当前子树的高度,并在这之后根据树型是LL型、LR型、RR型、RL型之一来进行图9-33的平衡操作,代码如下:

//插入权值为v的结点
void insert(node* &root,int v){
	if(root == NULL){
		root = newNode(v);
		return;
	}
	if(v < root->v){
		insert(root->lchild,v);
		updateHeight(root);	//更新树高 
		if(getBalanceFactor(root) == 2){
			if(getBalanceFactor(root->lchild) == 1){
				R(root);
			}else if(getBalanceFactor(root->lchild) == -1){
				L(root->lchild);
				R(root);
			}
		}
	}else{
		insert(root->rchild,v);
		updateHeight(root);
		if(getBalanceFactor(root) == -2){
			if(getBalanceFactor(root->rchild) == -1){
				L(root);
			}else if(getBalanceFactor(root->rchild) == 1){
				R(root->rchild);
				L(root);
			}
		} 
	}
} 

建树操作

有了上面插入操作的基础,AVL树的建立就非常简单了,因为只需依次插入n个结点即可。代码如下:

//AVL树的建立
node* Create(int data[],int n){
	node* root = NULL;
	for(int i=0;i<n;i++){
		insert(root,data[i]);
	}
	return root;
} 

其他

  • 所有引起树的结构变化的操作都要传地址,如上述的Create()insert()L()R()

题型训练

【PAT A1066】Root of AVL Tree

参考文档

算法笔记

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!