【C++】AVL树的模拟实现

在这里插入图片描述
个人主页<—请点击
C++专栏<—请点击

一、AVL树的概念

1.1 为什么需要 AVL 树?

我们都知道二叉搜索树BST。在理想情况下,BST的搜索、插入、删除操作的时间复杂度是O(log n)。但是,如果插入的数据是有序的(例如 1, 2, 3, 4, 5),BST就会退化成一条链表,时间复杂度恶化到O(n)

AVL 就是为了解决这个问题而诞生的。它是一种 自平衡的二叉搜索树。它的核心思想是:在插入和删除节点时,通过一系列的 旋转 操作,始终保持树的左右子树高度大致相等,从而确保树的高度始终保持在 O(log n) 级别,进而保证所有操作的时间复杂度都是稳定的 O(log n)

1.2 AVL 树的核心:平衡因子

AVL的核心机制是 平衡因子

  • 平衡因子:对于树中的任意一个节点,它的平衡因子定义为 其左子树的高度减去其右子树的高度。 平衡因子 = 右子树高度 - 左子树高度

  • 平衡条件AVL要求每个节点的平衡因子只能是 -1, 0, 或 1。如果任何一个节点的平衡因子的绝对值超过了 1,那么这个树就是 不平衡 的,需要通过旋转来恢复平衡。

1.3 AVL 树的节点结构

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

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

如上图,我们采用了K-V这一通用结构,使用 std::pair 存储键值对父指针的作用:便于回溯更新平衡因子,简化旋转操作中的链接调整。

二、AVL树的实现

2.1 AVL树的 insert 操作

insert 操作也遵循二叉搜索树的规则,但和以往不同的是当插入一个结点时,我们需要关注平衡因子的变化,新插入的结点会影响祖先结点的平衡因子。所以我们需要维护更新从新增结点 -> 根节点的平衡因子,更新平衡因子过程中可能会出现不平衡,这时要对不平衡子树进行旋转,旋转本质在调平衡的同时,也降低了子树的高度,不会再影响上一层,所以插入结束

2.1.2 平衡因子的更新

更新原则

插入节点会影响parent结点的平衡因子变化,根据公式 平衡因子 = 右子树高度 - 左子树高度,新增节点是parent结点的右子树时,parent的平衡因子++,否则--parent所在子树的高度是否变化决定了是否向上更新。

更新停止条件
  • 当更新后的parent结点的平衡因子是0,说明是由1-1变成的0,那么这时候子树由一边高一边低变成了一样高,高度没有发生变化,不会影响parent的祖先,插入结束。
  • 当更新后的parent结点的平衡因子是1或者-1,说明更新之前是0。那么这时候子树由一样高变成了一边高一边低,高度发生变化,会影响parent的祖先,需要继续向上更新。
  • 当更新后的parent结点的平衡因子是2或者-2,说明是由1 -> 2或者-1 -> -2,这时候子树的平衡被破坏了需要进行旋转操作,旋转的目标有两个:1、把parent子树旋转平衡。2、降低parent子树的高度,恢复到插入结点以前的高度。所以旋转后也不需要继续往上更新,插入结束。

了解到以上这些我们来实现一下 insert 操作。

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 (kv.first > cur->_kv.first)
		{
			cur = cur->_right;
		}
		else if(kv.first < cur->_kv.first)
		{
			cur = cur->_left;
		}
		else
		{
			return false;
		}
	}

	cur = new Node(kv);
	cur->_bf = 0;
	if (cur->_kv.first > parent->_kv.first) parent->_right = cur;
	else parent->_left = cur;
	cur->_parent = parent;

	//更新维护平衡因子
	while (parent)
	{
		if (parent->_right == 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)
		{
			//旋转操作
		}
		else
		{
			//不是上述情况,说明这棵树之前就不是 AVL 树
			assert(false);
		}
	}
	return true;
}

如上就是除去旋转之外,插入函数的核心代码。代码中的插入部分和二叉搜索树一摸一样,就是最后需要维护平衡因子。

2.2 AVL树的 旋转 操作

2.2.1 旋转的原则

旋转的前提是要遵守搜索树的规则,其次要让不平衡的树变平衡,降低树的高度,旋转分为左单旋、右单旋、左右双旋、右左双旋

2.2.2 右单旋

在这里插入图片描述
旋转核心步骤,因为5 < b子树的值 < 10,将b变成10的左子树,10变成5的右子树,5变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的高度恢复到了插入之前的h+2,高度没有变化,插入结束。

实现细节:当结点的链接更改时,不要忘记更改结点的_parent指针,同时右单旋之后,parentsubL结点的平衡因子都是0

//右单旋
void RotateR(Node* parent)
{
	//parent 是平衡因子不符合规则的结点
	Node* subL = parent->_left;//相当于插入函数部分的 cur
	Node* subLR = subL->_right;

	//进行旋转操作
	parent->_left = subLR;
	if (subLR)//维护 subLR 的父指针
	{
		subLR->_parent = parent;
	}
	Node* pparent = parent->_parent; //subL 之后要更改的父指针指向

	subL->_right = parent;
	parent->_parent = subL;

	//判断之前 parent 是什么角色, 便于更改 subL 的父指针
	if (parent == _root)
	{
		_root = subL;
		_root->_parent = nullptr;
	}
	else
	{
		if (pparent->_left == parent)
		{
			pparent->_left = subL;
		}
		else pparent->_right = subL;

		subL->_parent = pparent;
	}
	
	//维护平衡因子
	parent->_bf = subL->_bf = 0;
}

右单旋在AVL树插入部分旋转处理的应用

else if (parent->_bf == 2 || parent->_bf == -2)
{
	//旋转操作
	if (parent->_bf == -2 && cur->_bf == -1)
	{
		//右单旋
		RotateR(parent);
	}
	break;
}

2.2.3 左单旋

在这里插入图片描述
旋转核心步骤,因为10 < b子树的值 < 15,将b变成10的右子树,10变成15的左子树,15变成这棵树新的根,符合搜索树的规则,控制了平衡,同时这棵的高度恢复到了插入之前的h+2,子树的高度没有发生变化,插入结束。

代码实现的细节部分和右单旋一样。

//左单旋
void RotateL(Node* parent)
{
	//parent 是平衡因子不符合规则的结点
	Node* subR = parent->_right; //相当于插入函数部分的 cur
	Node* subRL = subR->_left;

	//进行旋转操作
	parent->_right = subRL;
	if (subRL)//维护 subRL 的父指针
	{
		subRL->_parent = parent;
	}
	
	Node* pparent = parent->_parent; //subR 之后要更改的父指针指向

	subR->_left = parent;
	parent->_parent = subR;
	
	//判断之前 parent 是什么角色, 便于更改 subR 的父指针
	if (parent == _root)
	{
		_root = subR;
		subR->_parent = nullptr;
	}
	else
	{
		if (pparent->_left == parent)
		{
			pparent->_left = subR;
		}
		else pparent->_right = subR;

		subR->_parent = pparent;
	}

	//维护平衡因子
	parent->_bf = subR->_bf = 0;
}

左单旋在AVL树插入部分旋转处理的应用

else if (parent->_bf == 2 || parent->_bf == -2)
{
	//旋转操作
	if (parent->_bf == -2 && cur->_bf == -1)
	{
		//右单旋
		RotateR(parent);
	}
	else if (parent->_bf == 2 && cur->_bf == 1)
	{
		//左单旋
		RotateL(parent);
	}
	break;
}

2.2.4 左右双旋

情况一::h >= 1时,新增结点插入在e子树,e子树高度从h-1变为h,并不断更新8->5->10平衡因子,引发旋转,其中8的平衡因子为-1先以5为旋转点进行一个左单旋,再以10为旋转点进行一个右单旋,这棵树就平衡了,旋转后85平衡因子为010平衡因子为1
在这里插入图片描述
情况二h >= 1时,新增结点插入在f子树,f子树高度从h-1变为h并不断更新8->5->10平衡因子,引发旋转,其中8的平衡因子为1先以5为旋转点进行一个左单旋,再以10为旋转点进行一个右单旋,这棵树就平衡了,旋转后810平衡因子为05平衡因子为-1
在这里插入图片描述
情况三h == 0时,a/b/c都是空树,b自己就是一个新增结点,不断更新5->10平衡因子,引发旋转,其中8的平衡因子为0先以5为旋转点进行一个左单旋,再以10为旋转点进行一个右单旋,这棵树就平衡了,旋转后8105平衡因子均为0
在这里插入图片描述

搞清楚左右双旋的三种情况之后就可以实现代码了。

// 左右双旋
void RotateLR(Node* parent)
{
	Node* subL = parent->_left;
	Node* subLR = subL->_right;

	int bf = subLR->_bf; // 维护平衡因子的关键点

	RotateL(subL);
	RotateR(parent);

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

左右双旋在AVL树插入部分旋转处理的应用

else if (parent->_bf == 2 || parent->_bf == -2)
{
	// 旋转操作
	if (parent->_bf == -2 && cur->_bf == -1)
	{
		// 右单旋
		RotateR(parent);
	}
	else if (parent->_bf == 2 && cur->_bf == 1)
	{
		// 左单旋
		RotateL(parent);
	}
	else if (parent->_bf == -2 && cur->_bf == 1)
	{
		// 左右双旋
		RotateLR(parent);
	}
	break;
}

2.2.5 右左双旋

情况一h >= 1时,新增结点插入在e子树,e子树高度从h-1变为h,并不断更新12->15->10平衡因子,引发旋转,其中12的平衡因子为-1先以15为旋转点进行一个右单旋,再以10为旋转点进行一个左单旋,这棵树就平衡了,旋转后1012平衡因子为015平衡因子为1
在这里插入图片描述
情况二h >= 1时,新增结点插入在f子树,f子树高度从h-1变为h,并不断更新12->15->10平衡因子,引发旋转,其中12的平衡因子为1先以15为旋转点进行一个右单旋,再以10为旋转点进行一个左单旋,这棵树就平衡了,旋转后1512平衡因子为010平衡因子为-1
在这里插入图片描述
情况三h == 0时,a/b/c都是空树,b自己就是一个新增结点,不断更新15->10平衡因子,引发旋转,其中12的平衡因子为0先以15为旋转点进行一个右单旋,再以10为旋转点进行一个左单旋,这棵树就平衡了,旋转后101215平衡因子均为0
在这里插入图片描述

和左右双旋一样,搞清楚右左双旋的三种情况,就可以实现代码了。

// 右左双旋
void RotateRL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	int bf = subRL->_bf; // 维护平衡因子的关键点

	RotateR(subR);
	RotateL(parent);

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

右左双旋在AVL树插入部分旋转处理的应用

else if (parent->_bf == 2 || parent->_bf == -2)
{
	// 旋转操作
	if (parent->_bf == -2 && cur->_bf == -1)
	{
		// 右单旋
		RotateR(parent);
	}
	else if (parent->_bf == 2 && cur->_bf == 1)
	{
		// 左单旋
		RotateL(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;
}

2.3 中序遍历

public:
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}
private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;
	
		_InOrder(root->_left);
		cout << root->_kv.first << " ";
		_InOrder(root->_right);
	}

测试代码:

void TestAVLTree1()
{
	AVLTree<int, int> t;
	// 特殊的带有双旋场景的测试用例
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	for (auto e : a)
	{
		if (e == 14)
		{
			int x = 0;
		}

		t.insert({ e, e });
	}
	t.InOrder();
}

测试结果:
在这里插入图片描述

2.4 其它接口的实现

2.4.1 查找函数

Node* Find(const K& key)
{
	Node* cur = _root;

	while (cur)
	{
		if (key > cur->_kv.first)
			cur = cur->_right;
		else if (key < cur->_kv.first)
			cur = cur->_left;
		else
			return cur;
	}
	return nullptr;
}

2.4.2 Size 和 Height 函数

public:
	int Size()
	{
		return _Size(_root);
	}
	
	int Height()
	{
		return _Height(_root);
	}
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;
		}
	
		int leftH = _Height(root->_left);
		int rightH = _Height(root->_right);
	
		return leftH > rightH ? leftH + 1 : rightH + 1;
	}

2.4.3 AVL树的判断函数

public:
	bool IsBalanceTree()
	{
		return _IsBalanceTree(_root);
	}
private:
	bool _IsBalanceTree(Node* root)
	{
		if (root == nullptr)
		{
			return true;
		}
	
		int leftH = _Height(root->_left);
		int rightH = _Height(root->_right);
	
		int bf = rightH - leftH;
	
		if (abs(bf) > 2 || bf != root->_bf)
		{
			cout << root->_kv.first << ":" << root->_kv.second << "平衡因子异常" << endl;
			return false;
		}
	
		return _IsBalanceTree(root->_left) 
			 && _IsBalanceTree(root->_right);
	}

测试代码:

void TestAVLTree2()
{
	AVLTree<int, int> t;
	// 特殊的带有双旋场景的测试用例
	int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	for (auto e : a)
	{
		if (e == 14)
		{
			int x = 0;
		}

		t.insert({ e, e });
		cout << "insert:" << e << "->" << t.IsBalanceTree() << endl;
	}
	t.InOrder();
	cout << t.IsBalanceTree() << endl;
	cout << t.Size() << " " << t.Height() << endl;
	cout << t.Find(2) << " " << t.Find(17) << endl;
}

测试结果:
在这里插入图片描述

总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~

【HW3000的收发程序】是一个与硬件设备HW3000相关的软件实现,主要用于处理数据的发送和接收。在这个项目中,我们关注的是一个基于STM8S105微控制器的示例代码,它使用了IAR集成开发环境。这个压缩包包含了名为"A版本"的代码示例,这表明可能还有其他版本存在,例如"B版本"或"C版本",每个版本可能对应不同的功能改进或优化。 STM8S105是意法半导体(STMicroelectronics)推出的一款8位微控制器,属于STM8系列。该芯片具有高性能、低功耗的特点,常用于各种嵌入式系统,如工业控制、消费电子和汽车电子等领域。IAR是一个流行的嵌入式系统开发工具,提供了一整套集成开发环境(IDE),包括编译器、调试器和其他辅助工具,使得开发者能够方便地编写、编译和调试STM8S105上的代码。 在HW3000的收发程序中,我们预计会看到以下关键知识点: 1. **硬件接口**:为了与HW3000通信,代码可能定义了特定的GPIO引脚来控制数据传输和握手信号。这些引脚可能是通过STM8S105的端口和引脚配置来实现的。 2. **通信协议**:HW3000可能使用某种串行通信协议,如SPI、I2C或UART。代码会包含相应的初始化函数和数据传输函数,以遵循该协议。 3. **中断服务例程**:为了实时响应数据收发事件,程序可能使用中断服务例程(ISR)。当硬件检测到新的数据或发送完成时,中断将被触发,并执行相应处理。 4. **数据缓冲区管理**:在发送和接收数据时,可能需要使用缓冲区来存储待发送的数据或接收的数据包。代码会包含关于如何填充、读取和清空缓冲区的逻辑。 5. **错误检测和处理**:良好的通信程序会包含错误检测机制,如CRC校验或奇偶校验,以及错误处理代码,以确保数据的完整性和正确性。 6. **同步机制**:为了确保发送和接收的顺序,可能使用了互斥锁、信号量或其他同步原语来避免数据冲突。 7. **代码注释**:描述中提到代码注释详细且通俗易懂,这意味着开发者可以很容易地理解每一部分的功能,这对于理解和维护代码至关重要。 由于这是一个"A版本"的代码,我们可以假设它可能是项目的基础版本,可能随着项目的进展,后续的版本会包含更高级的功能、优化的性能或者修复的已知问题。对这个压缩包的深入研究将有助于我们了解HW3000硬件的工作原理,以及如何有效地与之进行软件交互。
【源码免费下载链接】:https://renmaiwang.cn/s/fmfod 在 Cisco 交换机上管理 VLAN 是网络管理员日常工作中不可或缺的一部分。VLAN(虚拟局域网)的创建和删除有助于组织网络流量、提高安全性并优化网络性能。然而,在某些情况下,需要删除不再使用的 VLAN,以避免资源浪费和潜在的配置冲突。在 Cisco 交换机中,简单地执行`no vlan`命令并不能彻底删除一个 VLAN,因为这只会从 VLAN 数据库中移除 VLAN 的定义,而不会处理相关的接口配置。以下是一步一步的详细步骤,确保完全删除一个 VLAN:1. **连接到交换机**:可以通过控制台口或使用 Telnet 远程连接至交换机。一旦建立连接,将进入用户模式(User EXEC Mode),提示符显示为`<Switch>`。2. **切换到特权模式**:在用户模式下输入`en`命令,切换到特权模式(Privileged EXEC Mode),提示符变为`Switch#`。3. **进入全局配置模式**:在特权模式下,输入`conf t`命令,进入全局配置模式,此时提示符变为`Switch(config)#`。4. **移除接口上的 VLAN 配置**:如果 VLAN 已分配给接口,则必须先从接口上移除 VLAN 配置。例如,在删除 VLAN 20 时,可以输入`int f0/1`进入接口配置模式,然后执行命令`no switchport access vlan 20`。如果 VLAN 20 被多个接口使用,则需要对每个接口重复此操作。5. **移除 VLAN 接口**:接下来,删除 VLAN 接口,输入`no interface vlan 20`。这将解除 VLAN 20 对其所有相关接口的配置。6. **彻底删除 VLAN**:最后,执行命令`no vlan 20`,从 VLA
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

矢 望

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值