当前位置: 首页 > news >正文

AVL(平衡二叉树)

目录

1. AVL树的概念

2. AVL树节点定义

3. AVL树的插入

4. AVL树的旋转

4.1 左单旋

 4.2 右单旋

4.3 左右双旋 

 4.4 右左双旋

 5. AVL树的验证

 6. AVL树的查找

7. AVL树的性能


1. AVL树的概念

二叉搜索树虽可以缩短查找的效率,但如果数据有序或接近有序二叉搜索树将退化为单支树,查 找元素相当于在顺序表中搜索元素,效率低下。因此,两位俄罗斯的数学家G.M.Adelson-Velskii 和E.M.Landis在1962年 发明了一种解决上述问题的方法:

当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1(超过1需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。

AVL树可以是一棵空树,也可以是具有以下性质的一棵二叉搜索树:

  • 树的左右子树都是AVL树。
  • 树的左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)。


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

注意: 这里所说的二叉搜索树的高度是平衡的是指,树中每个结点左右子树高度之差的绝对值不超过1(右子树高度减左子树的高度),因为只有满二叉树才能做到每个结点左右子树高度之差均为0。

2. AVL树节点定义

我们这里直接实现KV模型的AVL树(KV就是键值对),为了方便后续的操作,这里将AVL树中的结点定义为三叉链结构,并在每个结点当中引入平衡因子(右子树高度-左子树高度)。除此之外,还需编写一个构造新结点的构造函数,由于新构造结点的左右子树均为空树,于是将新构造结点的平衡因子初始设置为0即可。

template<class K, class V>
struct AVLTreeNode
{
	//存储键值对
	pair<K, V> _kv;
	//三叉链
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;
	//平衡因子(右子树高度-左子树高度)
	int _bf; // balance factor

	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		,_bf(0)
	{}
};

3. AVL树的插入

AVL树插入结点时有以下三个步骤:

  1. 按照二叉搜索树的插入方法,找到待插入位置。
  2. 找到待插入位置后,将待插入结点插入到树中。
  3. 更新平衡因子,如果出现不平衡,则需要进行旋转。

因为AVL树本身就是一棵二叉搜索树,因此寻找结点的插入位置是非常简单的,按照二叉搜索树的插入规则:

  1. 待插入结点的key值比当前结点小就插入到该结点的左子树。
  2. 待插入结点的key值比当前结点大就插入到该结点的右子树。
  3. 待插入结点的key值与当前结点的key值相等就插入失败。

如此进行下去,直到找到与待插入结点的key值相同的结点判定为插入失败,或者最终走到空树位置进行结点插入。

与二叉搜索树插入结点不同的是,AVL树插入结点后需要更新树中结点的平衡因子,因为插入新结点后可能会影响树中某些结点的平衡因子。

由于一个结点的平衡因子是否需要更新,是取决于该结点的左右子树的高度是否发生了变化,因此插入一个结点后,该结点的祖先结点的平衡因子可能需要更新。

所以我们插入结点后需要倒着往上更新平衡因子,更新规则如下:

  1. 新增结点在parent的右边,parent的平衡因子++

  2. 新增结点在parent的左边,parent的平衡因子−−

每更新完一个结点的平衡因子后,都需要进行以下判断:

  • 如果parent的平衡因子等于-1或者1,表明还需要继续往上更新平衡因子。
  • 如果parent的平衡因子等于0,表明无需继续往上更新平衡因子了。
  • 如果parent的平衡因子等于-2或者2,表明此时以parent结点为根结点的子树已经不平衡了,需要进行旋转处理。

判断理由说明:

parent更新后的平衡因子分析
-1或1

只有0经过−−/++操作后会变成-1/1,说明新结点的插入使得parent的左子树或右子树增高了,即改变了以parent为根结点的子树的高度,从而会影响parent的父结点的平衡因子,因此需要继续往上更新平衡因子。

0

只有-1/1经过++/−−操作后会变成0,说明新结点插入到了parent左右子树当中高度较矮的一棵子树,插入后使得parent左右子树的高度相等了,此操作并没有改变以parent为根结点的子树的高度,从而不会影响parent的父结点的平衡因子,因此无需继续往上更新平衡因子。

-2或2此时parent结点的左右子树高度之差的绝对值已经超过1了,不满足AVL树的要求,因此需要进行旋转处理。
注意: parent的平衡因子在更新前只可能是-1/0/1(AVL树中每个结点的左右子树高度之差的绝对值不超过1)。


说明一下: 由于我们插入结点后需要倒着往上进行平衡因子的更新,所以我们将AVL树结点的结构设置为了三叉链结构,这样我们就可以通过父指针找到其父结点,进而对其平衡因子进行更新。

若是在更新平衡因子的过程当中,出现了平衡因子为-2/2的结点,这时我们需要对以该结点为根结点的树进行旋转处理,而旋转处理分为四种,在进行分类之前我们首先需要进行以下分析:

我们将插入结点称为cur,将其父结点称为parent,那么我们更新平衡因子时第一个更新的就是parent结点的平衡因子,更新完parent结点的平衡因子后,若是需要继续往上进行平衡因子的更新,那么我们必定要执行以下逻辑:

cur = parent;
parent = parent->_parent;

这里我想说明的是:当parent的平衡因子为-2/2时,cur的平衡因子必定是-1/1而不会是0。
理由如下:
若cur的平衡因子是0,那么cur一定是新增结点,而不是上一次更新平衡因子时的parent,否则在上一次更新平衡因子时,会因为parent的平衡因子为0而停止继续往上更新。
而cur是新增结点的话,其父结点的平衡因子更新后一定是-1/0/1,而不可能是-2/2,因为新增结点最终会插入到一个空树当中,在新增结点插入前,其父结点的状态有以下两种可能:

  1. 其父结点是一个左右子树均为空的叶子结点,其平衡因子是0,新增结点插入后其平衡因子更新为-1/1。(此时cur = parent,parent等于parent->_parent,继续向上判断平衡因子)
  2. 其父结点是一个左子树或右子树为空的结点,其平衡因子是-1/1,新增结点插入到其父结点的空子树当中,使得其父结点左右子树当中较矮的一棵子树增高了,新增结点后其平衡因子更新为0。(更新为0就不继续往上判断祖先的平衡因子了)

综上所述,当parent的平衡因子为-2/2时,cur的平衡因子必定是-1/1而不会是0。

根据此结论,我们可以将旋转处理分为以下四类:

  1. 当parent的平衡因子为-2,cur的平衡因子为-1时,进行右单旋。(单纯左边高)
  2.  当parent的平衡因子为2,cur的平衡因子为1时,进行左单旋。(单纯右边高)
  3. 当parent的平衡因子为-2,cur的平衡因子为1时,进行左右双旋。(整体左边高,子树右边高)
  4. 当parent的平衡因子为2,cur的平衡因子为-1时,进行右左双旋。(整体右边高,子树左边高)

并且,在进行旋转处理后就无需继续往上更新平衡因子了,因为旋转后树的高度变为插入之前了,即树的高度没有发生变化,也就不会影响其父结点的平衡因子了。具体原因请看后面的旋转讲解。

总结:同号单旋,异号双旋

bool Insert(const pair<K, V>& kv)
{
	//当AVL是空数,第一次插入
	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;
	}
	else
	{
		parent->_left = cur;
	}
	cur->_parent = parent;

	// 更新平衡因子
	while (parent)
	{
		//插入元素在父亲左边
		if (cur == parent->_left)
			parent->_bf--;
		else
			//插入元素在父亲右边
			parent->_bf++;

		if (parent->_bf == 0)
		{
			//更新结束(新增结点把parent左右子树矮的那一边增高了,此时左右高度一致)
			//parent树的高度没有发生变化,不会影响其父结点及以上结点的平衡因子
			break;
		}
		else if (parent->_bf == 1 || parent->_bf == -1)
		{
			// 继续往上更新
			cur = parent;
			parent = parent->_parent;
		}
		else if (parent->_bf == 2 || parent->_bf == -2)
		{
			// 不平衡了,旋转处理
			if (parent->_bf == 2 && cur->_bf == 1)
			{
				RotateL(parent);
			}
			else if (parent->_bf == -2 && cur->_bf == -1)
			{
				RotateR(parent);
			}
			else if (parent->_bf == 2 && cur->_bf == -1)
			{
				RotateRL(parent);
			}
			else
			{
				RotateLR(parent);
			}
			//旋转后就一定平衡了,无需继续往上更新平衡因子(旋转后树高度变为插入之前了)
			break;
		}
		else
		{
			//在插入前树的平衡因子就有问题
			assert(false);
		}
	}

	return true;
}

4. AVL树的旋转

4.1 左单旋

旋转示意图如下:

左单旋的步骤如下:

  1. 让subR的左子树作为parent的右子树。
  2. 让parent作为subR的左子树。
  3. 让subR作为整个子树的根。
  4. 更新平衡因子。

左单旋后满足二叉搜索树的性质:

  1. subR的左子树当中结点的值本身就比parent的值大,因此可以作为parent的右子树。
  2. parent及其左子树当中结点的值本身就比subR的值小,因此可以作为subR的左子树。

可以看到,经过左单旋后,树的高度变为插入之前了,即树的高度没有发生变化,所以左单旋后无需继续往上更新平衡因子。

代码如下:

//左旋
void RotatrL(Node* parent)
{
	Node* parentParent = parent->_parent;
	Node* subR = parent->_right;
	Node* subRl = subR->_left;
	parent->_right = subRl;
	subR->_left = parent;
	//subRl可能为空,因此需要判断,不能直接更新其内保存的父节点
	if (subRl)
	{
		subRl->_parent = parent;
	}
			
	parent->_parent = subR;
	//建立parentParent和subR之间的关系
	if (parentParent)
	{
		if (parentParent->_left == parent)
		{
			parentParent->_left = subR;
		}
		else
		{
			parentParent->_right = subR;
		}
		subR->_parent = parentParent;
	}
	else
	{
		_root = subR;
	}
	//旋转要修改parent和subR的平衡因子
	parent->_bf = 0;
	subR->_bf = 0;
}

注意: 结点是三叉链结构,改变结点关系时需要跟着改变父指针的指向。

 4.2 右单旋

右单旋的步骤如下:

  1. 让subL的右子树作为parent的左子树。
  2. 让parent作为subL的右子树。
  3. 让subL作为整个子树的根。
  4. 更新平衡因子。

右单旋后满足二叉搜索树的性质:

  1. subL的右子树当中结点的值本身就比parent的值小,因此可以作为parent的左子树。
  2. parent及其右子树当中结点的值本身就比subL的值大,因此可以作为subL的右子树。

平衡因子更新如下:

可以看到,经过右单旋后,树的高度变为插入之前了,即树的高度没有发生变化,所以右单旋后无需继续往上更新平衡因子。

代码如下:

//右旋
void RotatrR(Node* parent)
{
	Node* parentParent = parent->_parent;
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	parent->_left = subLR;
	subL->_right = parent;
	if (subLR)
	{
		subLR->_parent = parent;
	}
	parent->_parent = subL;
	if (parentParent == nullptr)
	{
		subL->_parent = nullptr;
		_root = subL;
	}
	else
	{
		if (parentParent->_left == parent)
		{
			parentParent->_left = subL;
		}
		else
		{
			parentParent->_right = subL;
		}
		subL->_parent = parentParent;
	}
	subL->_bf = parent->_bf = 0;
}

注意: 结点是三叉链结构,改变结点关系时需要跟着改变父指针的指向。

4.3 左右双旋 

左右双旋:树整体左边高,但左侧树的子树部分是右边高(所以先对子树进行左旋在对整颗树进行右旋)

1、插入新结点

2、以30为旋转点进行左单旋。

3、以90为旋转点进行右单旋。

左右双旋的步骤如下:

  1. 以subL为旋转点进行左单旋。
  2. 以parent为旋转点进行右单旋。
  3. 更新平衡因子。

左右双旋后满足二叉搜索树的性质:

左右双旋后,实际上就是让subLR的左子树和右子树,分别作为subL和parent的右子树和左子树,再让subL和parent分别作为subLR的左右子树,最后让subLR作为整个子树的根(结合图理解)。

  1. subLR的左子树当中的结点本身就比subL的值大,因此可以作为subL的右子树。
  2. subLR的右子树当中的结点本身就比parent的值小,因此可以作为parent的左子树。
  3. 经过步骤1/2后,subL及其子树当中结点的值都就比subLR的值小,而parent及其子树当中结点的值都就比subLR的值大,因此它们可以分别作为subLR的左右子树。

左右双旋后,平衡因子的更新随着subLR原始平衡因子的不同分为以下三种情况:

1、当subLR原始平衡因子是-1时,左右双旋后parent、subL、subLR的平衡因子分别更新为1、0、0

2、当subLR原始平衡因子是1时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、-1、0

1、当subLR原始平衡因子是0时,左右双旋后parent、subL、subLR的平衡因子分别更新为0、0、0

可以看到,经过左右双旋后,树的高度变为插入之前了,即树的高度没有发生变化,所以左右双旋后无需继续往上更新平衡因子

代码如下:

//先左旋在右旋
void RotateLR(Node* parent)
{
	Node* subL = parent->_right;
	Node* subLR = subL->_right;
	//提前记录subLR的平衡因子
	//因为下面双旋后subLR的平衡因子会变
	int bf = subLR->_bf;
	RotatrL(subL);
	RotatrR(parent);
	if (bf == 0)
	{
		parent->_bf = 0;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
	else if (bf == 1)
	{
		subL->_bf = -1;
		subLR->_bf = 0;
		parent->_bf = 0;
	}
	else if (bf == -1)
	{
		parent->_bf = 1;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

 4.4 右左双旋

左右双旋:树整体右边高,但右侧树的子树部分是左边高(所以先对子树进行右旋在对整颗树进行左旋)

动图当中的旋转示意图如下:

1、插入新结点

2、以90为旋转点进行右单旋。

3、以30为旋转点进行左单旋。

右左双旋的步骤如下:

  1. 以subR为旋转点进行右单旋。
  2. 以parent为旋转点进行左单旋。
  3. 更新平衡因子。

右左双旋后满足二叉搜索树的性质:

右左双旋后,实际上就是让subRL的左子树和右子树,分别作为parent和subR的右子树和左子树,再让parent和subR分别作为subRL的左右子树,最后让subRL作为整个子树的根(结合图理解)。

  1. subRL的左子树当中的结点本身就比parent的值大,因此可以作为parent的右子树。
  2. subRL的右子树当中的结点本身就比subR的值小,因此可以作为subR的左子树。
  3. 经过步骤1/2后,parent及其子树当中结点的值都就比subRL的值小,而subR及其子树当中结点的值都就比subRL的值大,因此它们可以分别作为subRL的左右子树。

右左双旋后,平衡因子的更新随着subLR原始平衡因子的不同分为以下三种情况:

1、当subRL原始平衡因子是1时,左右双旋后parent、subR、subRL的平衡因子分别更新为-1、0、0

2、当subRL原始平衡因子是-1时,左右双旋后parent、subR、subRL的平衡因子分别更新为0、1、0

3、当subRL原始平衡因子是0时,左右双旋后parent、subR、subRL的平衡因子分别更新为0、0、0

可以看到,经过右左双旋后,树的高度变为插入之前了,即树的高度没有发生变化,所以右左双旋后无需继续往上更新平衡因子。

//先右旋在左旋
void RotateRL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	//提前记录subRL的平衡因子
	//因为下面双旋后subRL的平衡因子会变
	int bf = subRL->_bf;
	RotatrR(subR);
	RotatrL(parent);
	if (bf == 0)
	{
		parent->_bf = 0;
		subR->_bf = 0;
		subRL->_bf = 0;
	}
	else if (bf == 1)
	{
		subRL->_bf = 0;
		subR->_bf = 0;
		parent->_bf = -1;
	}
	else if (bf == -1)
	{
		subRL->_bf = 0;
		parent->_bf = 0;
		subR->_bf = 1;
	}
	else
	{
		assert(false);
	}
}

 5. AVL树的验证

AVL树是在二叉搜索树的基础上加入了平衡性的限制,也就是说AVL树也是二叉搜索树,因此我们可以先获取二叉树的中序遍历序列,来判断二叉树是否为二叉搜索树。

代码如下:

//中序遍历
void Inorder()
{
	_Inorder(_root);
}
//中序遍历子函数
void _Inorder(Node* root)
{
	if (root == nullptr)
		return;
	_Inorder(root->_left);
	cout << root->_kv.first << " ";
	_Inorder(root->_right);
}

 _Inorder在类中要用private修饰,这样类外对象只能调用Inorder,而为什么要这样做呢?

因为_root在类中是被private修饰的,所以我们这边其实就是套了一层壳

但中序有序只能证明是二叉搜索树,要证明二叉树是AVL树还需验证二叉树的平衡性,在该过程中我们可以顺便检查每个结点当中平衡因子是否正确。

采用后序遍历,变量步骤如下:

  1. 从叶子结点处开始计算每课子树的高度。(每棵子树的高度 = 左右子树中高度的较大值 + 1)
  2. 先判断左子树是否是平衡二叉树。
  3. 再判断右子树是否是平衡二叉树。
  4. 若左右子树均为平衡二叉树,则返回当前子树的高度给上一层,继续判断上一层的子树是否是平衡二叉树,直到判断到根为止。(若判断过程中,某一棵子树不是平衡二叉树,则该树也就不是平衡二叉树了)

代码如下:

bool IsBalanceTree()
{
	return _IsBalanceTree(_root);
}

int _Height(Node* root)
{
	if (root == nullptr)
		return 0;
	int leftHeight = _Height(root->_left);
	int rightHeight = _Height(root->_right);
	return leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
}

bool _IsBalanceTree(Node* root)
{
	if (root == nullptr)
		return true;
	//计算左右子树的高度
	int leftHeight = _Height(root->_left);
	int rightHeight = _Height(root->_right);
	int diff = rightHeight - leftHeight;
	if (diff > 1 || diff < -1)
	{
		cout << root->_kv.first <<"高度出问题";
				
	}

	if (diff != root->_bf)
	{
		cout << root->_kv.first << "平衡因子出问题";
		cout << endl;
	}
	return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}
Node* _root = nullptr; 
};

 6. AVL树的查找

AVL树的查找函数与二叉搜索树的查找方式一模一样,逻辑如下:

  1. 若树为空树,则查找失败,返回nullptr。
  2. 若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。
  3. 若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。
  4. 若key值等于当前结点的值,则查找成功,返回对应结点。

代码如下:

//查找函数
Node* Find(const K& key)
{
	Node* cur = _root;
	while (cur)
	{
		if (key < cur->_kv.first) //key值小于该结点的值
		{
			cur = cur->_left; //在该结点的左子树当中查找
		}
		else if (key > cur->_kv.first) //key值大于该结点的值
		{
			cur = cur->_right; //在该结点的右子树当中查找
		}
		else //找到了目标结点
		{
			return cur; //返回该结点
		}
	}
	return nullptr; //查找失败
}

7. AVL树的性能

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

    相关文章:

  1. 【前端】 el-form-item的label由于字数多自行换行调整
  2. 常考计算机操作系统面试习题(二)(下)
  3. Spring Boot深度解析:从核心原理到最佳实践
  4. C语言字符函数,字符串函数以及内存函数
  5. 腾讯云大模型知识引擎x deepseek:打造智能服装搭配新体验
  6. Kubernetes 故障排查指南
  7. Linux启动之__vet_atags
  8. 23种设计模式-外观(Facade)设计模式
  9. unix网络编程
  10. annoy编译安装问题及解决
  11. 嵌入式八股文学习笔记——C++学习笔记面向对象相关
  12. Python第九章节——异常,模块与包
  13. leetcode128.最长连续序列
  14. Objects.equals() 和 Object.equals() 的区别:
  15. 信号处理中的窗
  16. 《Python实战进阶》第30集:Scikit-learn 入门:分类与回归模型
  17. Pytest的参数化测试
  18. Xamarin.Android实现本地化部署DeepSeek的对话功能
  19. 【docker】安装SQLServer
  20. DrRacket是一款专为Scheme和Racket编程语言设计的集成开发环境(IDE)
  21. 结束北京队与总决赛十年之痒的,为何会是一度被群嘲的许利民
  22. AI世界的年轻人|他用影像大模型解决看病难题,“要做的研究还有很多”
  23. 国际著名学者Charles M. Lieber全职受聘清华深圳国际研究生院
  24. 受天气等影响SC8041航班三次备降延误超12小时,山航致歉
  25. 山东省委组织部办公室主任吴宪利已任德州市委常委、组织部部长
  26. 车展之战:国产狂飙、外资反扑、智驾变辅助