详解AVL树(平衡二叉树)

Posted 水澹澹兮生烟.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了详解AVL树(平衡二叉树)相关的知识,希望对你有一定的参考价值。

目录

一.AVL树的概念与性质

1.1概念

1.2性质

二.AVL树操作

2.1AVL树节点定义

2.2AVL树插入

1.插入节点

2.保持树的平衡

3.更新parent平衡因子

三.AVL树的性能


我们在使用map/multimap/set/multiset这些容器时,有个共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。

一.AVL树的概念与性质

1.1概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

1.2性质

它具有以下性质:

  • 他的左右子树都是AVL树
  • 左右子树高度差(又称平衡因子)的绝对值不操作1
  • 如果一颗二叉搜索树是高度平衡的,他就是AVL树,如果他有n个节点,其高度可保持在O(log2n),搜索时间复杂度O(log2n)。

二.AVL树操作

AVL树的创建,进行插入与删除节点,同时进行平衡判断。(我在分析时,会将代码拆开来,而且只会写核心的部分,最终代码我会附上链接(C++实现链接),需要的直接去拿。)

2.1AVL树节点定义

struct AVLTreeNode
	AVLTreeNode* left;
	AVLTreeNode* right;
	AVLTreeNode* parent;
	T data;
	int bf;//VAL树中的平衡因子
	AVLTreeNode(const T& x)
		:left(nullptr)
		, right(nullptr)
		, parent(nullptr)
		, data(x)
		, bf(0)
;

2.2AVL树插入

1.插入节点

我们在对AVL树进行插入的时候,其实就是二叉搜索时的插入。

bool insert(const T& data)
	//先判断是否为空树
	if(nullptr == root)
		root = new Node(data);
		return true;
	
	Node* cur = root;
	Node* parent = nullptr;
	while(cur)
		parent = cur;
		if(data < cur->data)
			cur =cur->left;
		else if (data > cur->data)
			cur = cur ->right;
		else
			return false;
		
	
	//当找到要插入的位置时,进行插入
	cur = new Node(data);
	//如果此时插入的节点是大于父节点的,将其插入父节点的左边
	if(parent->data > data) parent->left = cur;
	else parent->right = cur;
	return true;

2.保持树的平衡

插入节点,如果此时的树满足平衡,那么就不用进行调节,但是如果此时树不再保持平衡,就要对树进行旋转调整,分为四种情况。(我在分析时,会将代码拆开来,而且只会写核心的部分,最终代码我会附上链接(C++实现),需要的直接去拿。)

(1)右单旋

发生的情况:当一个平衡二叉树满足具有较高左子树时(如下图A),且要插入的节点插入到了较高左子树的左侧(此时注意是左子树,并非左孩子,因此当插入节点20为30的有孩子,其操作是相同的),此时进行右单旋。

我们根据上图进行分析,在进行旋转时,第一步先将parent->left指向curR节点,此时并要对curR的parent进行更新,但是必须注意的是此时curR是否为空,如果此时curR为空再对他进行操作可能代码会崩溃,如下代码是对curR进行更新。

parent->left = curR;
//因为我们使用的是孩子双亲表示法,因此更新curR节点的parent
//此时curR不为空时,才能对其进行更新
if(curR) curR->parent = parent; 

 第一步操作结束,下来让cur->right指向parent节点,此时我们并不知道parent节点是否为之前树的根节点,因此要先将其父节点进行保存,后面要进行判断,再对插入的节点旋转结束后,对其旋转因子再进行更新。下面是第二步代码:

cur->right = parent;
Node* pparent = parent->parent;
//现在我们先更新parent节点的父节点
parent->parent = cur;
//再更新cur的父节点
cur->parent = pparent;
//此时我们要注意,判断pparent是否为空,如果为空,那么root直接就是cur,如果不为空在进行判断他是父节点的左子树还是右子树
if(pparent == nullptr)//说明此时更新节点后,cur应该为根节点
    root = cur;
else if(pparent->left == parent)//说明之前是根节点的左子树
    pparent->left = cur;
else//说明之前是右子树
     pparent->right = cur;
parent->bf = 0;
cur->bf = 0;

(2)左单旋

发生的情况:当一个平衡二叉树满足具有较高右子树时(如下图A),且要插入的节点插入到了较高右子树的右侧(此时注意是右子树,并非有孩子,因此,其操作是相同的),此时进行左单旋。

 我们根据上图进行分析,当开始进行左单旋时,我们先将parent节点进行处理,如下代码:

parent->right = curL;
//因为我们使用的是孩子双亲表示法,因此更新curL节点的parent
//此时curL不为空时,才能对其进行更新
if(curL) curL->parent = parent;

当处理完第一步之后,开始对cur节点进行处理,此时我们依旧要注意parent节点在左单旋之前是否是由父节点的,如下代码:

//在这里先创建parent的父节点,待会进行判断
Node* pparent = parent->parent;
cur->left = parent;
//更改parent的父节点
parent->parent = cur;
cur->parent = pparent;
//此时上面的代码将parent与curL两个节点的父节点都处理完毕,
//此时我们就要考虑cur此时是否有父节点,且是父节点的那个子树
if(pparent == nullptr)//此时parent的之前的父节点为空,那么此时cur直接就是根节点
    root = cur;
else if(pparent->left == parent)
    pparent->left = cur;
else
    ppanret->right = cur;
cur->bf = 0;
parent->bf = 0;

(3)左右双旋与右左双旋

左右双旋发生的情况:当原本的平衡二叉树是满足具有较高的左子树时,此时带插入的节点插入到了较高左子树的右侧(内侧),如下图,此时就要使用左右双旋。

我们在这里发现,他与要进行右单旋的情况有些像,但是在向AVL树中进行插入时,左子树的内侧,此时我们先试一试右单旋,观察他是否可以解决平衡问题,如下图。

 我们观察到上面的图,发现虽然这种情况下与右单旋的情况有些相似,但是不能够达到平衡的情况,因此我们就要重新考虑如何旋转使之平衡。如下图所示:

我们观察上图,可以先利用左单旋将parent的左子树进行调整,将其转化成结构B,此时的二叉树的结构符合右单旋的情况,因此再进行右单旋,最终满足平衡的情况,因此此时需要先进行左单旋,再进行右单旋。我们再看图D到图E的平衡因子的变化。因为,我们在前两次的左单旋与右单旋之后都对他当前的parent与cur的平衡因子进行的更改,在最后在curR上插入新节点的位置会引起不同的平衡因子的更新。此时的代码就是复用上面的代码。因此在这里就不再重复。

右左双旋发生的情况:当原本的平衡二叉树是满足具有较高的右子树时,此时带插入的节点插入到了较右子树的左侧(外侧),此时就要使用右左双旋。他的思路与左右双旋的思路相同,但是右左双旋实现将parent节点的右子树通过左单旋进行调整,使其满足左单旋的情况,再进行左单旋。此时二叉树就会满足平衡。

3.更新parent平衡因子

在插入之前,平衡因子是满足情况的,当插入了cur节点之后,我们要从cur插入的位置向上更新parent的平衡因子,在更新时,我们只需要判断cur被插入到了parent的那棵子树,插入到左子树时,对parent->buf--;插入到右子树时,对parent->buf++。代码如下:

while(parent)//当parent不为空,那就相当于要遍历到二叉树的根节点即可
	if(parent->right == cur)//说明此时插入到了右子树的位置
 	   parent->bf++;
	else//说明此时插入到了左子树的位置
  	  parent->bf--;
  	if( parent->bf == 0)//这里就是以parent为根的二叉树高度没有改变,对上层不会用影响
  		break;
  	else if(1 == parent->bf || -1 == parent->bf)
  		//以parent为根的二叉树的高度增建了,需要继续往上层更新
  		cur = parent;
  		parent = parent->parent;
  	else//此时已经违反了AVL树的特性
  		if(parent->bf == -2)	
  			if(cur->bf == -1)
  				//此时进行右单旋
  			else
  				//此时进行左右双旋
  			
  		else
  			if(cur->bf == -1)
  				//此时进行右左双旋
  			else
  			//此时进行左单旋
  			
  			break;
  		
  		

4.AVL树验证

 AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,可以分两步:

  •  验证其为二叉搜索树 如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
  •  验证其为平衡树 每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子) 节点的平衡因子是否计算正确
//求AVL树的深度(高度)
	int get_hight(node* root)
		if (root == nullptr) return 0;
		int hightleft = get_hight(root->left);
		int hightright = get_hight(root->right);
		int hight = hightleft > hightright ? hightleft + 1 : hightright + 1;
		return hight;
	
//判断是否是AVLTree
	bool isAVLTree(node* root)
		if (nullptr == root) return true;
		int hightleft = get_hight(root->left);
		int hightright = get_hight(root->right);
		int bf = hightright - hightleft;
		if (abs(root->bf) > 1 || bf != root->bf)//当平衡因子的绝对值大于等于2,或者在更新平衡因子时出错,都返回false
			cout << root->data << ":" << bf << "--" << root->bf << endl;
			return false;
		
		return isAVLTree(root->left) && isAVLTree(root->right);//在进行检测他的左右子树
		
	

三.AVL树的性能

AVL树是一棵绝对平衡的二叉搜索树,其要求每个节点的左右子树高度差的绝对值都不超过1,这样可以保证查询时高效的时间复杂度,即log2N 。但是如果要对AVL树做一些结构修改的操作,性能非常低下,比如:插入时要维护其绝对平衡,旋转的次数比较多,更差的是在删除时,有可能一直要让旋转持续到根的位置。 因此:如果需要一种查询高效且有序的数据结构,而且数据的个数为静态的(即不会改变),可以考虑AVL树, 但一个结构经常修改,就不太适合。

以上是关于详解AVL树(平衡二叉树)的主要内容,如果未能解决你的问题,请参考以下文章

什么是《平衡二叉树》

什么是平衡二叉树

平衡二叉树详解——PHP代码实现

PHP代码实现平衡二叉树详解

AVL树平衡旋转详解

平衡二叉树(AVL树)