C++的AVL树

news2024/10/6 2:22:55

目录

基本概念

插入的语言分析 

LL右旋 

RR左旋 

额外结论及问题1

LR左右旋

RL右左旋

额外结论及问题2

插入结点

更新bf与判断旋转方式

旋转代码实现

准备工作一

LL右旋的实现

RR左旋的实现

准备工作二

LR左右旋的实现

RL右左旋的实现

完整代码


基本概念

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

2、AVL是满足以下性质的二叉搜索树:

  • 左右子树都是AVL树
  • 根节点的平衡因子的绝对值不超过1

3、一个有n个结点的AVL树,其高度为log_2 n,搜索时的时间复杂度为O(log_2 n)

4、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; //平衡因子


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

5、二叉搜索树变为AVL树需要经历以下两步:

  • 按照二叉搜索树的方式插入新节点
  • 通过旋转的方式调整结点的平衡因子

6、平衡因子 = 左子树深度 - 右子树深度(右减左的算法也行,只要最后的绝对值不超过1)

7、当二叉搜索树新插入的一个结点导致它的某个祖先结点的bf变为2或-2时,就需要以该祖先结点为基础进行旋转:

  • 平衡因子变为2是因为原本祖先结点的左子树就比右子树更深一层,新插入的结点会在该基础上继续向祖先结点的左结点(L)的左或右子树(L  / R)中进行插入且成功让该子树高度+1(说成功+1是因为也有可能只是补了一个空位)
  • 平衡因子变为-2是因为原本祖先结点的右子树就比左子树更深一层,新插入的结点会在该基础上继续向祖先结点的右结点(R)的左或右子树(L  / R)中进行插入且成功让该子树高度+1(说成功+1是因为也有可能只是补了一个空位)

插入的语言分析 

下面例子中在说向a、b和c等AVL子树中插入时需要注意以下三点内容:

  1. 不关心具体插的是AVL子树的哪个位置
  2. 每次插入时肯定使得某个AVL子树多了一层(如果不是这样就不会进入旋转函数)
  3. 是向哪个AVL子树中插入

进行旋转时需要考虑两点内容:

  1. 旋转后的树是否是AVL树
  2. 旋转后的树是否符合二叉搜索树的基本性质左< 根 < 右

更细致的bf变化图懒得画了,注意绿色字体即可,有规律

真正得出该规律需要举太多例子或者需要用数学方法去计算验证,很麻烦

所以我在这里直接举一个例子,就指明该规律了

LL右旋 

1、现在我们假设这里有一个左子树深度大于右子树的AVL树:

①向a插入,并成功使该AVL子树的高度变为h+1时,30的bf变为1,60的bf变为2

旋转方式:将b作为60结点的左孩子,将60结点作为30结点的右孩子,此时30结点的左子树高度为h+1(向a中插入了一个结点导致该AVL子树的高度+1)等于右子树的h+1(新增的一个60结点+h)我们称这种旋转方式为右旋

插入后bf的前后变化:30结点的bf:1 -> 0、60结点的bf:2->0

结论:在AVL树的某个结点的左结点(L)的左子树(L)上进行插入需要进行右旋 

RR左旋 

2、现在我们假设这里有一个右子树深度大于左子树的AVL树:

①向c插入,并成功使该AVL子树的高度变为h+1时,70的bf变为-1,60的bf变为-2

旋转方式:将b作为60结点的右孩子,将60结点作为70结点的左孩子,此时70结点的右子树高度为h+1(向c中插入了一个结点导致该AVL子树的高度+1)等于左子树的h+1(新增的一个60结点+h)我们称这种旋转方式为左旋

插入后bf的前后变化:70结点的bf:-1 -> 0、60结点的bf:-2->0

结论:在AVL树的某个结点的右结点(R)的右子树(R)上进行插入需要进行左旋

额外结论及问题1

额外结论1:因为LL和RR导致的左或右旋,旋转后除了原本bf==2或-2的结点的bf会变为0,该结点的左或右结点的bf也会变为0(发生左旋就是左节点的bf变为0,发生右旋就是右节点的bf变为0),即使h == 0,说它们在旋转后都变为0是为了后续的写代码做准备

问题1:为什么不提新插入结点的bf由什么变为什么?

解释:因为插入时该结点的bf肯定为0,但是该结点的插入可能会使它的祖先结点的bf产生变化,当某个祖先结点的bf变为2时就需要以该祖先结点为基础进行旋转,通过上述案例我们可以得知不论是RR左旋还是LL右旋,完成插入后和完成旋转后两个状态时,新插入结点的bf第一为0,所以在完成旋转后不需要对该值进行更新从宏观上来看它一直没变

LR左右旋

3、现在我们假设这里有一个左子树深度大于右子树的AVL树(将b分为d、e和40是因为如果向某个结点的左结点的右子树中插入,如果b还是一个整体那么无法进行分析并旋转不信你按照单纯的左或者右旋试一试这都是别人研究后的结果,而且这里的分开也只是将b中的第一个具体结点透露出来,之前操作b时也是操作该结点的,懒得想那么多就直接往下看就行)

​​​​​​①向d插入,成功使该AVL子树的高度变为h时,40的bf变为1,30的bf变为-1,60的bf变为2

旋转方式:先将30作为旋转点,对其进行左旋(d成为30的右孩子,30成为40的左孩子,40成为60的左孩子)后将60作为旋转点,对其进行右旋(c成为60的左孩子,60成为40的右孩子)此时40的bf变为,我们称这种旋转方式为左右旋

插入后bf的前后变化:40结点的bf:1 -> 0、30结点的bf:-1 -> 0、60结点的bf:2 -> -1

②向e插入,成功使该AVL子树的高度变为h时,40的bf变为-1,30的bf变为-1,60的bf变为2

旋转方式:同上(区别在于30结点得到的右孩子d的高度仍为h-1,60结点得到的左孩子e的高度增加了1,这也是为什么会产生30和60结点的bf因为插入d还是e不同而不同的根本原因,如果嫌麻烦可以不看一句直接看下面的结论)

插入后bf的前后变化:40结点的bf:-1 -> 0、30结点的bf:-1 -> 1、60结点的bf:2 -> 0

结论:

1、在AVL树的某个结点的左结点(L)的右子树(R)上进行插入需要进行左右旋

2、如果40结点的bf在插入新结点后变为1,那么最后三个结点的bf分别是0(祖先结点)、-1(祖先结点的左结点)、0(祖先结点的左结点的右结点)

3、如果40结点的bf在插入新结点后变为-1,那么最后三个结点的bf分别是0(祖先结点)、1(祖先结点的左结点)、0(祖先结点的左结点的右结点)

4、产生上述差异的本质原因是:40的左孩子必然为30右孩子,右孩子必然为60的左孩子,左右孩子会因为插入的对象不同产生差异,进而导致接收它们的父结点的bf发生变化

RL右左旋

4、现在我们假设这里有一个右子树深度大于左子树的AVL树:

其余的懒得画了,直接上结论:

1、在AVL树的某个结点的右结点(R)的右子树(R)上进行插入需要进行右左旋

2、如果40结点的bf在插入新结点后变为1,那么最后三个结点的bf分别是0(祖先结点)、-1(祖先结点的右结点)、0(祖先结点的右结点的左结点)

3、如果40结点的bf在插入新结点后变为-1,那么最后三个结点的bf分别是1(祖先结点)、0(祖先结点的右结点)、0(祖先结点的右结点的左结点)

额外结论及问题2

额外结论2:

1、若h == 0,则会出现下图中的两种初始情况,即如果40结点的bf为0时(40结点本身就是新插入的结点),旋转后它的祖先结点和祖先结点的左或右结点的bf也会变为0,需要注意的是在中途旋转时父亲和爷爷结点的bf并不一定满足之前单纯LL右旋和RR左旋时所说都变为0的规律RR左旋后(因为我测试过不满足的情况,满足的情况可能有但我没试)

问题2:为什么只考虑三代结点?

解释:因为fd==2或-2的情况只会出现在三代内(其它情况的图就不画了),并且一个AVL树中只会存在一个由于新插入结点导致的非AVL子树,并且会在下次插入前理解将该AVL子树恢复正常,所以当该AVL子树恢复正常后就不需要继续考虑其他的结点了(这应该也算是一个规律,我们直接记住就行)

插入结点

bool Insert(const pair<K, V>& kv)
{
	if (_root == nullptr)
	{
		_root = new Node(kv);
		return true;
	}

	Node* parent = nullptr;
	Node* cur = _root;

	//主要负责确定要插入结点的父结点位置,同时保证不会出现冗余
	//cur负责为parent探路,最后cur一定指向空(相当于没用后被置空),parent指向的结点就是插入结点的父结点
	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);//重新令cur指向新申请的结点

	//新结点的k小于parent指向结点的k
	if (kv.first < parent->_kv.first)
	{
		parent->_left = cur;//将新结点插入到父结点的左子树上
	}
	else
	{
		parent->_right = cur;//将新结点插入父结点的右子树上
	}

	cur->_parent = parent;//新结点插入后,新结点有了父亲结点,它的父亲结点就是parent指向的结点,所以将cur的_parent更新为parent
}

更新bf与判断旋转方式

吐槽:能发现这种更新机制的人的大脑构造可能真的跟我们不一样🤡

基本概念:插入新结点必然会导致其父结点的bf产生变化,并可能会导致它的爷爷结点甚至更向上的某个祖先结点的bf发生变化,当某个祖先结点的bf == 2或-2时需要以该祖先结点为基础进行旋转

结论1:对父结点bf的修改可以通过判断插入结点是父结点的左还是右结点然后++或--即可

结论2:对爷爷结点和更向上的祖先结点的bf的修改需要利用循环判断的方式实现,当某个祖先结点的bf在修改后变为2或-2时就需要以该祖先结点为基础进行旋转

由语言分析的例子可得结论3:

1、bf == 2时它左结点的bf若为1则采取LL右旋,若为-1则采取LR左右旋

2、bf == -2时它右结点的bf若为-1要采取RR左旋,若为1则采取RL右左旋

(旋转时都以parent指向的结点为基础)

//更新平衡因子
while (parent)
{
	if (cur == parent->_left)
	{
		parent->_bf++;//新结点插在父结点左侧则父结点的bf++
++
	}
	else
	{
		parent->_bf--;//新结点插在父结点右侧则父结点的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);//2 + 1 == LL右旋
		}
		else if (parent->_bf == 2 && cur->_bf == -1)
		{
			RotateLR(parent);//2 + -1 == LR左右旋
		}
		else if (parent->_bf == -2 && cur->_bf == -1)
		{
			RotateR(parent);//-2 + -1  == RR左旋
		}
		else if (parent->_bf == -2 && cur->_bf == 1)
		{
			RotateRL(parent);//-2 + 1 == RL右左旋
		}

		break;
	}
	else
	{
		// 理论而言不可能出现这个情况
		assert(false);
	}
}

旋转代码实现

准备工作一

1、我们向旋转函数中传入的都是bf==2或-2的结点的指针,即parent

2、通过上述语言分析的例子,我们可以发现在旋转函数中需要三个指针

①当bf==2时

  • 指向bf==2结点的指针parent(函数的唯一形参,后两个指针需要依据该形参在函数中定义)
  • 指向parent指向结点的左结点的指针subL
  • 指向subL指向结点的右结点的指针subLR

②当bf==-2时

  • 指向bf==-2结点的指针parent(函数的唯一形参,后两个指针需要依据该形参在函数中定义)
  • 指向parent指向结点的右结点的指针subR
  • 指向subL指向结点的左结点的指针subRL

注意事项:

1、因为h>=0,所以当处理完h不为0的情况还要考虑h为0时存在的使用空指针问题

2、因为每个结点中存放了它的三个“亲人”结点的信息(左孩子、右孩子、父亲),所以如果bf==2或-2的结点为根结点那么在旋转时就不需要考虑该结点的父结点信息(它的父结点一直为nullptr),只需要考虑另外两个指针指向的结点的父结点信息(三个结点的孩子信息肯定一直需要考虑),如果不是bf==2或-2的结点不是根结点,由于它是“大哥”所以为了避免旋转后找不到它的父结点,需要先保存它的父结点信息,然后再进行旋转

3、旋转过程中三个指针指向的结点不会发生改变

LL右旋的实现

//右旋基础版(60结点是根结点)
void RotateR(Node* parent)//传入的结点都是结点为bf==2或-2的结点
{
	Node* subL = parent->_left;//subL指向根结点的左结点
	Node* subLR = subL->_right;//subLR指向subL所指向结点的右结点(若h==0则subLR指向空)

	parent->_left = subLR;//根结点的左结点更新为subLR指向的结点

    //只有在subLR不为空的时候才用更新它的父亲信息,如果为空时仍去更新就会出现使用空指针的问题
	if (subLR)
    {
        subLR->_parent = parent;//更新subLR的父结点信息(加上只是为了防止使用空指针实际影响不大)
    }
	
	subL->_right = parent;//subL的右结点变为根结点

	parent->_parent = subL;//后将根结点中存放的父结点信息由nullptr变为subL

    _root = subL;//右旋后是subL指向的结点作为根结点,令整棵树的_root也指向subL指向的结点
	_root->_parent = nullptr;//根结点没有父结点

    parent->_bf = subL->_bf = 0;//更新此时的平衡因子,在上面我们总结过了规律,LL右旋一定会导致新插入结点的父亲和爷爷结点的bf变为0
}

//右旋完全版(parent指向的结点可能不是初始根节点)
void RotateR(Node* parent)//传入的参数是平衡因子不为1、0、-1的结点
{
	Node* subL = parent->_left;//subL指向parent结点的左结点
	Node* subLR = subL->_right;//subLR指向subL指向的结点的右结点

	parent->_left = subLR;//parent的左结点更新为subLR指向的结点

    //subLR可能为空,只有它不为空的时候才去更新它的父亲信息
	if (subLR)
    {
        subLR->_parent = parent;//更新subLR的父结点信息
    }
	
	subL->_right = parent;//subL的右结点变为了parent指向的结点

	Node* ppNode = parent->_parent;//先保存自己原先得父亲结点信息

	parent->_parent = subL;//后将parent指向的结点中存放的父结点信息由原来的某个结点变为subL

    //如果parent指向的结点的确是初始根结点
	if (parent == _root)//_root指向原始根节点,在建树的时候就已经确定
	{
		_root = subL;//右旋后应该是subL作为初始根节点,令_root也指向subL指向的结点
		_root->_parent = nullptr;//初始根节点没有父结点,将存放父结点信息的指针置空
	}
	else//如果parent指向的结点不是初始根结点
	{
		if (ppNode->_left == parent)//如果parent是原父结点的左结点
		{
			ppNode->_left = subL;//将原父结点中存放的左结点信息变为subL
		}
		else//如果parent是原父结点的右结点
		{
			ppNode->_right = subL;//将原父结点中存放的右结点信息变为subL
		}
		    
        subL->_parent = ppNode;//同时将subL中存放的父结点的信息更新为parent之前的父结点
	}

	parent->_bf = subL->_bf = 0;//更新平衡因子
}

RR左旋的实现

道理与LL右旋基本一致,只需该改变一些变量,即可具体内容不再过多描述

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

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

	subR->_left = parent;
	Node* ppNode = parent->_parent;

	parent->_parent = subR;

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

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

准备工作二

1、双重旋只需要确定每次的旋转点即可(该点也是固定的),可以复用左或右旋的代码,它的关键点在于更新旋转后和parent指向结点的bf和parent指向结点的左或右结点的bf的值

2、旋转完后subLR和subRL指向的结点的bf均为0

3、通过语言分析部分的描述我们可以得出以下结论:

①LR左右旋时

  • subLR指向结点的bf为1时,旋转后praent和subL指向结点的bf分别为-1和0
  • subLR指向结点的bf为-1时,旋转后praent和subL指向结点的bf分别为0和1
  • subLR指向结点的bf为0时,旋转后praent和subL指向结点的bf均为0

②RL右左旋时

  • subRL指向结点的bf为1时,旋转后praent和subR指向结点的bf分别为0和-1
  • subRL指向结点的bf为-1时,旋转后praent和subR指向结点的bf分别为1和0
  • subRL指向结点的bf为0时,旋转后praent和subR指向结点的bf均为0 

LR左右旋的实现

void RotateLR(Node* parent)
{
    //这里的subL和subLR的仅用于修改父亲和爷爷结点的bf
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	int bf = subLR->_bf;

	RotateL(subL);
	RotateR(parent);

    subLR->_bf = 0;//旋转完后subLR指向的结点的bf为0

	if (bf == 1)
	{
        parent->_bf = -1;
		subL->_bf = 0;	
	}
	else if (bf == -1)
	{
		parent->_bf = 0;
		subL->_bf = 1;
	}
	else if (bf == 0)
	{
		parent->_bf = 0;
		subL->_bf = 0;
	}
}

RL右左旋的实现

不解释了

void RotateRL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	int bf = subRL->_bf;

	RotateR(subR);
	RotateL(parent);

	subRL->_bf = 0;//旋转完后subRL指向的结点的bf为0

	if (bf == 1)
	{
		parent->_bf = 0;
		subR->_bf = -1;
	}
	else if (bf == -1)
	{
		parent->_bf = 1;
		subR->_bf = 0;
	}
	else
	{
		parent->_bf = 0;
		subR->_bf = 0;
	}
}

完整代码

AVL树的删除相比于插入更加复杂,这里不再过多阐述,考研或者面试一般不会要求手写AVL树

一些基础功能的代码就放在整体代码中了

#pragma once

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


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; //平衡因子


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


template<class K, class V>
class  AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	bool Insert(const pair<K, V>& kv)
	{
		if (_root == nullptr)
		{
			_root = new Node(kv);
			return true;
		}

		Node* parent = nullptr;
		Node* cur = _root;

		//主要负责确定要插入结点的父结点位置,同时保证不会出现冗余
		//cur负责为parent探路,最后cur一定指向空(相当于没用后被置空),parent指向的结点就是插入结点的父结点
		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);//重新令cur指向新申请的结点

		//新结点的k小于parent指向结点的k
		if (kv.first < parent->_kv.first)
		{
			parent->_left = cur;//将新结点插入到父结点的左子树上
		}
		else
		{
			parent->_right = cur;//将新结点插入父结点的右子树上
		}

		cur->_parent = parent;//新结点插入后,新结点有了父亲结点,它的父亲结点就是parent指向的结点,所以将cur的_parent更新为parent



		//更新平衡因子
		while (parent)
		{
			if (cur == parent->_left)
			{
				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);//2 + 1 == LL右旋
				}
				else if (parent->_bf == 2 && cur->_bf == -1)
				{
					//RotateLR(parent);//2 + -1 == LR左右旋
					continue;
				}
				else if (parent->_bf == -2 && cur->_bf == -1)
				{
					RotateR(parent);//-2 + -1  == RR左旋
				}
				else if (parent->_bf == -2 && cur->_bf == 1)
				{
					RotateRL(parent);//-2 + 1 == RL右左旋
				}

				break;
			}
			else
			{
				// 理论而言不可能出现这个情况
				std::cout << "bf != 0、-1、1、-2、2" << std::endl;
				assert(false);
			}
		}
	}

	//查找函数
	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;
	}

	//RR左旋
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

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

		subR->_left = parent;
		Node* ppNode = parent->_parent;

		parent->_parent = subR;

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

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


	void RotateLR(Node* parent)
{
	//这里的subL和subLR的仅用于修改父亲和爷爷结点的bf
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
		int bf = subLR->_bf;

   	RotateL(subL);
   	RotateR(parent);

   	subLR->_bf = 0;//旋转完后subLR指向的结点的bf为0

   	if (bf == 1)
   	{
   		parent->_bf = -1;
   		subL->_bf = 0;
   	}
   	else if (bf == -1)
   	{
   		parent->_bf = 0;
   		subL->_bf = 1;
   	}
   	{
   		parent->_bf = 0;
   		subL->_bf = 0;
   	}
   }
   


	//LL右旋
	//右旋完全版(parent指向的结点可能不是初始根节点)
	void RotateR(Node* parent)//传入的参数是平衡因子不为1、0、-1的结点
	{
		Node* subL = parent->_left;//subL指向parent结点的左结点
		Node* subLR = subL->_right;//subLR指向subL指向的结点的右结点

		parent->_left = subLR;//parent的左结点更新为subLR指向的结点

		//subLR可能为空,只有它不为空的时候才去更新它的父亲信息
		if (subLR)
		{
			subLR->_parent = parent;//更新subLR的父结点信息
		}

		subL->_right = parent;//subL的右结点变为了parent指向的结点

		Node* ppNode = parent->_parent;//先保存自己原先得父亲结点信息

		parent->_parent = subL;//后将parent指向的结点中存放的父结点信息由原来的某个结点变为subL

		//如果parent指向的结点的确是初始根结点
		if (parent == _root)//_root指向原始根节点,在建树的时候就已经确定
		{
			_root = subL;//右旋后应该是subL作为初始根节点,令_root也指向subL指向的结点
			_root->_parent = nullptr;//初始根节点没有父结点,将存放父结点信息的指针置空
		}
		else//如果parent指向的结点不是初始根结点
		{
			if (ppNode->_left == parent)//如果parent是原父结点的左结点
			{
				ppNode->_left = subL;//将原父结点中存放的左结点信息变为subL
			}
			else//如果parent是原父结点的右结点
			{
				ppNode->_right = subL;//将原父结点中存放的右结点信息变为subL
			}

			subL->_parent = ppNode;//同时将subL中存放的父结点的信息更新为parent之前的父结点
		}

		parent->_bf = subL->_bf = 0;//更新平衡因子
	}

		
	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;

		RotateR(subR);
		RotateL(parent);

		subRL->_bf = 0;//旋转完后subRL指向的结点的bf为0

		if (bf == 1)
		{
			parent->_bf = 0;
			subR->_bf = -1;
		}
		else if (bf == -1)
		{
			parent->_bf = 1;
			subR->_bf = 0;
		}
		else
		{
			parent->_bf = 0;
			subR->_bf = 0;
		}
	}


	//中序遍历函数
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

	//判断是否平衡
	bool IsBalance()
	{
		return _IsBalance(_root);
	}

	//判断高度
	int Height()
	{
		return _Height(_root);
	}

	//判断结点个数
	int Size()
	{
		return _Size(_root);
	}

	//避免接口暴露,让处理函数的权限为private
private:

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

	int _Height(Node* root)
	{
		if (root == nullptr)
			return 0;

		return max(_Height(root->_left), _Height(root->_right)) + 1;
	}

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

		int leftHeight = _Height(root->_left);
		int rightHeight = _Height(root->_right);
		// 不平衡
		if (abs(leftHeight - rightHeight) >= 2)
		{
			cout << root->_kv.first << endl;
			return false;
		}

		// 顺便检查一下平衡因子是否正确
		if (rightHeight - leftHeight != root->_bf)
		{
			cout << root->_kv.first << endl;
			return false;
		}

		return _IsBalance(root->_left)
			&& _IsBalance(root->_right);
	}

    //中序遍历
	void _InOrder(Node* root)
	{
		if (root == nullptr)
		{
			return;
		}

		_InOrder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;
		_InOrder(root->_right);
	}

private:
		Node* _root = nullptr;
};

//测试代码
void TestAVLTree1()
{
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7,16,14 };//插入14的时候出错了
	AVLTree<int, int> t1;
	for (auto e : a)
	{
		t1.Insert({ e,e });
	}

	t1.InOrder();
	cout << t1.IsBalance() << endl;
}

~over~

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

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

相关文章

机器学习算法手撕(一):KD树

import math import matplotlib.pyplot as pltclass Node:def __init__(self, data, leftNone, rightNone):self.data dataself.left leftself.right right# 创建KDTree类 class KDTree:def __init__(self, k):self.k kdef create_tree(self,dataset,depth):if not dataset…

Docker CIG使用

Docker CIG是什么 CIG为&#xff1a;CAdvisor监控收集、InfluxDB存储数据、Granfana图表展示 这个组合是一个常见的监控 Docker 容器的解决方案,它包括以下三个组件: cAdvisor (Container Advisor): cAdvisor 是一个开源的容器资源监控和性能分析工具。它能够收集有关正在运行的…

Java实现图书系统

首先实现一个图书管理系统,我们要知道有哪些元素? 1.用户分成为管理员和普通用户 2.书:书架 书 3.操作的是: 书架 目录 第一步:建包 第二步:搭建框架 首先:完成book中的方法 其次:完成BookList 然后:完成管理员界面和普通用户界面 最后:Main 第三步:细分方法 1.退…

除自身以外数组的乘积 ---- 前缀和

题目链接 题目: 分析: 计算某个区间的积, 同样可以使用前缀和算法的思想想要计算除i位置的积, 我们需要计算i位置之前[0,i-1]的前缀积 和 i位置之后[i1,n-1]的后缀积, n表示数组的长度 先求[0,i - 1]的积 用一个前缀数组f 此时f[i] 表示: 前i - 1个数的积, 那么f[i - 1] 就表…

虹科Pico汽车示波器 | 免拆诊断案例 | 2012 款雪佛兰科鲁兹车偶尔多个故障灯异常点亮

故障现象 一辆2012款雪佛兰科鲁兹车&#xff0c;搭载1.8 L 发动机&#xff0c;累计行驶里程约为9.6万km。该车组合仪表上的发动机故障灯、ABS故障灯及动力转向故障灯偶尔异常点亮&#xff0c;同时发动机转速表和发动机冷却液温度表的指针会突然归零&#xff0c;严重时发动机无…

上下文视觉提示实现zero-shot分割检测及多visual-prompt改造

文章目录 一、Closed-Set VS Open-set二、DINOv2.1 论文和代码2.2 内容2.3 安装部署2.4 使用效果 三、多visual prompt 改造3.1 获取示例图mask3.2 修改函数参数3.3 推理代码3.4 效果的提升&#xff01; 四、总结 本文主要介绍visual prompt模型DINOv&#xff0c;该模型可输入八…

Qt for Android 乱码问题

java文件乱码 导致编译失败 使用notepad等查看java文件的编码&#xff0c; 修改成utf-8&#xff0c;否则会因为乱码编译失败&#xff0c; 记住是utf8不是utf8-bom. 做如下修改确保utf8文件不被修改掉。 编译时错误显示的是乱码 如果开发其他乱码再改回&#xff0c; 原本是Sys…

【机器学习300问】99、多通道卷积神经网络在卷积操作时有哪些注意事项?

一、多通道卷积神经网络示例 还是以图像处理为例&#xff0c;如果你的目标不仅是分析灰度图像特性&#xff0c;还打算捕捉RGB彩色图像的特征。如下图&#xff0c;当面对一张66像素的彩色图像时&#xff0c;提及的“3”实际上是指红、绿、蓝三种颜色通道&#xff0c;形象地说&am…

BUUCTF-Misc24

从娃娃抓起1 1.打开附件 是两个文本文件 2.电报码 电报码在线翻译网站&#xff1a;https://usetoolbar.com/convert/cccn.html 3.汉字五笔编码 汉字五笔编码在线网站查询&#xff1a;https://www.qqxiuzi.cn/bianma/wubi.php 4.转化为MD5值 将文字保存到文本文档 用winR输入…

绘唐3模型怎么放本地sd安装及模型放置位置 及云端sd部署

绘唐3模型怎么放本地sd安装及模型放置位置 及云端sd部署 资料里面授权方式&#xff1a; https://qvfbz6lhqnd.feishu.cn/wiki/CcaewIWnSiAFgokOwLycwi0Encf 云端和模型之间存在某种关联性。云端通常用于存储和管理大量数据&#xff0c;并提供计算和资源的服务。模型是对数据进…

Day04:CSS 进阶

目标&#xff1a;掌握复合选择器作用和写法&#xff1b;使用background属性添加背景效果 一、复合选择器 定义&#xff1a;由两个或多个基础选择器&#xff0c;通过不同的方式组合而成。 作用&#xff1a;更准确、更高效的选择目标元素&#xff08;标签&#xff09;。 1、后…

蚁小二:又一款高效自媒体工具,免费用户可发5个账号

其实自媒体的群发工具有几个&#xff0c;除了前几天介绍的融媒宝还有蚁小二等。因为融媒宝免费用户只能添加5个账号&#xff0c;所以不够用的朋友可以再下载蚁小二使用&#xff0c;这样就有10个账号可以发布了&#xff1a; 蚁小二简介 蚁小二是由长沙草儿绽放科技有限公司自主…

【论文阅读】Prompt Fuzzing for Fuzz Driver Generation

文章目录 摘要一、介绍二、设计2.1、总览2.2、指导程序生成2.3、错误程序净化2.3.1、执行过程净化2.3.2、模糊净化2.3.3、覆盖净化 2.4、覆盖引导的突变2.4.1、功率调度2.4.2、变异策略 2.5、约束Fuzzer融合2.5.1、论据约束推理2.5.1、模糊驱动融合 三、评估3.1、与Hopper和OSS…

Honeyview看图神器,免费无广告!

之前看图软件使用的是BandiView&#xff0c;但是最近频繁弹出广告&#xff0c;今天换了款Honeyview&#xff0c;也叫蜜蜂浏览器&#xff0c;免费无广告&#xff0c;速度很快&#xff0c;还以直接查看压缩包中的图片&#xff0c;你懂的&#xff01; 软件设置 首先随便打开一张图…

Virtual Box安装Ubuntu及设置

Virtual Box安装Ubuntu及设置 本文包含以下内容&#xff1a; 使用Virtual Box安装Ubuntu Desktop。设置虚拟机中的Ubuntu&#xff0c;使之可访问互联网并可通过SSH访问。 Ubuntu Desktop下载 从官网下载&#xff0c;地址为&#xff1a;Download Ubuntu Desktop | Ubuntu U…

游戏缺失steam_api64.dll的修复方法,快速解决游戏启动问题

在现代科技发展的时代&#xff0c;电脑已经成为我们生活中不可或缺的一部分。然而&#xff0c;在使用电脑的过程中&#xff0c;我们经常会遇到一些常见的问题&#xff0c;其中之一就是找不到某个特定的动态链接库文件&#xff0c;比如steamapi64.dll。这个问题可能会导致某些应…

Google的MLP-MIXer的复现(pytorch实现)

Google的MLP-MIXer的复现&#xff08;pytorch实现&#xff09; 该模型原论文实现用的jax框架实现&#xff0c;先贴出原论文的代码实现&#xff1a; # Copyright 2024 Google LLC. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may …

springboot + Vue前后端项目(第十一记)

项目实战第十一记 1.写在前面2. 文件上传和下载后端2.1 数据库编写2.2 工具类CodeGenerator生成代码2.2.1 FileController2.2.2 application.yml2.2.3 拦截器InterceptorConfig 放行 3 文件上传和下载前端3.1 File.vue页面编写3.2 路由配置3.3 Aside.vue 最终效果图总结写在最后…

【NumPy】关于numpy.clip()函数,看这一篇文章就够了

&#x1f9d1; 博主简介&#xff1a;阿里巴巴嵌入式技术专家&#xff0c;深耕嵌入式人工智能领域&#xff0c;具备多年的嵌入式硬件产品研发管理经验。 &#x1f4d2; 博客介绍&#xff1a;分享嵌入式开发领域的相关知识、经验、思考和感悟&#xff0c;欢迎关注。提供嵌入式方向…

H.机房【蓝桥杯】/数组链式前向星建图+堆优化版dijkstra

机房 数组链式前向星建图堆优化版dijkstra #include<iostream> #include<queue> #include<cstring> #include<vector> using namespace std; typedef pair<int,int> pii; //无向图开两倍 int e[200005],ne[200005],v[200005],h[200005],du[1000…