高级数据结构——平衡二叉树(AVL树)

news2024/12/23 22:32:00

目录

1. 底层结构

2. AVL数的概念 

3. AVL树节点的定义

4. 基本框架

5. AVL树的插入

6. AVL树的旋转

6.1 左单旋

6.2 右单旋

6.3 左右双旋 

6.4 右左双旋

7. AVL树的验证

8. AVL树的查找

9. AVL树的删除

10. AVL树的性能

11. 总代码

11.1 AVLTree

11.2 Test.cpp

1. 底层结构

前面对map、multimap、set、multiset进行了简单的介绍,这几个容器有个共同点是:其底层都是按照二叉搜索树来实现的,但是二叉搜索树有其自身的缺陷,假如往树中插入的元素有序或者接近有序,二叉搜索树就会退化成单支树,时间复杂度会退化成O(N),因此map、set等关联式容器的底层结构是对二叉树进行了平衡处理,即采用平衡树来实现。

2. AVL数的概念 

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

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

  1. 它的左右子树都是AVL树
  2. 任何一颗左右子树高度之差(简称平衡因子)的绝对值不超过1(-1/0/1)。
  • 0代表左右高度相等;
  • 1代表右子树高1;
  • -1代表左子树高1。

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

3. AVL树节点的定义

这里我们实现的AVL树为KV模型,自然节点的模板参数有两个,并且节点定义为三叉连结构(左孩子,右孩子,父亲),在二叉链表的基础上加了一个指向父结点的指针域,使得即便于查找孩子结点,又便于查找父结点。接着还需要创建一个变量_bf作为平衡因子(右子树 - 左子树的高度差)。最后写一个构造函数初始化变量即可。

//节点类
template<class K, class V>
struct AVLTreeNode
{
	//存储的键值对
	pair<K, V> _kv;
	//三叉连结构
	AVLTreeNode<K, V>* _left;//左孩子
	AVLTreeNode<K, V>* _right;//右孩子
	AVLTreeNode<K, V>* _parent;//父亲
	//平衡因子_bf
	int _bf;//右子树 - 左子树的高度差
	//构造函数
	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _bf(0)
	{}
};

4. 基本框架

此部分内容为AVL树的类,主要作用是来完成后续的插入旋转删除……操作:

//AVL树的类
template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
    //……
private:
	Node* _root;
};

5. AVL树的插入

插入主要分为这几大步骤:

  • 1、一开始为空树,直接new新节点
  • 2、一开始非空树,寻找插入的合适位置
  • 3、找到插入的合适位置后,进行父亲与孩子的双向链接
  • 4、更新新插入的节点祖先的平衡因子
  • 5、针对不合规的平衡因子进行旋转调整

接下来对其进行逐个分析:

  • 1、一开始为空树,直接new新节点

因为树为空的,所以直接new一个新插入的节点,将其作为根_root即可,接着更新平衡因子_bf为0,最后返回true。

  • 2、一开始非空树,寻找插入的合适位置

这里和二叉搜索树的寻找合适的插入位置的思想一样,都要遵循以下几步:

  1. 插入的值 > 节点的值,更新到右子树查找
  2. 插入的值 < 节点的值,更新到左子树查找
  3. 插入的值 = 节点的值,数据冗余插入失败,返回false

当循环结束的时候,就说明已经找到插入的合适位置,即可进行下一步链接。

  • 3、找到插入的合适位置后,进行父亲与孩子的双向链接:

注意这里节点的构成为三叉链,因此最后链接后端孩子和父亲是双向链接,具体操作如下:

  1. 插入的值 > 父亲的值,把插入的值链接在父亲的右边
  2. 插入的值 < 父亲的值,把插入的值链接在父亲的左边
  3. 因为是三叉连,插入后记得双向链接(孩子链接父亲)

走到这,说明节点已经插入完毕,但是接下来就要更新平衡因子了

  • 4、更新新插入的节点祖先的平衡因子:

当我们插入新节点后,子树的高度可能会发生变化,针对这一变化,我们给出以下要求:

  1. 子树的高度变了,就要继续往上更新
  2. 子树的高度不变,则更新完成
  3. 子树违反平衡规则(平衡因子的绝对值 >= 2),则停止更新,需要旋转子树进行调整

具体的更新规则如下:

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

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

  1. 如果parent的平衡因子等于-1或者1(说明原先是0,左右登高,插入节点后使左子树或右子树增高了)。表明还需要继续往上更新平衡因子
  2. 如果parent的平衡因子等于0(说明原先是1或-1,一高一低,插入节点后填上了矮的那一方)表明无需更新平衡因子了
  3. 如果parent的平衡因子等于-2或者2(说明原先是1或-1,一高一低,插入节点后填上了高的那一方),表明此时以parent结点为根结点的子树已经不平衡了,需要进行旋转处理
  • 5、针对不合规的平衡因子进行旋转调整:

当父亲parent的平衡因子为2或-2时,就要进行旋转调整了,而又要分为以下4类进行旋转:

  1. 当parent的平衡因子为2,cur的平衡因子为1时,进行左单旋。
  2. 当parent的平衡因子为-2,cur的平衡因子为-1时,进行右单旋
  3. 当parent的平衡因子为-2,cur的平衡因子为1时,进行左右双旋。
  4. 当parent的平衡因子为2,cur的平衡因子为-1时,进行右左双旋。

代码如下:

//Insert插入
bool Insert(const pair<K, V>& kv)
{
	//1、一开始为空树,直接new新节点
	if (_root == nullptr)
	{
		//如果_root一开始为空树,直接new一个kv的节点,更新_root和_bf
		_root = new Node(kv);
		_root->_bf = 0;
		return true;
	}
	//2、寻找插入的合适位置
	Node* cur = _root;//记录插入的位置
	Node* parent = nullptr;//保存parent为cur的父亲
	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
		{
			//插入的值 = 节点的值,数据冗余插入失败,返回false
			return false;
		}
	}
	//3、找到了插入的位置,进行父亲与插入节点的链接
	cur = new Node(kv);
	if (parent->_kv.first < kv.first)
	{
		//插入的值 > 父亲的值,链接在父亲的右边
		parent->_right = cur;
	}
	else
	{
		//插入的值 < 父亲的值,链接在父亲的左边
		parent->_left = cur;
	}
	//因为是三叉连,插入后记得双向链接(孩子链接父亲)
	cur->_parent = parent;
	//4、更新新插入节点的祖先的平衡因子
	while (parent)//最远要更新到根
	{
		if (cur == parent->_right)
		{
			parent->_bf++;//新增结点在parent的右边,parent的平衡因子++
		}
		else
		{
			parent->_bf--;//新增结点在parent的左边,parent的平衡因子 --
		}
		//判断是否继续更新?
		if (parent->_bf == 0)// 1 or -1 -> 0 填上了矮的那一方
		{
			//1 or -1 -》 0 填上了矮的那一方,此时正好,无需更新
			break;
		}
		else if (parent->_bf == 1 || parent->_bf == -1)
		{
			//0 -》 1或-1  此时说明插入节点导致一边变高了,继续更新祖先
			cur = cur->_parent;
			parent = parent->_parent;
		}
		else if (parent->_bf == 2 || parent->_bf == -2)
		{
			//1 or -1 -》2或-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);//右左双旋
			}
			break;
		}
		else
		{
			//插入之前AVL树就存在不平衡树,|平衡因子| >= 2的节点
			//实际上根据前面的判断不可能走到这一步,不过这里其实是为了检测先前的插入是否存在问题
			assert(false);
		}
	}
	return true;
}

6. AVL树的旋转

AVL树的旋转分为4种:

  1. 左单旋
  2. 右单旋
  3. 左右双旋
  4. 右左双旋

AVL树的旋转要遵循下面两个原则:

  • 1、保持搜索树的规则
  • 2、子树变平衡

6.1 左单旋

  • 条件:新节点插入较高右子树的右侧

  左单旋的情况非常多,这里我们画一张抽象图来演示:

这里的长方形条(a、b、c)表示的是子树,h为子树的高度,而30和60为实打实的节点。上述左单旋操作主要是完成了四件事:

  1. 让subRL变成parent的右子树,更新subRL的父亲为parent;
  2. 让subR变成根节点;
  3. 让parent变成subR的左子树,更新parent的父亲为subR;
  4. 更新平衡因子。

注意:

  1. parent可能为整棵树的一个子树,则需要链接parent的父亲和subR。
  2. subRL可能为空,但是更新subRL的父亲为parent是建立在subRL不为空的前提下完成的。

解释为何上述左单旋的可行性:

  • 首先,根据底层二叉搜索树的结构:b节点的值肯定是在30~60之间的,b去做30的右子树没有任何问题,且这里把60挪到根部,随即把30作为60的右子树,这样整体的变化就像是一个左旋一样,而且也满足二叉搜索树的性质且均平衡。

代码如下:

void RotateL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	Node* ppNode = parent->_parent;//提前保持parent的父亲
	//1、建立parent和subRL之间的关系
	parent->_right = subRL;
	if (subRL)//防止subRL为空
	{
		subRL->_parent = parent;
	}
	//2、建立subR和parent之间的关系
	subR->_left = parent;
	parent->_parent = subR;
	//3、建立ppNode和subR之间的关系
	if (parent == _root)
	{
		_root = subR;
		_root->_parent = nullptr;
	}
	else
	{
		if (parent == ppNode->_left)
		{
			ppNode->_left = subR;
		}
		else
		{
			ppNode->_right = subR;
		}
		subR->_parent = ppNode;//三叉链双向链接关系
	}
	//4、更新平衡因子
	subR->_bf = parent->_bf = 0;
}

6.2 右单旋

  •  条件:新节点插入较高左子树的左侧

图示:

同样这里的长方形条(a、b、c)表示的是子树,h为子树的高度,而30和60为实打实的节点。上述左单旋操作主要是完成了四件事:

  1. 让subLR变成parent的左子树,更新subLR的父亲为parent
  2. 让subL变成根节点
  3. 让parent变成subL的右子树,更新parent的父亲为subL
  4. 更新平衡因子

注意:

  1. parent可能为整棵树的一个子树,则需要链接parent的父亲和subL。
  2. subLR可能为空,但是更新subLR的父亲为parent是建立在subLR不为空的前提下完成的。

代码如下:
 

//2、右单旋
void RotateR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	Node* ppNode = parent->_parent;
	//1、建立parent和subLR之间的关系
	parent->_left = subLR;
	if (subLR)
	{
		subLR->_parent = parent;
	}
	//2、建立subL和parent之间的关系
	subL->_right = parent;
	parent->_parent = subL;
	//3、建立ppNode和subL的关系
	if (parent == _root)
	{
		_root = subL;
		_root->_parent = nullptr;
	}
	else
	{
		if (parent == ppNode->_left)
		{
			ppNode->_left = subL;
		}
		else
		{
			ppNode->_right = subL;
		}
		subL->_parent = ppNode;//三叉链双向关系
	}
	//4、更新平衡因子
	subL->_bf = parent->_bf = 0;
}

6.3 左右双旋 

  • 条件:新节点插入较高左子树的右侧

接下来图示解析左右双旋的具体解法。

  • 1、插入新节点:

此类模型既不满足左单旋的条件也不满足右单旋的条件,但是我们可以将其组合起来,即左右双旋(先左单旋,再右单旋)的办法重新建立平衡。接下来执行下一步左单旋:

  • 2、以节点30(subL)为旋转点左单旋:

此时再观察这幅图,这部就是一个妥妥的右单旋模型吗,把60的左子树看成一个整体,此时新插入的节点即插入较高左子树的左侧,刚好符合右单旋的性质,接下来即可进行右单旋:

  • 3、以节点90(parent)为旋转点进行右单旋:

此时左右双旋已经完成,最后一步为更新平衡因子,但是更新平衡因子又分如下三类:

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

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

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

这里可以看出唯有subLR平衡因子为0的情况下在进行左右旋转后,三个节点的平衡因子都要更新为0。

  • 代码如下:
//3、左右双旋
void RotateLR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;
	int bf = subLR->_bf;//提前记录subLR的平衡因子
	//1、以subL为根传入左单旋
	RotateL(subL);
	//2、以parent为根传入右单旋
	RotateR(parent);
	//3、重新更新平衡因子
	if (bf == 0)
	{
		parent->_bf = 0;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
	else if (bf == 1)
	{
		parent->_bf = 0;
		subL->_bf = -1;
		subLR->_bf = 0;
	}
	else if (bf == -1)
	{
		parent->_bf = 1;
		subL->_bf = 0;
		subLR->_bf = 0;
	}
	else
	{
		assert(false);//此时说明旋转前就有问题,检查
	}
}

6.4 右左双旋

  • 条件:新节点插入较高右子树的左侧

接下来图示解析左右双旋的具体解法。

  • 1、插入新节点:

注意这里的新节点插在了较高右子树的左侧,不能用上文的单旋转以及左右旋转,相反而应使用右左旋转(先右单旋,再左单旋)来解决,接下来执行下一步右单旋:

  • 2、以节点90(subR)为旋转点右单旋:

 此时再观察这幅图,这部就是一个妥妥的左单旋模型吗,把60的右子树看成一个整体,此时新插入的节点即插入较高右子树的右侧,刚好符合左单旋的性质,接下来即可进行左单旋:

  • 3、以节点30(parent)为旋转点左单旋:

此时左右双旋已经完成,最后一步为更新平衡因子,但是更新平衡因子又分如下三类:

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

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

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

 这里可以看出唯有subRL平衡因子为0的情况下在进行左右旋转后,三个节点的平衡因子都要更新为0。

  • 代码如下:
//4、右左双旋
void RotateRL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	int bf = subRL->_bf;//提前记录subLR的平衡因子
	//1、以subL为根传入左单旋
	RotateR(subR);
	//2、以parent为根传入右单旋
	RotateL(parent);
	//3、重新更新平衡因子
	if (bf == 0)
	{
		parent->_bf = 0;
		subR->_bf = 0;
		subRL->_bf = 0;
	}
	else if (bf == 1)
	{
		parent->_bf = -1;
		subR->_bf = 0;
		subRL->_bf = 0;
	}
	else if (bf == -1)
	{
		parent->_bf = 0;
		subR->_bf = 1;
		subRL->_bf = 0;
	}
	else
	{
		assert(false);//此时说明旋转前就有问题,检查
	}
}

7. AVL树的验证

AVL树是在二叉搜索树的基础上加入了平衡性的限制,因此要验证AVL树,就是看它是否为二叉搜索树,以及是否为一颗平衡树。接下来分别讨论:

  • 1、验证其为二叉搜索树:

这里我们只需要进行中序遍历看看结果是否可得到一个有序的序列,如果可以则证明是二叉搜索树,而中序遍历的实现非常简单,先前的二叉搜索树的实现已然完成过,这里直接给出代码:

//中序遍历的子树
void _InOrder(Node* root)
{
	if (root == nullptr)
		return;
 
	_InOrder(root->_left);
	cout << root->_kv.first << " ";
	_InOrder(root->_right);
}
//中序遍历
void InOrder()
{
	_InOrder(_root);
	cout << endl;
}

检测是否为二叉搜索树的代码实现后,接下来开始验证是否为平衡树。

  • 2、验证其为平衡树:

规则如下:(递归的思想)

  1. 空树也是平衡树,一开始就要判断
  2. 封装一个专门计算高度的函数(递归计算高度)后续用来计算高度差(平衡因子diff)
  3. 如过diff不等于root的平衡因子(root->_bf),或root平衡因子的绝对值超过1,则一定不是AVL树
  4. 继续递归到子树&&右树,直至结束

代码如下:

//验证一棵树是否为平衡树
bool IsBalanceTree()
{
	return _IsBalanceTree(_root);
}
//判读是否平衡的子树
bool _IsBalanceTree(Node* root)
{
	//空树也是AVL树
	if (nullptr == root)
		return true;
	//计算root节点的平衡因子diff:即root左右子树的高度差
	int leftHeight = _Height(root->_left);
	int rightHeight = _Height(root->_right);
	int diff = rightHeight - leftHeight;
	//如果计算出的平衡因子与root的平衡因子不相等,或root平衡因子的绝对值超过1,则一定不是AVL树
	if ((abs(diff) > 1))
	{
		cout << root->_kv.first << "节点平衡因子异常" << endl;
		return false;
	}
	if (diff != root->_bf)
	{
		cout << root->_kv.first << "节点平衡因子与root的平衡因子不等,不符合实际" << endl;
		return false;
	}
	//继续递归检测,直到结束
	return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
}
//求高度的子树
int _Height(Node* root)
{
	if (root == nullptr)
		return 0;
	int lh = _Height(root->_left);
	int rh = _Height(root->_right);
	return lh > rh ? lh + 1 : rh + 1;
}

综合上述两大步骤的操作即可对一棵树充分的验证是否为AVL树。

8. AVL树的查找

Find查找函数的思路很简单,定义cur指针从根部开始按如下规则遍历:

  1. 若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。
  2. 若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。
  3. 若key值等于当前结点的值,则查找成功,返回true。
  4. 若遍历一圈cur走到nullptr了说明没有此结点,返回false。
//Find查找
bool Find(const K& key)
{
	Node* cur = _root;
	while (cur)
	{
		if (cur->_key < key)
		{
			cur = cur->_right;//若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。
		}
		else if (cur->_key > key)
		{
			cur = cur->_left;//若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。
		}
		else
		{
			return true;//若key值等于当前结点的值,则查找成功,返回true。
		}
	}
	return false;//遍历一圈没找到返回false
}

9. AVL树的删除

删除了解即可。

因为AVL树也是二叉搜索树,主要是三个大思路:

  1. 按二叉搜索树的规则删除
  2. 更新平衡因子
  3. 出现不平衡,需要旋转调整

只不过与搜索二叉树的删除不同的是,删除节点后的平衡因子需要不断更新,最差情况下一直要调整到根节点的位置。具体这里就不再实现了,因为着实有点复杂。不过《算法导论》或《数据结构-用面向对象方法与C++描述》殷人昆版这两本书上是有详细的讲解的哈。

10. AVL树的性能

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

11. 总代码

11.1 AVLTree

#pragma once
#include<queue>
#include<vector>
#include<iostream>
using namespace std;
//节点类
template<class K, class V>
struct AVLTreeNode
{
	//存储的键值对
	pair<K, V> _kv;
	//三叉连结构
	AVLTreeNode<K, V>* _left;//左孩子
	AVLTreeNode<K, V>* _right;//右孩子
	AVLTreeNode<K, V>* _parent;//父亲
	//平衡因子_bf
	int _bf;//右子树 - 左子树的高度差
	//构造函数
	AVLTreeNode(const pair<K, V>& kv)
		:_kv(kv)
		, _left(nullptr)
		, _right(nullptr)
		, _parent(nullptr)
		, _bf(0)
	{}
};
//AVL树的类
template<class K, class V>
class AVLTree
{
	typedef AVLTreeNode<K, V> Node;
public:
	//1、按照搜索树的规则插入
	//2、是否违反平衡规则,如果违反就需要处理:旋转
	bool Insert(const pair<K, V>& kv)
	{	
		//1、一开始为空树,直接new新节点
		if (_root == nullptr)
		{
			//如果_root一开始为空树,直接new一个kv的节点,更新_root和_bf
			_root = new Node(kv);
			_root->_bf = 0;
			return true;
		}
		//2、寻找插入的合适位置
		Node* cur = _root;//记录插入的位置
		Node* parent = nullptr;//保存parent为cur的父亲
		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
			{
				//插入的值 = 节点的值,数据冗余插入失败,返回false
				return false;
			}
		}
		//3、找到了插入的位置,进行父亲与插入节点的链接
		cur = new Node(kv);
		if (parent->_kv.first < kv.first)
		{
			//插入的值 > 父亲的值,链接在父亲的右边
			parent->_right = cur;
		}
		else
		{
			//插入的值 < 父亲的值,链接在父亲的左边
			parent->_left = cur;
		}
		//因为是三叉连,插入后记得双向链接(孩子链接父亲)
		cur->_parent = parent;
		//4、更新新插入节点的祖先的平衡因子
		while (parent)//最远要更新到根
		{
			if (cur == parent->_right)
			{
				parent->_bf++;//新增结点在parent的右边,parent的平衡因子++
			}
			else
			{
				parent->_bf--;//新增结点在parent的左边,parent的平衡因子 --
			}
			//判断是否继续更新?
			if (parent->_bf == 0)// 1 or -1 -> 0 填上了矮的那一方
			{
				//1 or -1 -》 0 填上了矮的那一方,此时正好,无需更新
				break;
			}
			else if (parent->_bf == 1 || parent->_bf == -1)
			{
				//0 -》 1或-1  此时说明插入节点导致一边变高了,继续更新祖先
				cur = cur->_parent;
				parent = parent->_parent;
			}
			else if (parent->_bf == 2 || parent->_bf == -2)
			{
				//1 or -1 -》2或-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);//右左双旋
				}
				break;
			}
			else
			{
				//插入之前AVL树就存在不平衡树,|平衡因子| >= 2的节点
				//实际上根据前面的判断不可能走到这一步,不过这里其实是为了检测先前的插入是否存在问题
				assert(false);
			}
		}
		return true;
	}
//求一棵树的高度
	int Height()
	{
		return _Height(_root);
	}
//验证是否为一颗搜索二叉树
	void InOrder()
	{
		_InOrder(_root);//调用中序遍历子树
		cout << endl;
	}

//验证一棵树是否为平衡树
	bool IsBalanceTree()
	{
		return _IsBalanceTree(_root);
	}

private:
//1、左单旋
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		Node* ppNode = parent->_parent;//提前保持parent的父亲
		//1、建立parent和subRL之间的关系
		parent->_right = subRL;
		if (subRL)//防止subRL为空
		{
			subRL->_parent = parent;
		}
		//2、建立subR和parent之间的关系
		subR->_left = parent;
		parent->_parent = subR;
		//3、建立ppNode和subR之间的关系
		if (parent == _root)
		{
			_root = subR;
			_root->_parent = nullptr;
		}
		else
		{
			if (parent == ppNode->_left)
			{
				ppNode->_left = subR;
			}
			else
			{
				ppNode->_right = subR;
			}
			subR->_parent = ppNode;//三叉链双向链接关系
		}
		//4、更新平衡因子
		subR->_bf = parent->_bf = 0;
	}
//2、右单旋
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		Node* ppNode = parent->_parent;
		//1、建立parent和subLR之间的关系
		parent->_left = subLR;
		if (subLR)
		{
			subLR->_parent = parent;
		}
		//2、建立subL和parent之间的关系
		subL->_right = parent;
		parent->_parent = subL;
		//3、建立ppNode和subL的关系
		if (parent == _root)
		{
			_root = subL;
			_root->_parent = nullptr;
		}
		else
		{
			if (parent == ppNode->_left)
			{
				ppNode->_left = subL;
			}
			else
			{
				ppNode->_right = subL;
			}
			subL->_parent = ppNode;//三叉链双向关系
		}
		//4、更新平衡因子
		subL->_bf = parent->_bf = 0;
	}
//3、左右双旋
	void RotateLR(Node* parent)
	{
		Node* subL = parent->_left;
		Node* subLR = subL->_right;
		int bf = subLR->_bf;//提前记录subLR的平衡因子
		//1、以subL为根传入左单旋
		RotateL(subL);
		//2、以parent为根传入右单旋
		RotateR(parent);
		//3、重新更新平衡因子
		if (bf == 0)
		{
			parent->_bf = 0;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else if (bf == 1)
		{
			parent->_bf = 0;
			subL->_bf = -1;
			subLR->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 1;
			subL->_bf = 0;
			subLR->_bf = 0;
		}
		else
		{
			assert(false);//此时说明旋转前就有问题,检查
		}
	}
//4、右左双旋
	void RotateRL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;
		int bf = subRL->_bf;//提前记录subLR的平衡因子
		//1、以subL为根传入左单旋
		RotateR(subR);
		//2、以parent为根传入右单旋
		RotateL(parent);
		//3、重新更新平衡因子
		if (bf == 0)
		{
			parent->_bf = 0;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == 1)
		{
			parent->_bf = -1;
			subR->_bf = 0;
			subRL->_bf = 0;
		}
		else if (bf == -1)
		{
			parent->_bf = 0;
			subR->_bf = 1;
			subRL->_bf = 0;
		}
		else
		{
			assert(false);//此时说明旋转前就有问题,检查
		}
	}

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

		_InOrder(root->_left);
		cout << root->_kv.first <<" ";
		_InOrder(root->_right);
	}
	//求一棵树的高度的子树
	int _Height(Node* root)
	{
		if (root == nullptr)
			return 0;
		int lh = _Height(root->_left);
		int rh = _Height(root->_right);
		return lh > rh ? lh + 1 : rh + 1;
	}
	//判读是否平衡的子树
	bool _IsBalanceTree(Node* root)
	{
		//空树也是AVL树
		if (nullptr == root)
			return true;
		//计算root节点的平衡因子diff:即root左右子树的高度差
		int leftHeight = _Height(root->_left);
		int rightHeight = _Height(root->_right);
		int diff = rightHeight - leftHeight;
		//如果计算出的平衡因子与root的平衡因子不相等,或root平衡因子的绝对值超过1,则一定不是AVL树
		if ((abs(diff) > 1))
		{
			cout << root->_kv.first << "节点平衡因子异常" << endl;
			return false;
		}
		if (diff != root->_bf)
		{
			cout << root->_kv.first << "节点平衡因子与root的平衡因子不等,不符合实际" << endl;
			return false;
		}
		//继续递归检测,直到结束
		return _IsBalanceTree(root->_left) && _IsBalanceTree(root->_right);
	}
public:
	vector<vector<int>> levelOrder() {
		vector<vector<int>> vv;
		if (_root == nullptr)
			return vv;

		queue<Node*> q;
		int levelSize = 1;
		q.push(_root);

		while (!q.empty())
		{
			// levelSize控制一层一层出
			vector<int> levelV;
			while (levelSize--)
			{
				Node* front = q.front();
				q.pop();
				levelV.push_back(front->_kv.first);
				if (front->_left)
					q.push(front->_left);

				if (front->_right)
					q.push(front->_right);
			}
			vv.push_back(levelV);
			for (auto e : levelV)
			{
				cout << e << " ";
			}
			cout << endl;
			// 上一层出完,下一层就都进队列
			levelSize = q.size();
		}
		return vv;
	}
	
private:
	Node* _root = nullptr;
};

11.2 Test.cpp

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<vector>
#include<time.h>
#include<assert.h>
using namespace std;
#include"AVLTree.h"

void TestAVLTree1()
{
	int a[] = { 1,2,3,4,5,6,7,8 };
	AVLTree<int, int> t;
	t.Insert(make_pair(1, 1));
	t.Insert(make_pair(2, 2));
	t.Insert(make_pair(3, 3));
}
void TestAVLTree2()
{
	//int a[] = { 1,2,3,4,5,6,7,8 };
	int a[] = { 8,7,6,5,4,3,2,1 };
	AVLTree<int, int> t;
	for (auto e : a)
	{
		t.Insert(make_pair(e, e));
	}
}
void TestAVLTree3()
{
	const size_t N = 1024;
	vector<int> v;
	srand(time(0));
	v.reserve(N);
	for (size_t i = 0; i < N; i++)
	{
		//v.push_back(i);
		v.push_back(rand());
	}
	AVLTree<int, int> t;
	for (auto e : v)
	{
		t.Insert(make_pair(e, e));
	}
	t.levelOrder();
	cout << endl;
	t.InOrder();
}
void TestAVLTree4()
{
	const size_t N = 1024 * 1024;
	vector<int> v;
	srand(time(0));
	v.reserve(N);
	for (size_t i = 0; i < N; i++)
	{
		v.push_back(i);//有序插入 --》检测单旋是否正确
		//v.push_back(rand());//乱序插入 --》检测双旋是否正确
	}
	AVLTree<int, int> t;
	for (auto e : v)
	{
		t.Insert(make_pair(e, e));
	}
	cout << "是否平衡?" << t.IsBalanceTree() << endl;
	cout << "高度:" << t.Height() << endl;
	//t.InOrder();//判断是否二叉搜索树
}
int main()
{
	//TestAVLTree1();
	//TestAVLTree2();
	//TestAVLTree3();
	TestAVLTree4();
}

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

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

相关文章

mac本地创建ssh key连接github

起因 今天克隆自己github上面的笔记到新电脑上&#xff0c;用http连接进行克隆&#xff0c;然后要我输入账号密码&#xff0c;输入完报了个提示“remote: Support for password authentication was removed on August 13, 2021. Please use a personal access token instead.”…

JavaScript 手写代码 第一期

文章目录 1.为什么要手写代码&#xff1f;2.手写代码2.1 手写Object.create()方法2.1.1 基本使用2.1.2 使用实例2.1.3 手写实现 2.2 手写实现instanceof方法2.2.1 基本使用2.2.2 使用实例2.2.3 手写实现 2.3 手写实现new操作符2.3.1 基本使用2.3.2 使用实例2.3.3 手写实现 1.为…

分享一个下载按钮

先看效果&#xff1a; 再看代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>下载按钮</title><link href"https://fonts.googleapis.com/css2?familyHind&amp;d…

Redisson源码-单线程加解锁流程

Redisson源码-单线程加解锁流程 以下源码分析基于redisson-3.17.6版本&#xff0c;不同版本源码会有些许不同需注意。 <dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.17.6</version>&l…

推荐5 款好用的 Linux 音乐播放器

目前 Linux 上有几十个音乐播放器&#xff0c;这使得找到一个最好用的变成很困难。之前我们已经回顾了其中的一些播放器&#xff0c;如 Cantata&#xff0c;Exaile&#xff0c;甚至不那么出名的 Clementine&#xff0c;Nightingale 和 Quod Libet。 在本篇文章中我将涵盖更多的…

python学习——pandas数据处理 时间序列案例 matplotlib绘图案例

目录 pandas数据处理1.合并数据1) 堆叠合并2) 主键合并3) 重叠合并 2.分组和聚合3.索引和符合索引4.去除重复值5.处理缺失值6.处理离群值7.标准化数据1) 离差标准化函数2) 标准差标准化函数3) 小数定标差标准化函数 8.转换数据--离散处理9.时间序列【案例】时间序列案例案例1&a…

C++测试

开始对C嘎嘎下手&#xff01; 1.有关char数组 定义长度为5&#xff0c;但是实际长度是定义长度减1 突然就想到计网安全中的栈溢出问题了&#xff0c;C语言是不检查你是否越界的&#xff0c;如果通过让实参溢出覆盖掉原程序的返回地址&#xff0c;通过精心控制是可以让计算机执…

高级数据结构——红黑树

目录 1. 红黑树的概念 2. 红黑树的性质 3. 红黑树 6. 红黑树的验证 7. 红黑树的删除 8. 红黑树与AVL数的比较 9. 红黑树的应用 10. 完整代码 10.1 RBTree.h 10.2 test.cpp 1. 红黑树的概念 红黑树&#xff0c;是一种二叉搜索树&#xff0c;但在每个结点上增加一个存…

49天精通Java,第37天,可变参数列表

目录 一、可变参数列表二、可变参数列表的优缺点1、优点2、缺点 三、可变参数列表的适用场景1、函数重载2、命令行参数解析3、集合操作4、函数式编程 大家好&#xff0c;我是哪吒。 &#x1f3c6;本文收录于&#xff0c;49天精通Java从入门到就业。 全网最细Java零基础手把手…

SpringBoot 如何使用 @ResponseStatus 注解处理异常状态码

SpringBoot 如何使用 ResponseStatus 注解处理异常状态码 在 SpringBoot 应用程序中&#xff0c;异常处理是一个非常重要的话题。当应用程序出现异常时&#xff0c;我们需要对异常进行处理&#xff0c;以保证应用程序的稳定性和可靠性。除了使用异常处理器外&#xff0c;Sprin…

重新理解微服务之终究绕不过这4个坎之(一)

写在前头 大家曾经有没有遇过日常技术交流的时候&#xff0c;会讨论某某技术之间的关系是什么&#xff0c;某些技术是否应该用到微服务。我相信热爱技术交流的您&#xff0c;就算不是在微服务这里领域&#xff0c;或多或少都会跟其他同行会做一些争议话题的探讨&#xff0c;而…

华为OD机试真题B卷 JavaScript 实现【字符串分隔】,附详细解题思路

一、题目描述 输入一个字符串&#xff0c;请按长度为8拆分每个输入字符串并进行输出&#xff0c;长度不是8整数倍的字符串请在后面补数字0&#xff0c;空字符串不处理。 二、输入描述 连续输入字符串(每个字符串长度小于等于100)。 三、输出描述 依次输出所有分割后的长度…

k8s使用ceph存储

文章目录 初始化操作k8s使用ceph rbdvolumePV静态pv动态pv k8s使用cephfsvolume静态pv 初始化操作 ceph创建rbd存储池 ceph osd pool create k8s-data 32 32 replicated ceph osd pool application enable k8s-data rbd rbd pool init -p k8s-dataceph添加授权&#xff0c;需…

指针和数组--指针数组及其应用

目录 一、指针数组用于表示多个字符串 二、指针数组用于表示命令行参数 一、指针数组用于表示多个字符串 一维数组可存储一个字符串&#xff0c;二维数组可存储多个字符串。 二维数组的元素在内存中是连续存放的&#xff0c;存完第一行后&#xff0c;再存第二行&#xff0c;以…

多线程之JUC

写在前面 本文一起看下jdk并发包的相关内容。 1&#xff1a;JUC包提供了哪些功能 先通过包结构看下JUC提供的功能&#xff1a; 接下来分别看下。 1.1&#xff1a;锁 JUC中的锁机制提供了比synchronized&#xff0c;wait/notify更加灵活的同步控制&#xff0c;在java.util.…

大数据基础平台实施及运维进阶

1、完全分布式部署介绍 完全分部式是真正利用多台Linux主机来进行部署Hadoop&#xff0c;对Linux机器集群进行规划&#xff0c;使得Hadoop各个模块分别部署在不同的多台机器上。 2、nameNode HA完全分布式部署 2.1、nameNode切换方法 分别处于Active和Standby中 hadoop可以…

操作系统复习笔记4

1、queueType队列类型 队列中的数据也呈线性排列。虽然与栈有些相似&#xff0c;但队列中添加和删除数据的操作分别是在两端进行的。 线性表有顺序存储和链式存储&#xff0c;队列作为一种特殊的线性表&#xff0c;也同样存在这两种存储方式。 1.1 顺序队列 用数组存储队列…

C语言学习(二十五)---指针练习题(一)

在上一节内容中&#xff0c;我们学习了递归与冒泡排序法的有关内容&#xff0c;今天我们将继续往下学习&#xff0c;主要内容为指针练习题&#xff0c;好了&#xff0c;话不多说&#xff0c;开整&#xff01;&#xff01;&#xff01; 在之前的第18—22的内容中&#xff0c;我…

lnmp框架的应用

目录 应用一 nginx访问状态统计 1.先查看http_stub_status有没有安装 2.进入nginx的配置文件改配置 3.nginx-检查配置 重启服务 最后这个20就是显示的状态统计 应用二 给网站加密 1.首先安装http-tools软软件 2.把nginx设置锁也要有执行权限 3.进入nginx配置文件 4. 检查…

【Windows个性化设置篇】StartAllBack更改win11任务栏设置

【Windows个性化设置篇】StartAllBack更改win11任务栏设置 Windows11目前不支持更改任务栏位置固定的修改&#xff0c;因为想把任务栏固定到旁边&#xff0c;从而充分利用电脑屏幕位置。之前试过TranslucentTB可以把任务栏透明化&#xff0c;很漂亮&#xff0c;但在分屏操作时…