AVL树简单实现及原理

平衡树 专栏收录该内容
1 篇文章 0 订阅

AVL树的原理及简单实现

AVL是带有平衡条件的二叉查找树。AVL树是每个节点的左子树和右子树的高度差最多为1的二叉查找树(空树的高度为-1,空树高度的作用后面会有解释,请务必记住AVL树的定义,该定义贯穿了数据结构实现的全部)。除了插入和删除外,所有树的操作都可以以时间 O ( l o g N ) O(logN) O(logN)的时间完成。当进行插入和删除操作时,会破坏树的平衡关系,于是需要实时更新路径上节点的所有平衡信息并对树的平衡关系进行修正。而树的修正方式称为旋转:

旋转最基本的旋转方式为左旋和右旋:树的其他复杂的旋转方式都由这两种方式实现(如Spary树和红黑树的的旋转)。

旋转:

  • 左旋:对左儿子进行旋转。
    在这里插入图片描述
  • 右旋:对右儿子进行旋转
    在这里插入图片描述

AVL树的失衡种类及修正方法:

  • 左左:左旋:图中,k1节点的左节点比右节点高1保持平衡,k2节点失衡,因为k2左节点比右儿子节点高2
    在这里插入图片描述

  • 左右:左儿子右旋,自己再左旋:图中,k2节点保持平衡,k1节点虽然右子树比左子树高1,但是仍然是平衡的。对于k3,它的左儿子节点比右儿子节点高2,不平衡。

在这里插入图片描述

  • 右右:右旋:高度关系同左左
    在这里插入图片描述
  • 右左:右儿子左旋,自己再右旋:高度关系同左右

在这里插入图片描述

注意:对于以上的四种旋转方式,都可以看作某一个节点的子树在进行平衡,也就是说,上面四个图左边的树,都是某一棵树的子树。而这颗子树在进行平衡的时候,子树的根节点发生了变化,此时需要在子树发生旋转的同时,保证子树父节点的指向也随之改变。对于C++,可以采用引用的方式,将父节点的指针引用进来,改变函数参数的同时也改变了指向,但是此处是采用C语言实现,于是只能采用返回指针值得形式来修改,于是插入删除的函数形式都变成了Node * function(ElemType elem,Node* tree);而调用方式也都变成了pointer = function(elem,pointer)注意,这两个pointer是同一个变量。

这里需要提一下函数形参:函数参数无论是指针还是非指针,作为函数参数的时候(非引用方式)都是在内存中复制一份用于函数内部的计算。因此传值形式,改变值,不会对源值产生影响。而对于传指针的形式之所以能够改变值,是因为指针复制一份之后,虽然存储的位置发生了变化,但是指向的地址还是没有发生变化,因此可以通过*pointer的形式改变源值。但是,这不意味着改变函数参数中指针本身的值,就能够一起改变指针的源值,因为其本质上只是复制一份地址。

正是因为改变函数参数的指针值不能够改变源值,所以才需要返回指针并赋给源值。用C++的引用会方便很多。

AVL树插入维护采用自底向上的方式:对于除插入节点以外的路径上的节点,对其左右儿子的高度进行判断,如果不满足平衡条件,再分别读取左右儿子的儿子节点高度来确定需要旋转的类型。

插入函数:

Node * Insert(Elem_Type value,Node * Tree);

输入参数:节点数据,树地址

返回参数:树地址

使用方式:tree = Insert(value,tree);

插入逻辑:

  • 树为空:申请空间初始化并存储信息和高度,返回存储地址
  • 树非空:
    • 若插入点值等于当前节点值:
      • 基于不同的方式进行处理,比如在节点里加入一个count记录相同值的个数
    • 若插入点值小于当前节点值:
      • left_son=Insert(value,left_son);
    • 若插入节点大于当前值:
      • right_son=Insert(value,right_son);
    • 判断节点是否平衡:左右儿子之间的差值是否等于2,再确定旋转类型,并重新平衡AVL树
    • 更新节点高度

定义空树高度为-1的意义在于:因为插入的节点不需要判断是否平衡(肯定平衡),其父节点需要读取其左右儿子节点的高度值来判断是否平衡,当插入节点没有兄弟节点时,插入节点的父节点肯定有一个儿子节点指空,定义空树的高度有益于消除特判。这种情况下,插入节点父节点的儿子高度分别为0和-1,满足条件。如果插入节点引起了AVL树的不平衡,其祖父节点也会查询插入节点的兄弟节点,也解决了兄弟节点为空的特判。

AVL树删除维护

删除函数:Node * Delete(Elem_Type value,Node * Tree);

输入参数:待删除的节点值,树地址

输出参数:树地址

使用方式:tree = Delete(value,tree);

删除逻辑:

  • 当节点非空时
    • value小于当前值:tree->left = Delete(value,tree);
    • value大于当前值时:tree->right = Delete(value,tree);
    • value等于当前值时:
      • 当前节点左右儿子均不为空:从左子树找到一个数值最大的节点,令当前节点的值为数值最大节点的值(max_point = get_max();tree->value = max_point->value),并从左子树中删除值左子树最大节点(tree->left = Delete(max_point->value,tree->left)
      • 存在一个儿子节点为空,删除该节点,并返回一个儿子节点的地址
      • 左右儿子为空,删除该节点,返回NULL
    • 当节点非空(删除一个元素之后,节点可能为空,也有可能不为空),左右儿子可能失衡:
      • 判断左右儿子的高度关系,通过旋转平衡节点。
      • 更新节点高度。(可能会有这样的疑问,明明在旋转的时候更新了节点的高度,这里为什么还要写一次:对于需要旋转的,不会产生影响。但是不需要进行旋转的节点通过这个函数更新高度。)
  • 返回树地址

关于插入和删除的细节以及一些可能产生疑问的地方,我以注释的形式写在了下面的代码中。

#include<bits/stdc++.h>
using namespace std;
//AVL tree

struct node{
	int val;
	node * left;
	node * right;
	int height;
	//根据AVL树的定义:树的左右节点高度只差不能大于1,因此需要保存高度信息
}; 

int height(node * tree){
	if(tree==NULL)return -1;
	else return tree->height;
	/*
	有的朋友也许会产生疑问:为什么要定义这样一个函数来判断高度,不是直接能通过指针读取吗?
	也许,有的朋友会这样问:采用Height函数是用于解决哪些情况下空指针的特判?
	这里我简单说一下:
	1.某节点没有子树(这个节点在插入的时候的高度是直接赋0的),此时在其左儿子插入一个值,当延插入路
	径回溯的时候,需要更新该节点的高度,但是此时它有一个节点为NULL,不能够通过node->right->height
	的方式得到高度(会报错,程序GG),此时可以特判,但是由于它是对称的,于是当插入的是右节点也需要
	判断一次。通过一个函数的形式,简化了判断。
	2.某个节点发生了失衡,假设是左边子树高度-右边子树高度==2,此时需要访问左子树的左右儿子节点高度
	关系来判断是左左型还是左右型,此时和1里面说的一样,当左子树存在一个节点为空,就需要特判。而右子
	树比左子树高且失衡时也是如此。特判过程过于繁琐,且容易出错。通过一个函数的形式,既简化了代码表
	示,又减少了失误率。
	*/
}

node * makeempty(node * tree){
	if(tree!=NULL){
		makeempty(tree->left);
		makeempty(tree->right);
		free(tree); 
	}
	return NULL;
	//在宏里可能有作用,但是在这份代码里一点作用都没有
}

node * find(int val, node *tree){
	if(tree!=NULL){
		if(tree->val>val)return find(val,tree->left);
		else if(tree->val<val)return find(val,tree->right);
	}
	return tree;
	//普通的排序树查找
}

node * getmax(node * tree){
	if(tree!=NULL){
		while(tree->right!=NULL)tree=tree->right;
	}
	return tree;
}

node * getmin(node * tree){
	/*
	get_min和get_max函数关于NULL的特判需要解释一下:该函数不仅会在Delete函数中调用,也有可能单独调
	用,当单独调用的时候,需要判断是否为空
	当get_max在Delete函数中的调用,实际上是不需要判断为空的,因为其父节点左右子树非空是调用该函数
	的前提
	*/
	if(tree!=NULL){
		while(tree->left!=NULL)tree=tree->left;
	}
	return tree;
}

node * left_son_rotate(node * tree){
	//左子树旋转
	node * k2 = tree;
	node * k1 = tree->left;
	k2 ->left = k1->right;
	k1->right = k2;
	//先更新k2 再更新k1 因为此时k2成为了k1的子树 自下往上更新高度
	k2->height = max(height(k2->left),height(k2->right))+1;
	k1->height = max(height(k1->left),height(k1->right))+1; 
	return k1;//返回指针是为了改变源指针的值
}

node * right_son_rotate(node * tree){
	//同上
	node * k2 =tree;
	node * k1 = tree->right;
	k2 ->right = k1->left;
	k1->left = k2;
	k2->height = max(height(k2->left),height(k2->right))+1;
	k1->height = max(height(k1->left),height(k1->right))+1; 
	return k1;
}

node * left_right_son_rotate(node * tree){
	//先旋转子树 再旋转自己
	tree->left = right_son_rotate(tree->left);
	return left_son_rotate(tree);
}

node * right_left_son_rotate(node * tree){
	tree->right = left_son_rotate(tree->right);
	return right_son_rotate(tree);
}

node * insert(int val,node *tree){
	if(tree==NULL){//到达叶子节点,此时申请空间,初始化并赋值,返回节点地址用于父节点更新儿子指针
	//的指向
		tree=(node*)malloc(sizeof(node));
		tree->val = val;
		tree->left=NULL;
		tree->right = NULL;
		tree->height =0;
	}
	else if(tree->val >val){//插入和删除均采用自底向上的方式,先执行插入/删除的操作,然后再平衡树
	//和更新节点高度
		tree->left = insert(val,tree->left);
		//所有的插入都会发生在叶子节点上 可以想想到达叶子节点的实现情况
		if(height(tree->left)-height(tree->right)==2){
			//该节点在插入操作之后失衡,由于是插入左子树,失衡只能是左子树比右子树高
			//判断是左左型还是左右型 根据类型调用相应的旋转函数
			if(val<tree->left->val)tree = left_son_rotate(tree);
			else tree = left_right_son_rotate(tree);
		}
	}else if(tree->val<val){//同leftinsert
		tree->right = insert(val,tree->right);
		if(height(tree->right)-height(tree->left)==2){
			if(val>tree->right->val)tree = right_son_rotate(tree);
			else tree= right_left_son_rotate(tree); 
		}
	}//此处没有写等于的情况,可以说这是实现了一个set,但是如果需要实现重复元素存储,可以在树节点中
	//加入一个count标记用来标记相同数值的节点个数,当运行到此处的时候,只需要执行count++即可
	tree->height = max(height(tree->left),height(tree->right))+1;
	/*
 	这个高度更新需要解释一下:这里容易发生疑问的就是,明明上面在进行旋转操作的时候,就已经更新了节
 	点的height信息,为什么这里还需要多此一举来再更新一次高度信息呢?其实不然,产生这种想法是把自己
 	绕进去了,其实插入不一定会产失衡,比如说插入前左子树比右子树高1,插入完成后,左右子树一样高,此
 	时就不会进入旋转操作,但是这个例子不更新节点高度不会产生影响,但是对于下面这个例子则不一样:插
 	入前树的两个儿子节点一样高,插入后,两个儿子节点高度差为1,此时该节点的高度也会增加1,如果没有
 	进行执行该代码,AVL树会产生bug
	*/
	return tree;
}

node * deletenode(int val,node * tree){
	if(tree!=NULL){//显然,节点为空就说明树里面没有这个值(逐层递归到NULL了还没找到,就是没有),
	//返回NULL
		if(tree->val>val)tree->left = deletenode(val,tree->left);
		//操作逻辑同插入:先删除,再维护
		else if(tree->val<val)tree->right = deletenode(val,tree->right);
		else{
		//对于存储多个相同值的情况,可以这样判断,当count>1时执行--,当count=1时执行下面的代码
			if(tree->left!=NULL&&tree->right!=NULL){
			//左右节点均不为空,那么就找到左子树的最大节点,将其值存储在当前节点,并从左子树中删去
			//左子树的最大节点,替罪羊
				node * p =getmax(tree->left);
				tree->val = p->val;
				tree->left = deletenode(tree->val,tree->left);
			}
			else {
				//对于上面的那种情况,最终也会转化成这种情况,因为一棵树的最大节点必没有右子树,但
				//是可能有左子树,此时不满足上面if的条件,便转而执行该段代码
				node * p = tree;//保存当前节点的地址,当树结构修改完成之后,free该地址
				if(tree->left==NULL)tree = tree->right;
				//左儿子为空 返回右儿子(右儿子也可能为空)
				else if(tree->right == NULL)tree = tree->left;
				//否则返回左儿子 此时右儿子必为空,因为不为空不会进入该代码块,而是if后面的代码块
				free(p);//树结构更新完毕,删除该节点
			}
		}
		//当节点删除完毕之后,对树的结构进行修正,此时树的左右儿子均是平衡的。可以由下往上推理,对
		//于已经删除的节点,其父节点通过下面的代码进行了平衡,于是返回到其祖父节点,而祖父节点的儿
		//子节点,也就是删除节点的父节点是平衡的,于是逐层向上,满足左右儿子都是平衡的前提。
		if(tree!=NULL){
		//这里的tree判空容易忘记导致出错,这里必须要考虑到树可能为空,也就是待删除的节点既有没左儿
		//子又没有右儿子,虽然关于旋转判断的代码不会出错,但是高度更新的tree->height会发生访问错误
			if(height(tree->left)-height(tree->right)==2){
				if(height(tree->left->left)>height(tree->left->right))tree = left_son_rotate(tree);
				else tree = left_right_son_rotate(tree);
			}
			else if(height(tree->right)-height(tree->left)==2){
				if(height(tree->right->right)>height(tree->right->left))tree = right_son_rotate(tree);
				else tree = right_left_son_rotate(tree);
			}
			//这里的高度更新和插入一样,在已经进行旋转操作的节点来说,是多此一举,但是对于没有发生
			//旋转操作的节点,却是必不可少的。
			tree->height = max(height(tree->left),height(tree->right))+1;
		}
	}
	return tree;
}

void print_tree(node * tree){
	if(tree!=NULL){
		print_tree(tree->left);
		printf("%d ",tree->val);
		print_tree(tree->right);
	}
}

int main(){
	node * tree = NULL;
	tree = makeempty(tree);
	int a[]={3,2,1,4,5,6,7,16,15,14,13,12,11,10,9,8};
	for(int i=0;i<16;++i)printf("tree:%d insert:%d\n",i,a[i]),tree = insert(a[i],tree),print_tree(tree),puts("");
	for(int i=0;i<16;++i)printf("tree:%d delete:%d\n",i,a[16-i-1]),tree = deletenode(a[i],tree),print_tree(tree),puts("");
	//此处采用的例子是《数据结构与算法分析C语言版第二版》中AVL树的例子,而本博客中的代码也均来自该
	//书,今后有机会会将书的电子版,题解,源码等上传到CSDN
}

温故而知新,第一次学AVL树的时候,只是把代码给背会了,但是没有完全理解其中的逻辑,对于其中很多细节性的问题完全解答不了,当第二次学的时候,想更加了解其中的逻辑,以及为什么要这么实现,更加注重对细节的理解,于是有了这篇博客。今后当第三次看AVL树的时候,能够更加精进,对它的理解到达一个新的层次。文章中有不足和错误之处在所难免,感谢各位的谅解。

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值