【C++进阶】AVL树详解

news2024/9/21 10:59:51

文章目录

  • 1. AVL树的概念
  • 2. AVL树结点的定义
  • 3. AVL 树的插入
    • 3.1 关于平衡因子
    • 3.2 插入代码
  • 4. AVL 树的旋转逻辑
    • 4.1 不需要旋转
    • 4.2 左旋
    • 4.3 右旋
    • 4.4 双旋
      • 4.4.1 先右后左单旋(RL 旋转)
      • 4.4.2 先左后右单旋(LR 旋转)
    • 4.5 完整插入代码(插入+旋转)
  • 5. AVL 树的验证
    • 5.1 中序遍历打印和计算树的高度
    • 5.2 验证
    • 5.3 数据测试

二叉搜索树虽可以缩短查找的效率,但 如果数据有序或接近有序二叉搜索树将退化为单支树,查找元素相当于在顺序表中搜索元素,效率低下。
所以就发明了 AVL 树。

1. AVL树的概念

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

  • 它的左右子树都是 AVL 树
  • 左右子树高度之差(简称平衡因子)的绝对值不超过 1(1/0/-1)
    结点是一个个插入的,有些情况无法做到高度差等于 0(如:2 个节点等偶数个结点)
    平衡因子 = 右子树高度 − 左子树高度 平衡因子=右子树高度-左子树高度 平衡因子=右子树高度左子树高度(我们这里是如此实现)
    平衡因子并不是必须的,它只是一种控制方式,帮助我们更便捷地控制树
    如果一棵二叉搜索树是高度平衡的,它就是AVL树。如果它有n个结点,其高度可保持在 O ( l o g 2 n ) O(log_2 n) O(log2n),搜索时间复杂度为 O ( l o g 2 n ) O(log_2 n) O(log2n)

2. AVL树结点的定义

template<class K, class V>
struct AVLTreeNode
{
	AVLTreeNode<K, V>* _left;
	AVLTreeNode<K, V>* _right;
	AVLTreeNode<K, V>* _parent;
	int _bf;//balance factor 平衡因子
	pair <K, V> _kv;

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

3. AVL 树的插入

ppParentcpCur

  1. =】哦怕【、按二叉搜索树规则插入(复用代)
  2. 更新平衡因子
    1. 插入结点会影响哪些结点的平衡因子呢?——新增结点的部分祖先
  3. 更新原则:
    1. cp 的左边,p->bf--
    2. cp 的右边,p->bf++
      是否继续更新取决于 p 的高度是否变化,是否会影响爷爷结点

3.1 关于平衡因子

pCur 插入后,pParent 的平衡因子一定需要调整,在插入之前,pParent 的平衡因子分为三种情况:-1,0, 1, 分以下两种情况:

  1. 如果pCur插入到pParent的左侧,只需给pParent的平衡因子-1即可
  2. 如果pCur插入到pParent的右侧,只需给pParent的平衡因子+1即可

检查平衡因子

  1. 更新后,p->bf ==0,p 所在的子树高度不变,不会影响爷爷。说明更新前,p 的 bf 是 1 或者-1,p 的矮的那边插入了节点,左右均衡了,p 的高度不变,不会影响爷爷——更新结束
    image.png

  2. 更新后,p->bf==1/-1,p 所在的子树的高度变了,会影响爷爷说明更新前,p 的 bf 是 0,p 的有一边插入,p 变得不均衡,但是不违反规则, p 的高度变了,会影响爷爷,需要往上检查一下——继续往上更新(往上结点走更新规则)
    image.png

  3. 更新后,p->bf==2/-2, 说明 p 所在的子树违反了平衡规则——处理 ->旋转
    image.png

结束条件

  1. c 更新到 root 位置
  2. 更新后,p->bf ==0 结束
  3. 旋转后结束
    旋转让 p 所在的子树高度回到插入之前的状态,不会对上层的 bf 有影响

3.2 插入代码

bool Insert(const pair<K, V>& kv)
{
	//和二叉搜索树的插入一样的逻辑,复用
	if (_root == nullptr)
	{
		_root = new Node(kv);
		return true;
	}

	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)//更新parent和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;
		}
	}

	//更新结点后,开辟出新结点,并使parent结点指向cur
	//因为在指向cur前的parent是树末端结点,指向nullptr
	cur = new Node(kv);
	if (parent->_kv.first < kv.first)
	{
		parent->_right = cur;
	}
	else
	{
		parent->_left = cur;
	}
	cur->_parent = parent;//容易忽略
	//更新cur里的parent指向——可以理解为二叉搜索树是树状的双向链表

	//更新双亲的平衡因子
	while (parent)
	{
		//继续向上更新(循环)
		//直到根节点或遇到一个平衡因子为0的节点为止
	
		if (cur == parent->_left)//新结点在当前的parent(那一树)左边插入,父节点的平衡因子--
		{
			parent->_bf--;
		}
		else//新结点在当前的parent(那一树)右边插入,父节点的平衡因子++
		{
			parent->_bf++;
		}


		if (parent->_bf == 0)//插入之前parent的平衡因子为正负1,插入后被调整成0,此时满足AVL树的性质,插入成功
		{
			break;
		}
		else if (parent->_bf == 1 || parent->_bf == -1)
		{
			//如果parent的平衡因子为正负1,
			// 说明插入前parent的平衡因子一定为0,插入后被更新成正负1,
			// 此时以parent为根的树的高度增加,需要继续向上更新
			cur = cur->_parent;
			parent = parent->_parent;
		}
		
		//检查是否需要旋转
		else if (parent->_bf == 2 || parent->_bf == -2)
		{
			//如果parent的平衡因子为正负2,
			// 则parent的平衡因子违反平衡树的性质,需要对其进行旋转处理
			
			//旋转处理
			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
			{
				RotateRL(parent);
			}
			break;
		}
		else//插入之前AVL树就有问题
		{
			assert(false);
		}

	}
	
	//不需要旋转,或者已经通过旋转恢复树的平衡,那么插入就完成了
	return true;
}

4. AVL 树的旋转逻辑

下面动画演示了不断将节点插入 AVL 树时的情况,并且演示了左旋(Left Rotation)、右旋(Right Rotation)、右左旋转(Right-Left Rotation)、左右旋转(Left-Right Rotation)以及带子树的右旋(Right Rotation with children)
4aa211fa83339206c904e08ab2576bca.gif|585
当插入一个新结点后当前 parent 平衡因子为 2/-2,这时候需要旋转调整,将以 parent 为根的这棵树调整为 AVL 树。
这时候还要分四种情况:

  • 新结点插入到左子树的左侧。这时候我们需要右单旋。
  • 新结点插入到右子树的右侧。这时候我们需要左单旋。
  • 新结点插入到右子树的左侧。这时候我们需要先右单旋再左单旋。
  • 新结点插入到左子树的右侧。这时候我们需要先左单旋再右单旋。

旋转目的

  1. 保持搜索规则
  2. 当前树从不平衡旋转为平衡
  3. 降低树的高度

旋转有 2 个作用

  1. 让左右子树均衡
  2. 同时使高度下降(或者保持旋转前高度)

4.1 不需要旋转

每个结点的平衡因子都在允许的范围内(1/0/-1)
image.png

4.2 左旋

新结点插入到右子树的右侧。这时候我们需要左单旋
image.png

  1. 不仅要动两个结点,还要考虑 parent
  2. subRL 可能为空——60节点的左孩子可能存在,也可能不存在
  3. 如果 parent结点 在一棵子树则要跟父亲结点进行链接,如果不是子树,subR 可能要跟根进行链接
    30可能是根节点,也可能是子树
    • 如果是根节点,旋转完成后,要更新根节点
    • 如果是子树,可能是parent的父节点的左子树,也可能是右子树
  4. 平衡因子的更新——旋转结束后,平衡因子需要调整的就两个结点,一个是起始parent结点,一个是起始parent结点的右结点(subR),其他结点的左右高度差没有变化。这两个结点调整后平衡因子都变为0
void RotateL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	//开始重新指向
	parent->_right = subRL;
	if (subRL)//subRL有可能是空树
	//如果该树(结点)存在,则重新指向其父节点
		subRL->_parent = parent;

	subR->_left = parent;
	//还有subR的parent还未处理
	Node* ppnode = parent->_parent;
	parent->_parent = subR;

	if (parent == _root)
	{
		_root = subR;
		subR->_parent = nullptr;
	}
	else//parent结点是在子树里,所以才需要ppnode,也就是爷爷结点
	{
	//subR链接上之前的爷爷结点
		if (ppnode->_left == parent)
		{
			ppnode->_left = subR;
		}
		else
		{
			ppnode->_right = subR;
		}
		subR->_parent = ppnode;
	}
	parent->_bf = 0;
	subR->_bf = 0;
}

4.3 右旋

新结点插入到左子树的左侧。这时候我们需要右单旋。
image.png

  1. 不仅要动两个结点,还要考虑 parent
  2. subRL 可能为空—— 60节点 的左孩子可能存在,也可能不存在
  3. 如果 parent结点 在一棵子树则要跟父亲结点进行链接,如果不是子树,subR 可能要跟根进行链接
    30 可能是根节点,也可能是子树
    • 如果是根节点,旋转完成后,要更新根节点
    • 如果是子树,可能是 parent 的父节点的左子树,也可能是右子树
  4. 平衡因子的更新——旋转结束后,平衡因子需要调整的就两个结点,一个是起始 parent 结点,一个是起始 parent 结点的左结点(subL),其他结点的左右高度差没有变化。这两个结点调整后平衡因子都变为 0

可以和 RotateL 进行比对,代码整体相似,细节除外

void RotateR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	parent->_left = subLR;
	if (subLR)
	{
		subLR->_parent = parent;
	}

	subL->_right = parent;

	Node* ppnode = parent->_parent;
	parent->_parent = subL;

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

4.4 双旋

4.4.1 先右后左单旋(RL 旋转)

新结点插入到右子树的左侧。这时候我们需要先右单旋再左单旋
image.png
注:它 subRL 是分开来(便于细分情况),原本就是一个整体
image.png|375
总的来看,中间值(如:60) 最终会作为(子树的)根,同时 平衡因子 为 0

这里我们看到,parent 的右子树的左侧插入新结点后导致 parent 不平衡,我们的策略是:先对 90 进行左单旋,再对 30 进行右单旋。

这里我们需要注意的是:我们可以调用上面已经写好的左单旋和右单旋的函数,但是,左单旋和右单旋的函数只对它们的 parent 结点及其左结点或者右结点的平衡因子作出了调整,并且都调整为了 0。我们观察上述抽象图,我们调整后,会有三个结点的平衡因子需要作出调整(结点 30、60、90)!

考虑以下三种情况:

  • 新结点插入的是 60 的左子树:插入后三个结点的平衡因子为 30 (bf==-2),90 (bf==-1),60 (bf==-1),调整后的三个平衡因子为 30 (bf==0),90 (bf==1),60 (bf==0)。
    image.png

  • 新结点插入的是 60 的右子树:插入后三个结点的平衡因子为 30 (bf==-2),90 (bf==-1),60 (bf==1),调整后的三个平衡因子为 30 (bf==-1),90 (bf==0),60 (bf==0)。
    image.png

  • 新结点插入的是一棵原本仅有 2 个结点的 AVL 树:插入后的三个结点的平衡因子为 30 (bf==2),90 (bf==-1),60 (bf==0),调整后的三个平衡因子为 30 (bf==0),90 (bf==0),60 (bf==0)。
    image.png

  • 根据旋转前 subRL->_bf 来判断新的结点在哪里插入

  • 并以此来确定如何更新 parentsubR平衡因子

void RotateRL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;
	int bf = subRL->_bf;//记录旋转之前该结点的bf

	RotateR(subR);
	RotateL(parent);
	
	subRL->_bf = 0;
	if (bf == 1)//在c插入
	{
		parent->_bf = -1;
		subR->_bf = 0;
	}
	else if (bf == -1)//在b插入
	{
		parent->_bf = 0;
		subR->_bf = -1;
	}
	else if (bf == 0)//60作为被插入的结点
	{
		parent->_bf = 0;
		subR->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

4.4.2 先左后右单旋(LR 旋转)

新结点插入到左子树的右侧。这时候我们需要先单左旋再右单旋
image.png
parent 的左子树的右侧插入新结点后导致 parent 不平衡,我们的策略是:先对 30 进行左单旋,再对 90 进行右单旋。

这里我们需要注意的是:我们可以调用上面已经写好的左单旋和右单旋的函数,但是,左单旋和右单旋的函数只对它们的 parent 结点及其左结点或者右结点的平衡因子作出了调整,并且都调整为了 0。我们观察上述抽象图,我们调整后,会有三个结点的平衡因子需要作出调整(结点 30、60、90)!

考虑以下三种情况:

  • 新结点插入的是 60 的左子树:插入后三个结点的平衡因子为 90 (bf==-2),30 (bf==1),60 (bf==-1),调整后的三个平衡因子为 90 (bf==1),30 (bf==0),60 (bf==0)。
    image.png

  • 新结点插入的是 60 的右子树):插入后三个结点的平衡因子为 90 (bf==-2),30 (bf==1),60 (bf==1),调整后的三个平衡因子为 90 (bf==0),30 (bf==-1),60 (bf==0)。
    image.png

  • 新结点插入的是一棵原本仅有 2 个结点的 AVL 树:插入后的三个结点的平衡因子为 90 (bf==-2),30 (bf==1),60 (bf==0),调整后的三个平衡因子为 90 (bf==0),30 (bf==0),60 (bf==0)。
    image.png

void RotateLR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	int bf = subLR->_bf;
	RotateL(parent->_left);
	RotateR(parent);

	//旋转结束,重置bf
	if (bf == -1)
	{
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 1;
	}
	else if (bf == 1)
	{
		subLR->_bf = 0;
		subL->_bf = -1;
		parent->_bf = 0;
	}
	else if (bf == 0)
	{
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 0;
	}
	else
	{
		assert(false);
	}
}

4.5 完整插入代码(插入+旋转)

bool Insert(const pair<K, V>& kv)
{
	//和二叉搜索树的插入一样的逻辑,复用
	if (_root == nullptr)
	{
		_root = new Node(kv);
		return true;
	}

	Node* parent = nullptr;
	Node* cur = _root;
	while (cur)//更新parent和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;
		}
	}

	//更新结点后,开辟出新结点,并使parent结点指向cur
	//因为在指向cur前的parent是树末端结点,指向nullptr
	cur = new Node(kv);
	if (parent->_kv.first < kv.first)
	{
		parent->_right = cur;
	}
	else
	{
		parent->_left = cur;
	}
	cur->_parent = parent;//容易忽略
	//更新cur里的parent指向——可以理解为二叉搜索树是树状的双向链表

	//更新双亲的平衡因子
	while (parent)
	{
		if (cur == parent->_left)//新结点在左边插入,父节点的平衡因子--
		{
			parent->_bf--;
		}
		else//新结点在右边插入,父节点的平衡因子++
		{
			parent->_bf++;
		}


		if (parent->_bf == 0)//插入之前parent的平衡因子为正负1,插入后被调整成0,此时满足AVL树的性质,插入成功
		{
			break;
		}
		else if (parent->_bf == 1 || parent->_bf == -1)
		{
			//如果parent的平衡因子为正负1,
			// 说明插入前parent的平衡因子一定为0,插入后被更新成正负1,
			// 此时以parent为根的树的高度增加,需要继续向上更新
			cur = cur->_parent;
			parent = parent->_parent;
		}
		else if (parent->_bf == 2 || parent->_bf == -2)
		{
			//如果pParent的平衡因子为正负2,
			// 则pParent的平衡因子违反平衡树的性质,需要对其进行旋转处理
			
			//旋转处理
			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
			{
				RotateRL(parent);
			}
			break;
		}
		else//插入之前AVL树就有问题
		{
			assert(false);
		}

	}
	return true;
}

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

	parent->_right = subRL;
	if (subRL)//subRL有可能是空树
		subRL->_parent = parent;

	subR->_left = parent;
	//还有subR的parent还未处理
	Node* ppnode = parent->_parent;
	parent->_parent = subR;

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

void RotateR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	parent->_left = subLR;
	if (subLR)
	{
		subLR->_parent = parent;
	}

	subL->_right = parent;

	Node* ppnode = parent->_parent;
	parent->_parent = subL;

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

void RotateLR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	int bf = subLR->_bf;
	RotateL(parent->_left);
	RotateR(parent);

	//旋转结束,重置bf
	if (bf == -1)
	{
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 1;
	}
	else if (bf == 1)
	{
		subLR->_bf = 0;
		subL->_bf = -1;
		parent->_bf = 0;
	}
	else if (bf == 0)
	{
		subLR->_bf = 0;
		subL->_bf = 0;
		parent->_bf = 0;
	}
	else
	{
		assert (false);
	}
}

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

	RotateR (subR);
	RotateL (parent);

	subRL->_bf = 0;
	if (bf == 1)//在 c 插入
	{
		parent->_bf = -1;
		subR->_bf = 0;
	}
	else if (bf == -1)//在 b 插入
	{
		parent->_bf = 0;
		subR->_bf = -1;
	}
	else if (bf == 0)//60 作为被插入的结点
	{
		parent->_bf = 0;
		subR->_bf = 0;
	}
	else
	{
		assert (false);
	}
}

5. AVL 树的验证

5.1 中序遍历打印和计算树的高度

void _InOrder(Node* root)
{
	if (root == nullptr)
		return;

	_InOrder(root->_left);
	cout << root->_kv.first << "[" << root->_bf << "]" << endl;
	_InOrder(root->_right);
}
void InOrder()
{
	_InOrder(_root);
}
int Height()
{
	return _Height(_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;
}

5.2 验证

  1. 验证其为二叉搜索树
    如果中序遍历可得到一个有序的序列,就说明为二叉搜索树
  2. 验证其为平衡树
    • 每个节点子树高度差的绝对值不超过1(注意节点中如果没有平衡因子)
    • 节点的平衡因子是否计算正确

在这里有两种方式:前序和(后序+引用参数),前者易于理解,后者高效且值得细品

//前序
bool _IsBalance(Node* root)
{
	if (root == nullptr)
	{
		return true;
	}

	int leftHeight = Height(root->_left);
	int	rightHeight = Height(root->_right);

	if (abs(rightHeight - leftHeight) >= 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);
}
bool IsBalance()
{
	return _IsBalance(_root);
}
  1. 基础情况:
    如果 root 为 nullptr(即树为空),那么它自然是平衡的,所以返回 true。
  2. 计算左右子树的高度:
    使用_Height 函数计算左子树和右子树的高度。
  3. 检查高度差:
    计算左右子树的高度差(abs (leftHeight - rightHeight))
    如果这个高度差大于或等于 2,说明树不是 AVL 树,所以打印出当前根节点的值(可能是为了调试目的),并返回 false。
  4. 检查平衡因子:
    AVL 树的每个节点通常都有一个平衡因子(BF),它是右子树高度减去左子树高度的结果。
    接下来,代码检查右子树高度减去左子树高度得到的差值是否等于当前节点的平衡因子(root->_bf)。
    如果不等,说明平衡因子不正确,可能是在之前的旋转或插入/删除操作中出现了错误,所以打印出当前根节点的值(为了调试)并返回 false。
  5. 递归检查子树:
    最后,代码递归地检查左子树和右子树是否都是 AVL 树。只有当两个子树都是 AVL 树时,当前树才是 AVL 树。

优点就是简单直观。
但是,这方法缺点很明显

  • 重复计算高度:每次检查节点的平衡状态时,都需要调用 Height 函数,导致子树的高度被多次重复计算,效率较低。
  • 效率低:由于高度的重复计算,导致时间复杂度较高,是 O ( n 2 ) O(n^2) O(n2)

以下方法没有用到上述的 Height() 函数

//后序
bool _IsBalance(Node* root, int& height)
	//用了后序,高度还是重复计数了,所以增加了一个引用参数
{
	if (root == nullptr)
	{
		height = 0;
		return true;
	}

	int leftHeight = 0,rightHeight = 0;
	
	//递归调用 `_IsBalance` 函数,分别检查左子树和右子树的平衡性,同时计算左右子树的高度。
	//如果左子树或右子树不平衡,直接返回 `false`。
	if (!_IsBalance(root->_left,leftHeight) || !_IsBalance(root->_right, rightHeight))
	{
		return false;
	}

	//检查当前节点的平衡性:
	//如果左右子树高度差的绝对值大于等于 2,输出当前节点的键值并返回 `false` 表示不平衡。
	if (abs(rightHeight - leftHeight) >= 2)
	{
		cout << root->_kv.first << "不平衡" << endl;
		return false;
	}
	//如果左右子树高度差不等于当前节点的平衡因子 `_bf`,输出当前节点的键值并返回 `false` 表示平衡因子异常。
	if (rightHeight - leftHeight != root->_bf)
	{
		cout << root->_kv.first << "平衡因子异常" << endl;
		return false;
	}
	
	height = leftHeight > rightHeight ? leftHeight + 1 : rightHeight + 1;
	//计算当前节点的高度并通过引用参数 `height` 返回。
	return true;
}
bool IsBalance()
{
	int height = 0;
	return _IsBalance(_root,height);
}

优点:

  • 避免重复计算:通过引用参数 height,避免了对子树高度的重复计算。例如,在遍历左子树和右子树时,会计算子树的高度并通过引用参数传递给父节点,父节点可以直接使用这些高度值,而不需要再次计算。
  • 后序遍历:该算法采用后序遍历的方式,确保在处理当前节点之前已经处理完左右子树,从而保证在返回当前节点的高度时,左右子树的高度已经计算完成。
  • 高效性:时间复杂度是 O(n),因为每个节点只被遍历一次,每个节点的高度也只被计算一次。
  • 平衡因子检查:不仅检查高度差是否在允许范围内,还检查每个节点的平衡因子 _bf 是否正确,确保 AVL 树的完整性。

5.3 数据测试

void TestAVLTree1()
{
	int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	AVLTree<int, int> t;
	for (auto e : a)
	{
		if (e == 14)
		{
			int x = 0;
		}

		t.Insert(make_pair(e, e));//它会自动推导模板参数的类型
	}
	t.InOrder();
	cout << t.IsBalance() << endl;
}

image.png|360
返回 1 即为确实是 AVL 树

关于调试
1、先看是插入谁导致出现的问题
2、打条件断点,画出插入前的树(打印、日志)
3、单步跟踪,对比图一一分析细节原因

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

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

相关文章

正则采集器之五——商品匹配规则

需求设计 实现分析 系统通过访问URL得到html代码&#xff0c;通过正则表达式匹配html&#xff0c;通过反向引用来得到商品的标题、图片、价格、原价、id&#xff0c;这部分逻辑在java中实现。 匹配商品的正则做成可视化编辑&#xff0c;因为不同网站的结构不同&#xff0c;同…

24小时在线的仪控专家

近年来&#xff0c;随着流程行业自动化水平的不断提高&#xff0c;仪表、阀门等设备在生产装置中的数量也越来越多&#xff0c;扮演着“眼睛”、“双手”和“神经”等角色&#xff0c;与生产过程的安全平稳息息相关&#xff0c;对企业追求效益最大化起着举足轻重的作用。 但仪控…

视频汇聚/安防监控/视频云存储EasyCVR平台实际通道数和授权数不一致的原因排查与解决

多协议接入/GB28181安防综合管理系统EasyCVR视频汇聚平台能在复杂的网络环境中&#xff0c;将前端设备统一集中接入与汇聚管理。智慧安防/视频存储/视频监控/视频汇聚EasyCVR平台可以提供实时远程视频监控、视频录像、录像回放与存储、告警、语音对讲、云台控制、平台级联、磁盘…

NOILinux2.0安装

NOI官方已发布NOILinux2.0&#xff0c;可是如何安装使用呢&#xff1f;我来教你。 首先下载VMWare和NOILinux2.0的ios&#xff0c;当然你用什么虚拟机软件都可以。这里我用的是VMware。 NOIlinux2.0的下载链接&#xff1a; NOI Linux 2.0发布&#xff0c;将于9月1日起正式启用…

2024电赛H题参考方案——自动行使小车

目录 一、题目要求 二、参考资源获取 三、参考方案 1、环境搭建及工程移植 2、移植MPU6050模块 3、移植TB6612电机驱动模块 其他模块根据需要移植 总结 一、题目要求 小编自认为&#xff1a;此次H题属于控制类题目&#xff0c;相较于往年较为简单&#xff0c;功能也算单一&…

hadoop学习(一)

一.hadoop概述 1.1hadoop优势 1&#xff09;高可靠性&#xff1a;Hadoop底层维护多个数据副本&#xff0c;即使Hadoop某个计算元素或存储出现故障&#xff0c;也不会导致数据的丢失。 2&#xff09;高扩展性&#xff1a;在集群间分配任务数据&#xff0c;可方便扩展数以千计…

开放式耳机和骨传导耳机哪个好?教你选择最好的开放式耳机!

​蓝牙耳机几乎成为和手机相同的数码设备&#xff0c;无论是在工作还是通勤过程&#xff0c;无论是娱乐还是线上办公&#xff0c;随身携带的蓝牙耳机都能提供更舒适、更便捷的听觉和通话体验。随着蓝牙耳机种类层出不穷&#xff0c;新型开放式耳机的加入&#xff0c;让更多消费…

DM数据库配置登录基于操作系统的身份验证

达梦数据库登录基于操作系统的身份验证 DM提供数据库身份验证模式和外部身份验证模式来保护对数据库访问的安全。数据库身份验证模式需要利用数据库口令&#xff0c;即在创建或修改用户时指定用户口令&#xff0c;用户在登录时输入对应口令进行身份验证&#xff1b;外部身份验…

React 和 Vue _使用区别

目录 一、框架介绍 1.Vue 2.React 二、框架结构 1.创建应用 2.框架结构 三、使用区别 1.单页面组成 2.样式 3.显示响应式数据 4.响应式html标签属性 5.控制元素显隐 6.条件渲染 7.渲染列表 react和vue是目前前端比较流行的两大框架&#xff0c;前端程序员应该将两…

鸿蒙HarmonyOS开发:如何灵活运用服务卡片提升用户体验

文章目录 一、ArkTS卡片相关模块二、卡片事件能力说明三、卡片事件的主要使用场景3.1、使用router事件跳转到指定UIAbility3.1.1、卡片内按钮跳转到应用的不同页面3.1.2、服务卡片的点击跳转事件 3.2、通过message事件刷新卡片内容3.2.1、在卡片页面调用postCardAction接口触发…

【Redis】Centos7 安装 redis(详细教程)

查看当前 Redis 版本&#xff1a; 当前的 redis 版本太老了&#xff0c;选择安装 Redis5。 一、使用 yum 安装 1、首先安装 scl 源 yum install centos-release-scl-rh 由于我之前已经安装过了&#xff0c;所以加载速度比较快&#xff0c;且显示已经安装成功&#xff0c;是最…

go-kratos 学习笔记(8) redis的使用

redis的在项目中的使用是很常见的&#xff0c;前面有了mysql的使用redis的也差不多&#xff1b;也是属于在data层的操作&#xff0c;所以需要新建一个 NewRedisCmd方法 在internal/data/data.go中新增NewRedisCmd 方法&#xff0c;注入到ProviderSet package dataimport (&quo…

【Java】类与对象、封装(008)

目录 类与对象 ♦️什么类与对象❓ &#x1f38f;类的定义 &#x1f383;定义一个类 &#x1f383;成员变量 &#x1f383;成员方法 &#x1f38f;对象的创建使用和引用传递 &#x1f383;对象的创建 &#x1f383;对象的引用 封装 ♦️什么是封装❓ ♦️实现封装 …

太阳伴星2600万年回转周期,或许正是它,导致地球生物周期性灭绝?!

我们知道地球已经有46亿年的寿命了&#xff0c;这相比人类生存的时间是极其漫长的。在地球历史中&#xff0c;恐龙在这里生活了1.6亿年&#xff0c;这是地球上相对独特的存在。当然&#xff0c;在恐龙的一生中&#xff0c;它们绝对是地球的统治者。当时&#xff0c;现在统治地球…

stm32入门-----DMA直接存储器存取(上——理论篇)

目录 前言 DMA 1.简介 2.存储器映像 3.DMA结构 4.数据宽度与对齐 5.DMA工作示例 前言 本期我们就开始学习DMA直接存储器存取&#xff0c;DMA是一个数据装运的小助手&#xff0c;执行数据的搬运处理&#xff0c;减少了CPU的负担&#xff0c;在stm32中担当重要的工作。在前…

《Milvus Cloud向量数据库指南》——不同开源向量数据库的适用数据规模及其技术特点深度剖析

在探讨向量数据库领域时,我们不得不提及多个备受瞩目的开源项目,它们各自以其独特的技术优势和适用场景赢得了广泛的关注。本文将深入剖析Milvus Cloud、Chroma、Weaviate、以及Qdrant这几个开源向量数据库在不同数据规模下的应用表现,以及它们各自的技术特点和优势。 引言…

SS9283403 开发环境搭建(二)

1.序 在前一篇“SS928&3403K开发环境搭建&#xff08;一&#xff09;”中已经借助Ebaina搭建好的ubuntu对开发板做了测试&#xff0c;这篇记录从零开始搭建SS928&3403K的开发环境&#xff1b; 2.开发前准备 下载VMware Workstation 16 Pro 16.1.0版本 下载ubuntu18.04…

封装导出功能(export)

业务描述: 通过一个button按钮, 实现导出功能, 导出后文件保存到电脑上 目录 一. file-saver 介绍 二. 项目中应用 1. 安装 file-saver库 2.创建 util / exportExcel.js 3. 页面内引入, 使用 4. 页面反馈 展示 一. file-saver 介绍 file-saver是一个用于在前端导出文件…

基于VueCli自定义创建Vue项目架子

基于VueCli自定义创建Vue项目架子 一、VueCli 自定义创建项目1.1安装脚手架 (已安装)1.2.创建项目1.2.1选项1.2.2手动选择功能&#xff08;按空格可选中&#xff09;1.2.3选择vue的版本1.2.4是否使用history模式1.2.5选择css预处理1.2.6选择eslint的风格 &#xff08;eslint 代…