AVL树详解

一、概念

  • 当向二叉搜索树中插入新结点后,如果能保证每个结点的左右子树高度之差的绝对值不超过1,可能需要对树中结点的位置进行调整,即可降低树的高度,从而减少平均搜索长度。则这棵二叉搜索树就是AVL树。
  • 一棵AVL树有两种情况,一种是空树,另一种是具有两种性质的二叉搜索树。即它的左右子树都是AVL树和左右子树高度之差(简称为平衡因子)的绝对值不超过1,即可能是-1、0、1的其中一个。
  • AVL树不一定有平衡因子,使用平衡因子只是它的一种实现方式。

二、图示

在这里插入图片描述

  • 圆圈上的数字为节点的平衡因子。

三、结构定义

1、介绍

  • 为了使AVL树的实现简单化,本文对该树的实现使用三叉树的结构,即有父母节点、左节点和右节点。
  • 为了使旋转简单化,使用平衡因子,即下方代码中的_bf成员变量就是对应节点的平衡因子。
  • 对AVL树相关操作的函数写在AVLTree类public限定符下。
  • 为了在编写对应的代码时方便,将AVLTreeNode< T >(节点)类型重定义为Node。
template<class T>
struct AVLTreeNode
{
	AVLTreeNode(const T& data = T())
		: _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _data(data)
		, _bf(0)
	{}

	AVLTreeNode<T>* _left;
	AVLTreeNode<T>* _right;
	AVLTreeNode<T>* _parent;
	T _data;
	int _bf;
};

template<class T>
class AVLTree
{
	typedef AVLTreeNode<T> Node;
public:
	AVLTree()
		: _root(nullptr)
	{}
private:
	Node* _root;
};

四、旋转

1、概念与种类

  • 如果在一棵原本是平衡的AVL树中插入一个新节点,这课AVL树可能会变得不平衡,此时必须调整树的结构,使之平衡,而旋转就可以达到这样的效果。
  • 根据节点插入位置的不同,AVL树的旋转分为四种,分别是左单旋、右单旋、左右双旋和右左双旋。

2、原则与目的

  • 旋转的原则是保持它的结构是搜索树。
  • 旋转的目的是左右均衡,降低整棵树的高度。

五、右单旋

1、操作

  • 当新节点插入到节点较高的左子树的左侧时,节点的平衡因子就会从-1变为-2,导致AVL不平衡。
  • 右单旋时,将节点的左子树高度减少一层,右子树高度增加一层,即将节点的左子树节点往上提,节点右转下来。
  • 因为节点的值比它左子树节点的值大,只能将其放在其左子树节点的右子树之上,而如果左子树节点有右子树,这个右子树节点的值一定大于上方所说的左子树节点的值,小于节点的值,只能将其放在节点的左子树上,旋转完成后,更新两个节点的平衡因子即可。
  • 在旋转过程中,需要注意虑以下情况。
  • 左子树节点的右子节点可能存在,也可能不存在。当它不存在时,不能访问它的父节点。
  • 旋转的节点可能是根节点,也可能是子树。如果是根节点,旋转完成后,要将其更新为根节点;如果是子树,可能是某个节点的左子树,也可能是右子树。

2、示意图

在这里插入图片描述

3、代码

void RotateR(Node* parent)
{
	Node* sub = parent->_left;
	Node* subR = sub->_right;
	Node* pParent = parent->_parent;
	parent->_left = subR;
	if (subR)
		subR->_parent = parent;
	sub->_right = parent;
	parent->_parent = sub;

	sub->_parent = pParent;

	if (pParent == nullptr)
	{
		_root = sub;
	}
	else
	{
		if (pParent->_left == parent)
			pParent->_left = sub;
		else
			pParent->_right = sub;
	}

	sub->_bf = parent->_bf = 0;
}

4、注意

  • 无论如何,sub的父节点都需要更改,而不是当pParent不为空时才需要修改,否则会形成环路问题。
  • pParent与sub的父子关系要联系全。

六、左单旋

1、操作

  • 左单旋的操作与右单旋的操作类似,只是旋转的方向相反,节点的位置也是相反的,还有插入新节点后,其中两个平衡因子变化的节点的平衡因子是正数的。

2、示意图

在这里插入图片描述

3、代码

void RotateL(Node* parent)
{
	Node* sub = parent->_right;
	Node* subL = sub->_left;
	Node* pParent = parent->_parent;
	parent->_right = subL;
	if (subL)
		subL->_parent = parent;
	sub->_left = parent;
	parent->_parent = sub;

	sub->_parent = pParent;

	if (pParent == nullptr)
		_root = sub;
	else
	{
		if (pParent->_left == parent)
			pParent->_left = sub;
		else
			pParent->_right = sub;
	}

	sub->_bf = parent->_bf = 0;
}

七、左右双旋

1、操作

  • 当AVL树的左子树(30)比右子树d高,左子树的右子树(60所在子树)比左子树的左子树(a)高时,需要采用左右双旋的方式降低树的高度以保持树的平衡。
  • 左右双旋的操作为,首先对左子树节点(30)进行左单旋,再对节点(90)进行右单旋。
  • 插入新节点的位置为下方示意图的左子树的右子树(60)节点、b子树与c子树,只有在这些位置插入新节点,才能引发左右双旋。

2、示意图

在这里插入图片描述

3、代码

void RotateLR(Node* parent)
{
	Node* sub = parent->_left;
	Node* subR = sub->_right;
	int bf = subR->_bf;
	RotateL(sub);
	RotateR(parent);	
	if (bf == -1)
		parent->_bf = 1;
	else if (bf == 1)
		sub->_bf = -1;
	else
		assert(false);
}
  • 代码复用了上方的左单旋和右单旋的代码。因为在对应的两个单旋的函数内,会将对应的三个节点的平衡因子赋值为0,所以,如果平衡因子最后为0时,在本函数内不做多余的修改。

八、右左双旋

1、操作

  • 右左双旋的操作与左右双旋的操作类似,只是旋转的方向与节点的位置相反。

2、示意图

在这里插入图片描述

3、代码

void RotateRL(Node* parent)
{
	Node* sub = parent->_right;
	Node* subL = sub->_left;
	int bf = subL->_bf;
	RotateR(sub);
	RotateL(parent);
	if (bf == -1)
		sub->_bf = 1;
	else if (bf == 1)
		parent->_bf = -1;
	else
		assert(false);
}

九、插入节点

1、操作

  • 指针parent无需初始化为_root,因为下面比较时一定会改变parent的值,而parent 是否初始化为AVL树根节点的值都无所谓。
  • 查找插入位置,当查找的值已经存在时,不再插入直接返回false。
  • 当查找到插入位置时,进行插入操作。
  • 最后就是处理平衡因子和旋转问题。

2、代码

bool Insert(const T& data)
{
	if (_root == nullptr)
	{
		_root = new Node(data);
		return true;
	}
	Node* cur = _root;
	Node* parent = nullptr;
	while (cur)
	{
		parent = cur;
		if (cur->_data > data)
			cur = cur->_left;
		else if (cur->_data < data)
			cur = cur->_right;
		else
			return false;
	}
	cur = new Node(data);
	if (parent->_data > data)
		parent->_left = cur;
	else
		parent->_right = cur;

	while (parent)
	{
		if (parent->_left == cur)
			parent->_bf--;
		else
			parent->_bf++;

		if (parent->_bf == 1 || parent->_bf == -1)
		{
			parent = parent->_parent;
			cur = cur->_parent;
		}
		else if (parent->_bf == 0)
		{
			break;
		}
		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)
				RotateLR(parent);
			else if (parent->_bf == 2 && cur->_bf == -1)
				RotateRL(parent);
			else
				assert(false);
			break;
		}
		else
		{
			assert(false);
		}
	}
	return true;
}

3、平衡因子处理

  • 新增节点可能会影响AVL树的高度(祖先节点的平衡因子),如果有影响就需要继续往上更新对应节点的平衡因子。如果子树的高度不变,就不会影响祖先节点,反之则会影响。
  • 本文平衡因子的计算是用右子树的高度减去左子树的高度。所以,在左子树新增节点,对应的父亲节点平衡因子自减一;在右子树新增节点,对应的父亲节点平衡因子自增一。
  • 当更新平衡因子后父亲节点的平衡因子的值为1或者-1时,说明父亲节点的子树高度变化了,必须继续往上更新。即在插入节点前,父亲节点的平衡因子的值0,其两边子树的高度─样高,而新插入的节点使其高度变化了。
  • 当更新平衡因子后父亲节点的平衡因子的值为0时,说明父亲节点的子树高度不变,不用继续往上更新,插入节点的操作完毕。因为在插入节点前,父亲节点的平衡因子的值为1或者-1,其两边子树的高度不一样,新插入的节点填上低的那边,整体高度不变。
  • 当更新平衡因子后父亲节点的平衡因子的值为2或者-2时,说明父亲节点的子树违反了AVL树的规则,需要进行旋转,以达到调整整棵树高度的目的。
  • 如果更新平衡因子后父亲节点的平衡因子的值不是上面的三种情况之一,说明在插入节点前,这棵树已经不是AVL树了。

十、检测

1、计算子树高度

(1)代码

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

(2)实现原理

  • AVL树的高度是由节点的数量决定的,计算则是一层为1,而不是一个节点为1。所以,可以采用递归的方式进行计算。
  • 节点所在的子树的高度是由其左右节点子树的高度决定的,所以,节点的高度为左右子树较高的那一个的高度加上节点的那一层,即加一。

2、检测函数

(1)代码

bool IsAVLTree()
{
	return _IsAVLTree(_root);
}
bool _IsAVLTree(Node* root)
{
	if (root == nullptr)
		return true;
	int leftHeight = _Height(root->_left);
	int rightHeight = _Height(root->_right);
	if (abs(rightHeight - leftHeight) > 1 || rightHeight - leftHeight != root->_bf)
		return false;
	return _IsAVLTree(root->_left) && _IsAVLTree(root->_right);
}

(2)实现原理

  • 由于检测AVL树是否符合规定需要用到根节点_root,而在我编写的代码中,我将这一成员变量放于私有限定符下。所以,在AVLTree类外无法调用该函数,而用一个类内函数封装一下就可以调用了,且不会因为要传参数而变得麻烦。
  • 判断AVL树既需要判断左右子树的高度差,又需要判断对应节点的平衡因子是否设置正确。而这样的判断操作需要对每个节点都检测一遍,这样才能真正确保AVL树的正确性。

本文到这里就结束了,如有错误或者不清楚的地方欢迎评论或者私信
创作不易,如果觉得博主写得不错,请点赞、收藏加关注支持一下💕💕💕

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值