AVL树详解

Posted 北川_

tags:

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

目录

AVL树的概念

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

  • 它的左右子树都是AVL树
  • 左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)
    如下图是一棵AVL树:

如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在O( log ⁡ 2 N \\log_2N log2N) ,搜索时间复杂度O( log ⁡ 2 N \\log_2N log2N) )。

AVL树节点的定义

template<class K, class V>
struct AVLTreeNode

	AVLTreeNode<K, V>* _left;	// 该节点的左孩子
	AVLTreeNode<K, V>* _right;	// 该节点的右孩子
	AVLTreeNode<K, V>* _parent;		// 该节点的双亲

	pair<K, V> _kv;		// 存储数据的键值对
	int _bf;			// 平衡因子(balance factor)
	AVLTreeNode(const pair<K, V>& kv)
		:_left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _kv(kv)
		, _bf(0)
	
;

每个节点平衡因子的计算为其右子树的高度减去左子树的高度。
一个新节点既没有左子树也没有右子树,可以将它左右子树高度看作0,所以在构造函数中将新节点的平衡因子_bf初始化为0。

AVL树如何高度平衡?

AVL树是高度平衡的二叉搜索树,其左右子树高度之差不超过一。通过平衡因子_bf来记录左右子树的高度差,也就是说每个节点的平衡因子只有三种取值(-1/0/1),平衡因子的计算为右子树的高度减去左子树的高度,平衡因子为-1说明左子树比右子树高1,0说明左右子树一样高,1说明右子树比左子树高1。
当平衡因子的高度更新为2或-2时,说明左右子树的高度差为2,此时AVL树不再平衡,那么如何控制它平衡呢?
通过以下四种旋转

右单旋

新节点插入较高左子树的左侧:右单旋
下面是抽象图:

在插入前,AVL树是平衡的,新节点插入到30的左子树中,30左子树增加了一层,导致以60为根的二叉树不平衡,通过右单旋使AVL树重新达到平衡。
将30的右子树作60的左子树,再将60作为30的右子树,这样就完成了右单旋。根据二叉搜索树的性质,30的右子树subLR都比30大,而subLR都比60小,所以先让subLR作60的左子树,再让60作30的右子树不会违反二叉搜索树的性质。
在旋转完成后更新节点的平衡因子即可。新插入的节点只会影响它父节点的平衡因子,以及它父节点的父节点的平衡因子…,此图中平衡因子受影响的节点为subL和parent,右单旋旋转完成后,parent的左右子树高度相同,subL的左右子树高度相同,它们的平衡因子都被改为0。
在旋转过程中,有以下几种情况需要考虑:
1.30节点的右孩子可能存在,也可能不存在
2. 60可能是根节点,也可能是子树
如果是根节点,旋转完成后,要更新根节点
如果是子树,可能是某个节点的左子树,也可能是右子树
需要更新30的父节点保证树的结构不会被破坏
右单旋具象图:
在AVL树中新插入节点5,AVL树不再平衡,通过右单旋使AVL树重新达到平衡。

右单旋代码:
由于节点采用三叉链结构,所以也要更改_parent指针。

void RotateR(Node* parent)

	Node* subL = parent->_left;		// 父节点左孩子
	Node* subLR = subL->_right;		// 父节点左孩子的右孩子
	parent->_left = subLR;
	if (subLR)	// subRL可能为空
		subLR->_parent = parent;
	subL->_right = parent;
	Node* parentParent = parent->_parent;
	parent->_parent = subL;
	if (parent == _root)	// 父节点为空节点
	
		_root = subL;
		_root->_parent = nullptr;
	
	else			// 父节点不是空节点
	
		if (parentParent->_left == parent)
			parentParent->_left = subL;
		else
			parentParent->_right = subL;
		subL->_parent = parentParent;
	
	subL->_bf = parent->_bf = 0;	// 更新平衡因子

左单旋

新节点插入较高右子树的右侧:左单旋
左单旋抽象图:

左单旋的旋转可以参考右单旋,将subRL作为30的右子树,再将30作为60的左子树。
30右子树都比30大,所以subRL可以做30的右子树,subRL和30以及30的左子树都比30小,可以做60的左子树,这样旋转不会违反二叉搜索树的性质。
蓝色的新增节点会影响subR和parent的平衡因子,左单旋AVL树平衡后,subR和parent的左右子树高度相同,它们的平衡因子改为0。
仍然需要注意的是subRL可能为空,parent可能为根节点,也可能不是根节点,在实现代码时要考虑以上情况。
左单旋具象图:
在AVL树中新插入节点90,AVL树不再平衡,通过右单旋使AVL树重新达到平衡。

左单旋代码:

void RotateL(Node* parent)

	Node* subR = parent->_right;	// 父节点的右孩子
	Node* subRL = subR->_left;		// 父节点右孩子的左孩子
	parent->_right = subRL;
	if (subRL)		// subRL可能为空
		subRL->_parent = parent;
	subR->_left = parent;
	Node* parentParent = parent->_parent;
	parent->_parent = subR;
	if (parent == _root)	// 父亲是根节点
	
		_root = subR;
		_root->_parent = nullptr;
	
	else		// 父亲不是根节点
	
		if (parentParent->_left == parent)
			parentParent->_left = subR;
		else
			parentParent->_right = subR;
		subR->_parent = parentParent;
	
	// 更新平衡因子
	subR->_bf = parent->_bf = 0;

左右双旋

新节点插入较高左子树的右侧:先左单旋再右单旋
左右双旋抽象图:

新增节点在60的左子树上,将双旋变成单旋后再旋转,即:先对30进行左单旋,然后再对90进行右单旋,,简单来说就是60的左子树做30的右子树,60的右子树做90的左子树,然后30和90做60的左右子树,60成为树的根。
此时平衡因子的更新分为三种情况:
第一种情况就是上图新增节点在60的左子树,60的平衡因子变为-1,此时60的左子树比右子树高1,把60高的左子树做30的右子树,30左右子树高度相同,平衡因子更新为0,把60低的右子树做90的左子树,90的平衡因子为h - (h - 1),平衡因子为1,把30和90分别作为60的左右子树,此时60的左右子树高度相同,60的平衡因子更新为0。
第二种情况新增节点在60的右子树,

60的平衡因子变为1,此时60的右子树比左子树高1,把60低的左子树做30的右子树,30的平衡因子更新为(h - 1) - h 为-1,把60高的右子树作为90的左子树,90的平衡因子更新为h - h = 0,60为新的根节点,其左右子树高度相同,平衡因子更新为0。
第三种情况60就是新增节点

此时三个节点的平衡因子都为0。
左右双旋代码:

void RotateLR(Node* parent)

	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	int bf = subLR->_bf;
	RotateL(parent->_left);		// 以父亲的左孩子为根左单旋
	RotateR(parent);        // 以父亲为根右单旋
	// 更新平衡因子,分别对应上面的三种情况
	if (bf == 1)		
	
		subL->_bf = -1;
		parent->_bf = 0;
		subLR->_bf = 0;
	
	else if (bf == -1)
	
		subLR->_bf = 0;
		parent->_bf = 1;
		subLR->_bf = 0;
	
	else if (bf == 0)
	
		subL->_bf = 0;
		parent->_bf = 0;
		subLR->_bf = 0;
	
	else
	
		// 平衡因子出错
		assert(false);
	

右左双旋

新节点插入较高右子树的左侧:先右单旋再左单旋
右左双旋抽象图:

将双旋变成单旋后再旋转,即:先对90进行右单旋,然后再对30进行左单旋,旋转完成后再考虑平衡因子的更新。
右左双旋也分为三种情况:
第一种情况如上如图新增节点在60的右子树,此时60的平衡因子为1。旋转完成后30的平衡因子变为(h - 1) - h,为-1,90的平衡因子变为h - h 为0,60的平衡因子为(h + 1) - (h + 1)为0。
第二种情况新增节点在60的左子树

此时新增节点在60的左子树,60的左子树比右子树高1,60的平衡因子为-1,把60的左子树做30的右子树,30的平衡因子变为h - h为0,60的右子树做90的左子树,90的平衡因子更新为h - (h - 1)为1,60的左右子树高度相等,平衡因子更新为0。
第三种情况60就是新增节点

此时三个节点左右子树高度都相同,平衡因子都为0。
右左双旋代码:

void RotateRL(Node* parent)

	Node* subR = parent->_right;	// 父节点的右孩子
	Node* subRL = subR->_left;	//父节点右孩子的左孩子
	int bf = subRL->_bf;	
	RotateR(parent->_right);	// 以父亲的右子树为根右旋
	RotateL(parent);		// 以父节点为根左旋
	// 三种情况更新平衡因子
	if (bf == 1)
	
		subR->_bf = 0;
		parent->_bf = -1;
		subRL->_bf = 0;
	
	else if (bf == -1)
	
		subR->_bf = 1;
		parent->_bf = 0;
		subRL->_bf = 0;
	
	else if(bf == 0)
	
		subR->_bf = 0;
		parent->_bf = 0;
		subRL->_bf = 0;
	
	else
	
		// 平衡因子出错
		assert(false);
	

当AVL树不平衡时通过以上四种旋转,使AVL树保持平衡。

AVL树的插入

先找到应插入节点的位置,插入后更新平衡因子,如果不平衡了通过旋转使AVL树重新平衡。
插入控制平衡更新原则
1.新增节点在parent右边,parent->bf++
2.新增节点在parent左边,parent->bf- -
a.如果parent的平衡因子等于1 or -1 继续往上更新 // 说明parent所在子树的高度变了
b.如果parent的平衡因子等于0 停止更新 // 高度不变
c.如果parent的平衡因子等于2 or -2 已经出现不平衡,需要旋转处理
AVL树插入节点代码:

bool Insert(const pair<K, V>& kv)

	if (_root == nullptr)
	
		_root = new Node(kv);
		return true;
	
	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)		// 找到应插入节点的位置
	
		if (cur->_kv.first < kv.first)
		
			parent = cur;
			cur = cur->_right;
		
		else if (cur->_kv.first > kv.first)
		
			parent = cur;
			cur = cur->_left;
		
		else
		
			return false;
		
	
	cur = new Node(kv);
	if (parent->_kv.first < kv.first)
	
		parent->_right = cur;
		cur->_parent = parent;
	
	else
	
		parent->_left = cur;
		cur->_parent = parent;
	
	// 控制平衡
	while (cur != _root)
	
		if (cur == parent->_right)
		
			parent->_bf++;
		
		else
		
			parent->_bf--;
		
		if (parent->_bf == 0)
		
			break;
		
		else if (parent->_bf == 1 || parent->_bf == -1)
		
			// parent所在的子树高度变了,会影响parent->parent
			// 继续往上更新
			cur = parent;
			parent = parent->_parent;
           
		else if (parent->_bf == 2 || parent->_bf == -2)
		
			// parent所在子树已经不平衡,需要旋转处理
			if (parent->_bf == -2)
			
				if (cur->_bf == -1)
				
					RotateR(parent);	// 右单旋
				
				else    // cur->_bf == 1
				
					RotateLR(parent);	// 左右双旋
				
			
			else    // parent->_bf == 2
			
				if (cur->_bf == 1)
				
					RotateL(parent);	// 左单旋
				
				else    // cur->_bf == -1
				
					RotateRL(parent);	// 右左双旋
				
			
			break;
		
		else
		
			// 插入节点之前,树已经不平衡了,或者bf出错,需要检查其他逻辑
			assert(false);
		
	
	return true;

AVL树的查找

根据二叉搜索树的性质进行查找

Node* Find(const K& key)

	Node* cur = _root;
	while (cur)
	
		if (cur->_kv.first < key)
			cur = cur->_right;
		else if (cur->_kv.first > key)
			cur = cur->_left;
		else
			return cur;
	
	return nullptr;

AVL树的性能

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

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

AVL树平衡旋转详解

[C/C++]详解STL容器6--AVL树的介绍及部分模拟实现

[C/C++]详解STL容器4--AVL树的介绍及部分模拟实现

[C/C++]详解STL容器6--AVL树的介绍及部分模拟实现

数据结构 - 从二叉搜索树说到AVL树之二叉搜索树的操作与详解(Java)

平衡树AVL树