AVL树超详解上

news2024/12/23 3:05:41

前言

        学习过了二叉树以及二叉搜索树后(不了解二叉搜索树的朋友可以先看看这篇博客,二叉搜索树详解-CSDN博客),我们在一般情况下对于二叉搜索树的插入与查询时间复杂度都是O(lgN),是十分快的,但是在一些特殊的情况下,二叉搜索树会退化成类似链表的存在,如下图。此时时间复杂度就恶化到了O(N)。

        为了解决二叉搜索树的恶化,伟大的科学家们发明了AVL树与红黑树,我们今天了解的就是其中一种解决方法AVL树。

AVL树定义

      两位俄罗斯的数学家G.M.Adelson-Velskii 和E.M.Landis在1962年 发明了一种解决上述问题的方法:当向二叉搜索树中插入新结点后,如果能保证每个结点的左右 子树高度之差的绝对值不超过1(需要对树中的结点进行调整),即可降低树的高度,从而减少平均搜索长度。AVL树就是根据他们的名字来命名的。

        AVL树的定义与二叉搜索树类似。

       1.它的左右子树都是AVL树

        2.左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)

        3. AVL树的左边所有非空结点值小于根结点, AVL树的右边所有非空结点值大于根结点

        在每次的插入或者删除后,都尽量保持二叉树的平衡,可以将AVL树看成一种平衡的二叉搜索树。为什么高度差的绝对值要不超过1,而不是等于0呢?原因也十分简单,假如有2个结点,我们不可能高度差为0,只能保持较为平衡的状态,如下图。

        只有当结点个数刚好为2^n+1时才可以完全平衡,如下图。所以为了通用性,不只在结点个数特殊的情况下使用,规定左右子树高度差最大可以为一。

 AVL树判断

        下面我们来看几个事例判断是否是AVL树。

        

        上图是AVL树么?我们首先判断他是不是二叉搜索树,其中序遍历为10 12 15 17,故为二叉搜索树,然后判断有没有平衡。此时就要依据平衡因子。

        为了后序论述的方便我们规定平衡因子为右子树的高度减去左子树的高度(左子树的高度减去右子树的高度也可以,只需要在旋转操作时改下判断)。我们便可以在上面的图上分别标出他们的平衡因子.如下图。

        通过上图我们可以发现以15为根结点的AVL树是平衡的,但以10为根结点的AVL树是不平衡的.其平衡因子的绝对值大于1.所以上面的二叉树不是AVL树。我们根据图像也可以明显的观察出右子树高度比左子树大很多,给人一种不平衡的感觉。

        下面我们再看一个事例

        上述的二叉树是AVL树么?首先依旧是看他的中序遍历结果4 9 10 12 15 17,有序所以为二叉搜索树,接下来看他是否平衡。

        通过标出他们的平衡因子我们发现他们的绝对值都不大于1,所以平衡为AVL树。

   二叉树中序遍历技巧

           第一步是描点,在所有结点下面描个点,如下图

        第二步是描边,用一条线,将二叉搜索树从根结点开始描一圈,如下图

         然后将线上的按照出现的顺序依次写出来1 3 4 6 7 8 10 13 14,这样就可以得到一个二叉树的中序遍历结果了,前序遍历就是在结点左边描点,后序遍历就是在结点右边描点,大家感兴趣可以尝试,在这里就不多赘述了。

AVL树实现

AVL树结点

        通过前面的定义,我们可以知道AVL树就是一种特殊的二叉树,我们就可以充分的利用之前的果实帮助我们完成AVL树。

template<class T>
struct BSTNode
{
	BSTNode(T k= T())
		:_left(nullptr)
		, _right(nullptr)
		, _key(k)
	{
 
	}
 
	BSTNode* _left;
	BSTNode* _right;
	T _key;
};

        二叉搜索树的一个结点定义如上,我们定义了左右指针加上当前节点的值,但AVL树判断平衡要频繁使用平衡因子,我们就可以设置一个平衡因子变量,其次在后面旋转操作的时候,我们要经常访问当前结点的父亲结点,就可以再增加一个指针指向父亲结点。

        这样每一个结点就如下图,便于我们操作。

        于是我们便可以定义出如下结点,和AVL类。这里为了方便将AVLTNode<T>类型取了别名node。

template<class T>
struct AVLTNode
{
	AVLTNode(T k = T())
		:_left(nullptr)
		, _right(nullptr)
		, _key(k)
		, _parent(nullptr)
		,_bf(0)
	{

	}

	AVLTNode* _left;//左子结点
	AVLTNode* _right;//右子结点
	AVLTNode* _parent;//父亲结点
	T _key;//有效值
	int _bf;//平衡因子  balance factor
};
template<class T>
class AVLTree
{
private:
	typedef AVLTNode<T> node;


public:


private:

	node* _root;
};

AVL树构造函数

        这里我们就写几个基础的构造函数,核心是后面的插入,删除操作。

AVLTree()
{
	_root = nullptr;
}
AVLTree(int k)
{
	_root = new node(k);
}

AVL树析构函数

        有了构造函数自然少不了析构函数了。AVL树的内存是用new在堆上开辟出来的,我们必须要些析构函数来主动的释放内存,否则会造成内存泄漏。

        由于AVL树也是二叉树,我们可以采用递归式的销毁,先释放左子树再释放右子树,最后释放当前结点。

        但是析构函数本身不支持传递参数,无法用于递归销毁,我们就可以重新定义个子函数帮助我们销毁。其中子函数最好放在private修饰符下,这样就只可以再类内访问该函数,类外面就使用不了。

	~AVLTree()
	{
		AVLTreeDestory(_root);
		_root = nullptr;
	}


private:
	void AVLTreeDestory(node* _root)
	{
		if (_root == nullptr)
			return;

		AVLTreeDestory(_root->_left);
		AVLTreeDestory(_root->_right);

		delete _root;
	}

AVL树插入

        一种数据结构,就是一种数据存储与管理的方式。我们实现这种数据结构必定离不开四大经典功能,增删查改,不过在AVL树种改边改变key基本不会用,就去掉这种功能。AVL树主要用于数据的查找,时间复杂度为O(lgN),效率十分快。

        接下来我们就来实现数据的插入。

        我们定义插入函数主体如下,返回bool值,如果插入成功就返回true,这次实现的AVL树是不支持相同数字插入的,所以如果插入元素在AVL树中就返回false。

bool insert(T k)
{

}

        在插入前,我们首先要找到插入的位置在哪里。假如我们在下图中插入13

        我们可以将13与根结点的值比较大小,比他大就在根结点的右子树找,比他小就在根结点的左子树找,依次往下遍历找,我们可以发现13最后要插入到12的右子树上。如下图。

bool insert(T k)
{
	//二叉搜索树没有结点
	if (_root == nullptr)
	{
		_root = new node(k);
		return true;
	}

	//找到合适的位置
	node* parent = nullptr;
	node* cur = _root;

	while (cur)
	{
		//提前存下父节点
		parent = cur;

		//k大于结点值就去右边,小于就去左边
		if (cur->_key < k)
			cur = cur->_right;
		else if (cur->_key > k)
			cur = cur->_left;
		//一般的二叉搜索树不允许出现相同的数字
		else
			return false;
	}

	//先判断再插入
	if (parent->_key > k)
	{
		parent->_left = new node(k);	
		cur = parent->_left;
		cur->_parent = parent;
	}
	else
	{
		parent->_right = new node(k);
		cur = parent->_right;
		cur->_parent = parent;
	}

	return true;
}

   平衡因子变化

        由此我们便可以在AVL树中插入一个数,但此时还没有结束,当我们插入一个数的时候,AVL数种节点的平衡因子会发生改变,所以我们要修改平衡因子。变化如下图

        我们发现只有插入点13的父亲一族平衡因子会发生改变,其他的结点不会改变。于是在修改平衡因子的时候我们只需要不断遍历当前结点的父亲结点即可,不需要全部遍历一遍,此时我们在每一个结点中加的父亲指针就大大简化了代码。

        我们可以发现如果插入的结点在12的右边,那么12的平衡因子就加一,如果插入的结点在12的左边,那么12的平衡因子就减一。原因很简单,我们规定平衡因子等于右子树的高度减左子树的高度,在12的那边插入,那边子树的高度就加一,从而造成12的平衡因子变化。

        此时我们需要对于12的平衡因子进行判断

        1.平衡因子为0

        如果12的平衡因子为0,说明此时插入时原来较矮的一边子树,此时以12为根结点的AVL树平衡,就不需要继续往上更新父亲的平衡因子了。因为以12为根结点的AVL树高度无变化,不可能引起父亲的平衡因子变化。例如将上图在加个11结点。

        2.平衡因子为1或-1

        此时我们知道当前结点由原来的平衡,转变为了较不平衡的状态,高度发生了变化,一边高,一边低,此时就需要不断的向上更新父亲的平衡因子了.例如对于12的父亲结点15来说,12为根结点的AVL树高度发生了变化,那么15的平衡因子也需要发生变化。此时15的变化也就可以类似判断12是15的左节点还是右节点,我们规定平衡因子等于右子树的高度减左子树的高度,那么在左结点就减一,右节点就加一。

        上述两种情况实现的代码大致如下。

	//修改平衡因子
	//1.根节点的父亲结点为空,最坏情况走到根结点结束
	//2.插入完成后刚好parent,cur指向需要的位置
	while (parent)
	{
		if (parent->_left == cur)
			parent->_bf--;
		else
			parent->_bf++;

		//没有引起高度变化
		if (parent->_bf == 0)
			break;
		//引起可以接受的高度变化
		else if (parent->_bf == 1 || parent->_bf == -1)
		{
			cur = parent;
			parent = parent->_parent;
		}

	}

        3.平衡因子为2或-2

        此时我们的结点插入在了原本就不平衡的树上,并且还插入到了原来较高的那一边子树上,使AVL树更加的不平衡,违背了AVL树的定义,此时我们就需要进行旋转来改变平衡因子。

        左单旋

        例如我们在下面左图的AVL树中加入18结点,对于17结点来说他是可以接受的不平衡就继续往上更新父亲结点的平衡因子。

        cur结点来自parent的右边,所以15的平衡因子较原来的加一,为1在可接受范围内,就继续往上更新。

        同理可以求的当前的parent的平衡因子为2,cur的平衡因子为1,即以10为根结点的AVL树不平衡。我们此时需要进行旋转操作。

        经过G.M.Adelson-Velskii 和E.M.Landis两位伟大的数学家研究发现,此时只需要将AVL树左旋即可。

        将根节点10从左边向下旋转,成为cur的左子树,将cur原来的左子树改为parent的右子树,此时旋转过后,parent与cur的平衡因子都为0.由此我们便通过一次旋转让原本不平衡的AVL树再次平衡起来。

        注意左单旋的条件是parent的平衡因子为2,cur的平衡因子为1.新结点插入在c子树上

        此时我们就可以,将上述图像抽象出来,更加的理解左单旋。如下图

        旋转之后我们还要判断他是否是AVL树,我们通过图片分析可以知道,a,b,c三个子树没变化,为AVL子树,parent,cur结点的左右子树高度相同,故他们的平衡因子都为0,符合要求。

        接着我们要判断旋转后的树是不是二叉搜索树。此时我们就可以根据定义来看。首先根据未旋转图像我们可以判断出  a<30,   30< b<60,  c>60.然后再旋转后b子树跑到了30节点的右子树上面,满足30右子树大于30条件,此时以30为根结点的二叉树是二叉搜索树,再以60为根结点来看, 以30为根结点的二叉搜索树 所有元素小于60,满足60的左子树所有值小于60,所以综上所述,旋转后的二叉树是AVL树。

        由上图分析我们就可以单独创建一个左旋函数,在这个函数里面对AVL树进行旋转。上述是以根节点为例画图理解,如果parent结点不是根结点,是某段子树与之分析一样,除了以parent为根结点AVL树变化,其他部分不变。在这里就不过多赘述了。()

        最终分析完成后代码的结果如下。

void RotateL(node* parent)
{
	node* subR = parent->_right;
	node* subRL = parent->_right->_left;

	node* parentparent = parent->_parent;
	//parent为根节点特殊处理
	if (parentparent == nullptr)
	{
		subR->_parent = nullptr;
		_root = subR;
	}
	else
	{
		//parent为某段子树
		if (parentparent->_left == parent)
			parentparent->_left = subR;
		else
			parentparent->_right = subR;

		subR->_parent = parent->_parent;
	}

	parent->_right = subRL;
	if (subRL)
		subRL->_parent = parent;

	subR->_left = parent;
	parent->_parent = subR;

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

        于是我们就可以将上面,上面的代码修改如下。直接调用封装好的函数。

	//修改平衡因子
	//1.根节点的父亲结点为空,最坏情况走到根结点结束
	//2.插入完成后刚好parent,cur指向需要的位置
	while (parent)
	{
		if (parent->_left == cur)
			parent->_bf--;
		else
			parent->_bf++;

		//没有引起高度变化
		if (parent->_bf == 0)
			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
		{
			assert(false);
		}
	}
        右单旋

        右单旋与左单旋十分的类似,我们采用左单旋是因为右子树看起来偏大,高度高,同理当左子树高度高时,我们就可以考虑用右单旋。

        下面我们同样看个示例,与上个示例类似,只不过此时我们插入3

        然后不断向上循环判断平衡因子。

        当我们再向上遍历时,此时的parent的平衡因子为-2,我们此时就要对他进行旋转,只有将他旋转为正常的AVL树才可以继续向上遍历。

        我们就可以以9为parent结点开始右旋,如下图。

        将结点9从右边向下旋转,成为cur的右子树,将cur原来的左子树改为parent的左子树(没有就指向空),此时旋转过后,parent与cur的平衡因子都为0.由此我们便通过一次旋转让原本不平衡的AVL树再次平衡起来。

        同样注意右单旋的条件是parent的平衡因子为-2,cur的平衡因子为-1.新结点插入在a子树上

        旋转后4结点的平衡因子为0,9的平衡因子为0.我们可以抽象为如下图(为了方便同样假定parent结点为根结点,像本例parent为某段子树根节点的分析与整个树根结点分析基本相同)

        

        与左旋相同,我们要判断旋转之后他是否是AVL树,我们通过图片分析可以知道,a,b,c三个子树没变化,为AVL子树,parent,cur结点的左右子树高度相同,故他们的平衡因子都为0,符合要求。

        接着我们要判断旋转后的树是不是二叉搜索树。此时我们就可以根据定义来看。首先根据未旋转图像我们可以判断出  a<15,   15< b<30,  c>30.然后再旋转后b子树跑到了30节点的左子树上面,满足30左子树小于30条件,此时以30为根结点的二叉树是二叉搜索树,再以15为根结点来看, 以30为根结点的二叉搜索树 所有元素大于15,满足60的右子树所有值大于15,所以综上所述,旋转后的二叉树是AVL树。

         由上图分析我们就可以单独创建一个右旋函数,在这个函数里面对AVL树进行旋转。经过上述的分析,最终分析完成后代码的结果如下。

void RotateR(node* parent)
{
	node* parentparent = parent->_parent;
	node* subL = parent->_left;
	node* subLR = subL->_right;


	subL->_right = parent;
	if (parentparent == nullptr)
	{
		subL->_parent = nullptr;
		_root = subL;
	}
	else
	{
		subL->_parent = parentparent;
		if (parentparent->_left == parent)
			parentparent->_left = subL;
		else
			parentparent->_right = subL;
	}

	parent->_left = subLR;
	if (subLR)
		subLR->_parent = parent;
	parent->_parent = subL;

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

        那么我们根据平衡因子判断就可以扩充为如下代码。注意我们最后加了个else断言,这是用来测试的,一旦我们的平衡因子出现不是0 1 -1 2 -2的值说明我们之前的处理是有问题的,可以提醒我们修改。

//没有引起高度变化
if (parent->_bf == 0)
	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
{
	assert(false);
}
        右左旋转

        光有上面两个是不够的,我们在实际的插入中还会有其他的情况。

        例如在下图中插入12结点就会导致原本较为平衡的AVL树失去平衡,10结点的平衡因子为2,就需要调整AVL树使其保持平衡。

        此时我们对10进行左旋是解决不了问题的,如下图。旋转后二叉树任然不平衡,此时我们就需要多次旋转,进行右左双旋。

        右左双旋。就是先以15为根结点进行右旋如下图,此时AVL树就变为第一种左单旋的情况,再以10为根结点进行左旋即可。

          同样注意右左双旋的条件是parent的平衡因子为2,cur的平衡因子为-1.新结点插入在b或c子树上

        与上述一样我们可以画出抽象图理解。不过此时相较于之前有些特殊,如下图我们只能推断出a,d子树的高度为h,我们只知道以40为根结点的二叉树高度为h+1,但对于具体b,c的子树高度是不知道的,我们此时就需要分类讨论了。

        假如b的高度为h-1,c的高度为h,则他们的变化如下图(不熟悉旋转操作的可以先看下左右单旋,双旋只不过进行两次单旋罢了。)

        此时我们根据左右子树的高度再更新他们的平衡因子,如下图

         假如b的高度为h,c的高度为h-1经过上述相同的变化如下图.

        注意此时的平衡因子发生了变化,待会写代码时要额外注意。

         假如b的高度为h,c的高度为h,经过上述相同的变化如下图.此时就为刚开始举得示例情况,b,c为空子树

        此时的平衡因子都为0.由此我们便认识完了右左双旋的所有情况,就可以同样封装的函数写了,我们注意到右左双旋实际上就是之前的左单旋和右单旋的组合,就可以复用之前的代码,不过最后要注意平衡因子会发生变化。

        最后代码如下

	void RotateRL(node* parent)
	{
		node* subR = parent->_right;
		node* subRL = subR->_left;
		int bf = subRL->_bf;
		//先右旋
		RotateR(subR);
		//再左旋
		RotateL(parent);

		//更新平衡因子,b,c 子树的高度不同,就可以通过,subRL的平衡因子反应
		if (bf == 0)
		{
			parent->_bf = subR->_bf = subRL->_bf = 0;
		}
		else if (bf == 1)
		{
			parent->_bf = -1;
			subR->_bf = subRL->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = subRL->_bf = 0;
			subR->_bf = 1;
		}
		else
		{
			assert(false);
		}
	}
左右旋转

        有右左双旋,自然也有左右双旋, 例如在下图中插入9结点就会导致原本较为平衡的AVL树失去平衡,10结点的平衡因子为-2,就需要调整AVL树使其保持平衡。

        同样我们对他进行右旋是解决不了问题的。

        我们要对他进行两次旋转,第一次以8为根结点左旋,第二次以10为根结点右旋

        

        同样注意左右双旋的条件是parent的平衡因子为-2,cur的平衡因子为1.新结点插入在b或c子树上

        与上述一样我们可以画出抽象图理解。不过此时相较于之前有些特殊,如下图我们只能推断出a,d子树的高度为h,我们只知道以40为根结点的二叉树高度为h+1,但对于具体b,c的子树高度是不知道的,我们此时就需要分类讨论了。与右左双旋的情况十分相似。

         假如b的高度为h,c的高度为h-1经过上述的变化如下图.

        此时要注意的是平衡因子发生变化。根据左右子树的高度即可求出

          假如b的高度为h,c的高度为h,经过上述相同的变化如下图.同时再求出平衡因子,此时就为刚开始举得示例情况,b,c为空子树

         假如b的高度为h-1,c的高度为h,经过上述相同的变化如下图..同时再求出平衡因子

        有了上面的分析,我们就可以写出左右旋转的函数了。


	void RotateLR(node* parent)
	{
		node* subL = parent->_left;
		node* subLR = subL->_right;
		int bf = subLR->_bf;
		//先左旋
		RotateL(subL);
		//再右旋
		RotateR(parent);

		//更新平衡因子,b,c 子树的高度不同,就可以通过,subRL的平衡因子反应
		if (bf == 0)
		{
			parent->_bf = subL->_bf = subLR->_bf = 0;
		}
		else if (bf == 1)
		{
			subL->_bf = -1;
			parent->_bf = subLR->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 1;
			subL->_bf = subLR->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

       插入函数的全部代码如下。

bool insert(T k)
{
	//二叉搜索树没有结点
	if (_root == nullptr)
	{
		_root = new node(k);
		return true;
	}

	//找到合适的位置
	node* parent = nullptr;
	node* cur = _root;

	while (cur)
	{
		//提前存下父节点
		parent = cur;

		//k大于结点值就去右边,小于就去左边
		if (cur->_key < k)
			cur = cur->_right;
		else if (cur->_key > k)
			cur = cur->_left;
		//一般的二叉搜索树不允许出现相同的数字
		else
			return false;
	}

	//先判断再插入
	if (parent->_key > k)
	{
		parent->_left = new node(k);	
		cur = parent->_left;
		cur->_parent = parent;
	}
	else
	{
		parent->_right = new node(k);
		cur = parent->_right;
		cur->_parent = parent;
	}

	//修改平衡因子
	//1.根节点的父亲结点为空,最坏情况走到根结点结束
	//2.插入完成后刚好parent,cur指向需要的位置
	while (parent)
	{
		if (parent->_left == cur)
			parent->_bf--;
		else
			parent->_bf++;

		//没有引起高度变化
		if (parent->_bf == 0)
			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 if (parent->_bf == -2 && cur->_bf == 1)
			{
				RotateLR(parent);
			}
			break;
		}
		else
		{
			assert(false);
		}
	}

        注意在我们旋转后就将循环结束了,不需要再向上更新平衡因子了。这是因为旋转后子树的高度与我们插入前的高度一样,所以不会引起父亲结点平衡因子的变化。

        到此我们的插入函数就讲完了!!真的十分不容易,感谢你能看到这。也请点点赞和关注。

AVL树测试

        到这里我们就完成了AVL树的插入,构造和析构函数,当然还有删除,删除我打算放在下一篇文章再开始写,我们写完了插入必定少不了测试,尤其是这么复杂的AVL树插入,写错某个小点就会导致代码错误,我们就可以针对刚才写的代码进行测试,检验我们代码是否正确,错了就修改。

        首先我们写的主要代码是插入,我们就可以检测插入后的二叉树是不是AVL树,如果不是就改BUG。虽然改BUG十分的烦人,但我们还是要静下心,慢慢改。

判断中序是否有序

        我们可以先判断中序遍历结果,判断是不是二叉搜索树,再判断高度差。

        与前面的问题一样,我们无法在类外部访问根结点,就需要再设置个子函数。采用先遍历左子树,根,然后遍历右子树就可以得到中序遍历结果了。

	void Inorder()
	{
		_Inorder(_root);
	}


private:
	void _Inorder(node* root)
	{
		if (root == nullptr)
			return;

		_Inorder(root->_left);
		cout << root->_key << " ";
		_Inorder(root->_right);
	}

        于是我们就可以写出第一个测试用例,结果如下是正确的

void test1()
{
	//vector<int> a= { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	vector<int> a = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };

	AVLTree<int> t;

	for (int i = 0; i < a.size(); i++)
	{
		t.insert(a[i]);
	}

	t.Inorder();

}

        我们可以在测试一组,结果也是没有问题的

        上述测试用例基本可以说明我们的中序是没有问题的了,接下来就是判断左右子树高度了。

平衡因子判断

        判断平衡因子我们要经常求子树的高度,我们就可以先写求树高度的函数。如下

int height(node* root)
{
	if (root == nullptr)
		return 0;
	int left = height(root->_left);
	int right = height(root->_right);

	return left > right ? left + 1 : right + 1;
}

        接着我们就可以递归的判断AVL树是否平衡,先求出当前结点平衡因子,判断是否合规,然后判断左子树和右子树。


	void IsBalance()
	{
		if(_IsBalance(_root))
			cout << "是AVL树" << endl;
		else
			cout << "不是AVL树" << endl;
	}

private:
	bool _IsBalance(node* root)
	{
		if (root == nullptr)
			return true;

		int left = height(root->_left);
		int right = height(root->_right);
		int bf = right - left;

		if (bf != root->_bf || bf > 1 || bf < -1)
			return false;
		
		return _IsBalance(root->_left) && _IsBalance(root->_right);
	}

        于是我们就可以写出以下测试代码

void test2()
{
	vector<int> a= { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	//vector<int> a = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };

	AVLTree<int> t;

	for (int i = 0; i < a.size(); i++)
	{
		if (a[i] == 7)
		{
			int c = 0;
		}
		t.insert(a[i]);
		cout << a[i] << "当前状态:";
		t.IsBalance();
	}

	//t.IsBalance();
}

        运行结果如下,说明我们的代码基本正确。

        到处就可以确定我们的程序基本没有问题了,是正确的。如有错误欢迎大家指正(最终的代码大家以附录为准,前面复制过程中可能会出现错误。)

        今天的代码到这里就结束了,欢迎大家指出错误。

        

附录代码

        头文件代码

#pragma once
#include<iostream>
#include<assert.h>
using namespace std;

template<class T>
struct AVLTNode
{
	AVLTNode(T k = T())
		:_left(nullptr)
		, _right(nullptr)
		, _key(k)
		, _parent(nullptr)
		,_bf(0)
	{

	}

	AVLTNode* _left;//左子结点
	AVLTNode* _right;//右子结点
	AVLTNode* _parent;//父亲结点
	T _key;//有效值
	int _bf;//平衡因子  balance factor
};

template<class T>
class AVLTree
{
	typedef AVLTNode<T> node;
public:
	AVLTree()
	{
		_root = nullptr;
	}
	AVLTree(int k)
	{
		_root = new node(k);
	}

	~AVLTree()
	{
		AVLTreeDestory(_root);
		_root = nullptr;
	}

	bool insert(T k)
	{
		//二叉搜索树没有结点
		if (_root == nullptr)
		{
			_root = new node(k);
			return true;
		}

		//找到合适的位置
		node* parent = nullptr;
		node* cur = _root;

		while (cur)
		{
			//提前存下父节点
			parent = cur;

			//k大于结点值就去右边,小于就去左边
			if (cur->_key < k)
				cur = cur->_right;
			else if (cur->_key > k)
				cur = cur->_left;
			//一般的二叉搜索树不允许出现相同的数字
			else
				return false;
		}

		//先判断再插入
		if (parent->_key > k)
		{
			parent->_left = new node(k);	
			cur = parent->_left;
			cur->_parent = parent;
		}
		else
		{
			parent->_right = new node(k);
			cur = parent->_right;
			cur->_parent = parent;
		}

		//修改平衡因子
		//1.根节点的父亲结点为空,最坏情况走到根结点结束
		//2.插入完成后刚好parent,cur指向需要的位置
		while (parent)
		{
			if (parent->_left == cur)
				parent->_bf--;
			else
				parent->_bf++;

			//没有引起高度变化
			if (parent->_bf == 0)
				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 if (parent->_bf == -2 && cur->_bf == 1)
				{
					RotateLR(parent);
				}
				break;
			}
			else
			{
				assert(false);
			}
		}


		return true;
	}

	void Inorder()
	{
		_Inorder(_root);
	}

	void IsBalance()
	{
		if(_IsBalance(_root))
			cout << "是AVL树" << endl;
		else
			cout << "不是AVL树" << endl;
	}

private:
	int height(node* root)
	{
		if (root == nullptr)
			return 0;
		int left = height(root->_left);
		int right = height(root->_right);

		return left > right ? left + 1 : right + 1;
	}

	bool _IsBalance(node* root)
	{
		if (root == nullptr)
			return true;

		int left = height(root->_left);
		int right = height(root->_right);
		int bf = right - left;

		if (bf != root->_bf || bf > 1 || bf < -1)
			return false;
		
		return _IsBalance(root->_left) && _IsBalance(root->_right);
	}

	void _Inorder(node* root)
	{
		if (root == nullptr)
			return;

		_Inorder(root->_left);
		cout << root->_key << " ";
		_Inorder(root->_right);
	}


	void AVLTreeDestory(node* _root)
	{
		if (_root == nullptr)
			return;

		AVLTreeDestory(_root->_left);
		AVLTreeDestory(_root->_right);

		delete _root;
	}

	void RotateL(node* parent)
	{
		node* subR = parent->_right;
		node* subRL = parent->_right->_left;

		node* parentparent = parent->_parent;
		//parent为根节点特殊处理
		if (parentparent == nullptr)
		{
			subR->_parent = nullptr;
			_root = subR;
		}
		else
		{
			//parent为某段子树
			if (parentparent->_left == parent)
				parentparent->_left = subR;
			else
				parentparent->_right = subR;

			subR->_parent = parent->_parent;
		}

		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;

		subR->_left = parent;
		parent->_parent = subR;

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


	void RotateR(node* parent)
	{
		node* parentparent = parent->_parent;
		node* subL = parent->_left;
		node* subLR = subL->_right;


		subL->_right = parent;
		if (parentparent == nullptr)
		{
			subL->_parent = nullptr;
			_root = subL;
		}
		else
		{
			subL->_parent = parentparent;
			if (parentparent->_left == parent)
				parentparent->_left = subL;
			else
				parentparent->_right = subL;
		}

		parent->_left = subLR;
		if (subLR)
			subLR->_parent = parent;
		parent->_parent = subL;

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


	void RotateRL(node* parent)
	{
		node* subR = parent->_right;
		node* subRL = subR->_left;
		int bf = subRL->_bf;
		//先右旋
		RotateR(subR);
		//再左旋
		RotateL(parent);

		//更新平衡因子,b,c 子树的高度不同,就可以通过,subRL的平衡因子反应
		if (bf == 0)
		{
			parent->_bf = subR->_bf = subRL->_bf = 0;
		}
		else if (bf == 1)
		{
			parent->_bf = -1;
			subR->_bf = subRL->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = subRL->_bf = 0;
			subR->_bf = 1;
		}
		else
		{
			assert(false);
		}
	}

	void RotateLR(node* parent)
	{
		node* subL = parent->_left;
		node* subLR = subL->_right;
		int bf = subLR->_bf;
		//先左旋
		RotateL(subL);
		//再右旋
		RotateR(parent);

		//更新平衡因子,b,c 子树的高度不同,就可以通过,subRL的平衡因子反应
		if (bf == 0)
		{
			parent->_bf = subL->_bf = subLR->_bf = 0;
		}
		else if (bf == 1)
		{
			subL->_bf = -1;
			parent->_bf = subLR->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 1;
			subL->_bf = subLR->_bf = 0;
		}
		else
		{
			assert(false);
		}
	}

	node* _root;
};

        源文件代码

#include"AVLTree.h"
#include<vector>

void test1()
{
	//vector<int> a= { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	vector<int> a = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };

	AVLTree<int> t;

	for (int i = 0; i < a.size(); i++)
	{
		t.insert(a[i]);
	}

	t.Inorder();

}

void test2()
{
	//vector<int> a= { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	vector<int> a = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };

	AVLTree<int> t;

	for (int i = 0; i < a.size(); i++)
	{
		if (a[i] == 14)
		{
			int c = 0;
		}
		t.insert(a[i]);
		cout << a[i] << "当前状态:";
		t.IsBalance();
	}

	//t.IsBalance();
}

int main()
{
	test2();


	return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1940833.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

太速科技-基于XCVU9P+ C6678的8T8R的无线MIMO平台

基于XCVU9P C6678的8T8R的无线MIMO平台 一、板卡概述 板卡基于TI TMS320C6678 DSP和XCVU9P高性能FPGA&#xff0c;FPGA接入4片AD9361 无线射频&#xff0c;构建8输入8输出的无线MIMO平台&#xff0c;丰富的FPGA资源和8核DSP为算法验证和信号处理提供强大能力。 二…

有人泼冷水:为什么AI基础设施创业如此艰难?

最近&#xff0c;Adept AI 宣布被亚马逊收购&#xff0c;这印证了 JOHN HWANG&#xff08;前 AWS 生成式 AI 架构师&#xff0c;摩根士丹利交易主管&#xff09;对未来的判断。于是他写了这篇文章&#xff0c;表达了对 AI 基础设施这个领域创业的隐忧。认为“AI 基础设施创业公…

有观点认为NVIDIA中国特供GPU H20也将面临禁售

为了能将GPU卖给中国市场&#xff0c;NVIDIA一直在魔改各种特供版&#xff0c;但是每每遭遇美国政府加码&#xff0c;随时都会被再次禁售&#xff0c;H20又要面临同样的命运。NVIDIA H20只配备了78组SM单元&#xff0c;相当于H100 SXM版本的六成、PCIe 5.0版本的接近七成&#…

在qt的c++程序嵌入一个qml窗口

//拖拽一个QQuickWidget c端和qml通信的桥梁 找到qml的main.qml的路径 ui->quickWidget->setSource(QUrl::fromLocalFile("../../../code/main.qml"));// QML 与 Qt Widgets 通信//窗口就成了一个类实例对象pRoot (QObject*)ui->quickWidget->rootObje…

centos8 一键安装nginx指定版本

centos 8 的源里面已经有nginx&#xff0c;支持命令直接安装 dnf install nginx -y但默认安装 1.14 版本&#xff0c;版本较老&#xff0c;存在安全问题。在官网找到指定版本rpm包&#xff0c;可直接安装 http://nginx.org/packages/centos/8/x86_64/RPMS/这里选择1.18&#…

python—爬虫爬取视频样例

下面是一个使用Python爬虫爬取视频的基本例子。创建一个Python爬虫来爬取视频通常涉及到几个步骤&#xff1a;发送HTTP请求、解析网页内容、提取视频链接、下载视频文件。 import jsonimport requests from lxml import etreeif __name__ __main__:# UA伪装head {"User…

一款国外开发的高质量WordPress下载站模板主题

5play下载站是由国外站长开发的一款WordPress主题&#xff0c;主题简约大方&#xff0c;为v1.8版本&#xff0c; 该主题模板中包含了上千个应用&#xff0c;登录后台以后只需要简单的三个步骤就可以轻松发布apk文章&#xff0c; 我们只需要在WordPress后台中导入该主题就可以…

《简历宝典》17 - 简历中“技术能力”,如何丰满且有层次,前端篇

这一节开始对技术能力模块做讲解&#xff0c;我们身边的这些互联网IT从业者们&#xff0c;前端开发、Java开发、软件测试又或者是其他职位的开发者们&#xff0c;技术能力这个模块是绕不过去的&#xff0c;从简历上看&#xff0c;这个模块体现了我们之前软件工作生涯中的技术功…

二、C#数据类型

本文是网页版《C# 12.0 本质论》第二章解读。欲完整跟踪本系列文章&#xff0c;请关注并订阅我的Essential C# 12.0解读专栏。 前言 数据类型&#xff08;Data Type&#xff09;是一个很恼人的话题。 似乎根本没必要对数据类型进行展开讲解&#xff0c;因为人人都懂。 但是…

Ubuntu 24.04 LTS Noble安装Docker Desktop简单教程

Docker 为用户提供了在 Ubuntu Linux 上快速创建虚拟容器的能力。但是&#xff0c;那些不想使用命令行管理容器的人可以在 Ubuntu 24.04 LTS 上安装 Docker Desktop GUI&#xff0c;本教程将提供用于设置 Docker 图形用户界面的命令…… Docker Desktop 是一个易于使用的集成容…

IVI(In-Vehicle Infotainment,智能座舱的信息娱乐系统)

IVI能够实现包括三维导航、实时路况、辅助驾驶等在线娱乐功能。 IVI人机交互形式&#xff08;三板斧&#xff09;&#xff1a;声音、图像、文字 IVI人机交互媒介I&#xff08;四件套&#xff09;&#xff1a;中控屏幕&#xff08;显示、触控&#xff09;、仪表显示、语言、方…

Python爬虫(2) --爬取网页页面

文章目录 爬虫URL发送请求UA伪装requests 获取想要的数据打开网页 总结完整代码 爬虫 Python 爬虫是一种自动化工具&#xff0c;用于从互联网上抓取网页数据并提取有用的信息。Python 因其简洁的语法和丰富的库支持&#xff08;如 requests、BeautifulSoup、Scrapy 等&#xf…

规范:前后端接口规范

1、前言 随着互联网的高速发展&#xff0c;前端页面的展示、交互体验越来越灵活、炫丽&#xff0c;响应体验也要求越来越高&#xff0c;后端服务的高并发、高可用、高性能、高扩展等特性的要求也愈加苛刻&#xff0c;从而导致前后端研发各自专注于自己擅长的领域深耕细作。 然…

SpringCloud极限速通版

1.SpringCloud概述 1.1 什么是微服务 1.1.1 单体架构 业务所有功能都打包在一个war包或jar包&#xff0c;这种方式就是单体架构&#xff0c;单体架构的应用就是单体应用。这种架构开发简单&#xff0c;部署简单&#xff0c;一个项目包含所有功能&#xff1b;省去了多个项目之…

图片上传成功却无法显示:静态资源路径配置问题解析

1、故事的背景 最近&#xff0c;有个学弟做了一个简单的后台管理页面。于是他开始巴拉巴拉撘框架&#xff0c;写代码&#xff0c;一顿操作猛如虎&#xff0c;终于将一个简单的壳子搭建完毕。但是在实现功能&#xff1a;点击头像弹出上传图片进行头像替换的时候&#xff0c;卡壳…

力扣高频SQL 50 题(基础版)第一题

文章目录 力扣高频SQL 50 题&#xff08;基础版&#xff09;第一题1757.可回收且低脂的产品题目说明思路分析实现过程准备数据&#xff1a;实现方式&#xff1a;结果截图&#xff1a; 力扣高频SQL 50 题&#xff08;基础版&#xff09;第一题 1757.可回收且低脂的产品 题目说…

昇思25天学习打卡营第14天 | SSD目标检测

探索SSD目标检测算法 在深入学习SSD&#xff08;Single Shot MultiBox Detector&#xff09;目标检测算法的过程中&#xff0c;我对现代计算机视觉中的目标检测技术有了更加深入的理解。SSD作为一种有效的单阶段目标检测算法&#xff0c;它在准确性和检测速度之间取得了良好的…

HAL库源码移植与使用之RTC时钟

实时时钟(Real Time Clock&#xff0c;RTC)&#xff0c;本质是一个计数器&#xff0c;计数频率常为秒&#xff0c;专门用来记录时间。 普通定时器无法掉电运行&#xff01;但RTC可由VBAT备用电源供电&#xff0c;断电不断时 这里讲F1系列的RTC 可以产生三个中断信号&#xff…

分类损失函数 (一) torch.nn.CrossEntropyLoss()

1、交叉熵 是一种用于衡量两个概率分布之间的距离或相似性的度量方法。机器学习中&#xff0c;交叉熵常用于损失函数&#xff0c;用于评估模型的预测结果和实际标签的差异。公式&#xff1a; y&#xff1a;真是标签的概率分布&#xff0c;y&#xff1a;模型预测的概率分布 …

FPGA实验3:D触发器设计

一、实验目的及要求 熟悉Quartus II 的 VHDL 文本设计简单时序电路的方法&#xff1b; 掌握时序电路的描述方法、波形仿真和测试&#xff0c;特别是时钟信号的特性。 二、实验原理 运用Quartus II 集成环境下的VHDL文本设计方法设计简单时序电路——D触发器&#xff0c;依据…