【C++】手撕红黑树

> 作者简介:დ旧言~,目前大二,现在学习Java,c,c++,Python等
> 座右铭:松树千年终是朽,槿花一日自为荣。

> 目标:能直接手撕红黑树。

> 毒鸡汤:行到水穷处,坐看云起时。。

> 望小伙伴们点赞👍收藏✨加关注哟💕💕 

🌟前言  

相信大家肯定听过在C++大名鼎鼎的两颗树,这两颗树分别是AVL树和红黑树,学过的小伙伴听到都是瑟瑟发抖,像一些大厂中可能会考手撕AVL树或红黑树。学习这两棵树确实难度很大,正所谓难度越大动力就越大,那本篇我们学习这两棵树的一颗树--红黑树。

⭐主体

学习多态咱们按照下面的图解:

🌙红黑树的概念

红黑树,是一种二叉搜索树,但在每个结点上增加一个存储位表示结点的颜色,可以是Red或 Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制,红黑树确保没有一条路 径会比其他路径长出俩倍,因而是接近平衡的。

🌙红黑树的性质

红黑树有以下五点性质:

  1. 每个结点不是红色就是黑色。
  2. 根结点是黑色的。
  3. 如果一个结点是红色的,则它的两个孩子结点是黑色的。
  4. 对于每个结点,从该结点到其所有后代叶子结点的简单路径上,均包含相同数目的黑色结点。
  5. 每个叶子结点都是黑色的(此处的叶子结点指定是空结点)。

问题探讨:

红黑树如何确保从根到叶子的最长可能路径不会超过最短可能路径的两倍?

问题分析:

红黑树第三条性质说明红黑树不能存在连续(父子相连)的红结点,可以存在连续的黑结点,又由于第四条性质每个路径上的黑结点个数都相同 ,所以对于最短路径来说一定是都是黑色结点,对于最长路径来说一定是黑色红色相间的路径,所以最长路径不超过最短路径长度的二倍

图解分析:

🌙红黑树的结点

三叉链结构,对比AVL数节点的定义,把平衡因子替换成节点颜色,采用枚举的方式:

编写代码:

// 定义红黑树结点
template<class K,class V>
struct RBTreeNode
{
	RBTreeNode<K, V>* _left;   // 左
	RBTreeNode<K, V>* _right;  // 右
	RBTreeNode<K, V>* _parent; // 父亲

	Color _col; 
	pair<K, V> _kv; //存储的键值对

	RBTreeNode(const pair<K,V>& kv) // 初始化列表
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_col(RED)
	{}
};

代码分析:

为什么构造结点时,默认将结点的颜色设置为红色?

  1. 如果默认颜色为黑,那么在插入中插入一个黑结点一定会让该路径上的黑结点数量加1,从而与其他路径上黑结点数量造成不一致,而一定会影响该棵红黑树
  2. 如果默认颜色为红,那么在插入中插入一个红结点,可能新插入结点的父结点为黑色结点则没有影响,也可能新插入结点的父结点为红结点,由于不能存在连续的(父子相连的)红色结点,而对该棵树造成影响
  3. 所以默认为红色比较黑色来说是好的

🌙红黑树的插入

红黑树插入结点的逻辑分为三步:

  1. 按二叉搜索树的插入方法,找到待插入位置。
  2. 将待插入结点插入到树中。
  3. 若插入结点的父结点是红色的,则需要对红黑树进行调整。

红黑树调整时具体应该如何调整,主要是看插入结点的叔叔(插入结点的父结点的兄弟结点),根据插入结点叔叔的不同,可将红黑树的调整分为三种情况。


情况一:插入结点的叔叔存在,且叔叔的颜色是红色。(cur为红,p为红,g为黑,u存在且为红)

分析:

  • 此时为了避免出现连续的红色结点,我们可以将父结点变黑,但为了保持每条路径黑色结点的数目不变,因此我们还需要将祖父结点变红,再将叔叔变黑。这样一来既保持了每条路径黑色结点的数目不变,也解决了连续红色结点的问题。
  • 但调整还没有结束,因为此时祖父结点变成了红色,如果祖父结点是根结点,那我们直接再将祖父结点变成黑色即可,此时相当于每条路径黑色结点的数目都增加了一个。
  • 但如果祖父结点不是根结点的话,我们就需要将祖父结点当作新插入的结点,再判断其父结点是否为红色,若其父结点也是红色,那么又需要根据其叔叔的不同,进而进行不同的调整操作。

图解:

情况二:cur为红,p为红,g为黑,u不存在/u为黑,gpc在同一侧

探讨:

如果u结点不存在,则cur一定是新增结点,因为如果cur不是新增结点:则cur和p一定有一个节点时黑色,就不满足每条路径都有相同的黑色结点的性质。

如果u结点存在,则其一定是黑色的,那么c节点原来的颜色一定是黑色,在其子树调整过程中变为了红色

分析:

  • 如果p为g的左孩子,cur为p的左孩子,则进行右单旋转
  • 如果p为g的右孩子,cur为p的右孩子,则进行左单旋转

①u不存在,cur为新增节点,进行右单旋

图解:

②u结点存在且为黑:

情况二: cur为红,p为红,g为黑,u不存在/u为黑,gpc不在同一侧

分析:

  • p为g的左孩子,cur为p的右孩子,对p做左单旋转,
  • p为g的右孩子,cur为p的左孩子,对p做右单旋转,

说明:

  • 这时候我们就需要进行双旋了

图解:

编写代码:

	// 插入结点
	bool Insert(const pair<K, V>& kv)
	{
		if (_root == nullptr) // 若红黑树为空叔,则插入结点直接作为根结点
		{
			_root = new Node(kv);
			_root->_col = BLACK; // 根结点必须是黑色
			return true;         // 插入成功
		}

		// 1.采用二叉搜索树的方法找插入位置
		Node* parent = nullptr;
		Node* cur = _root;
		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;
			}
		}

		// 2.将待插入结点插入到树中
		cur = new Node(kv); // 根据所给值构造一个结点(必须是红色)
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		cur->_parent = parent;

		// 3.若插入结点的父结点是红色的,则需要对红黑树进行调整
		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent; // parent是红色其父结点一定存在
			
			// parent为父的左孩子
			if (parent == grandfather->_left) 
			{
				Node* uncle = grandfather->_right;
				if (uncle && uncle->_col == RED)//情况一:叔叔存在且为红色
				{
					// 颜色调整
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					// 继续往上处理
					cur = grandfather;
					parent = cur->_parent;
				}
				else // 情况二:uncle不存在 + uncle存在且为黑
				{
					if (cur == parent->_left)// cur == parent->_left
					{
						RotateR(grandfather);  // 右单旋
						
						// 颜色调整
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else // cur == parent->_right
					{
						RotateL(parent); // 左单旋
						RotateR(grandfather); // 右单旋
						
						// 颜色调整
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break; // 子树旋转后,该子树的根变为黑色,无需往上处理
				}
			}
			else // parent是父的右孩子 
			{
				Node* uncle = grandfather->_left;
				
				// 情况一:叔叔存在且为红
				if (uncle && uncle->_col == RED)
				{
					// 颜色调整
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					// 继续往上处理
					cur = grandfather;
					parent = cur->_parent;
				}
				else // 情况二:叔叔不存在或者存在且为黑
				{
					
					if (cur == parent->_right)
					{
						// 右左双旋
						RotateL(grandfather);
						
						// 颜色调整
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else 
					{
						RotateR(parent);
						RotateL(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
		}
		// 根结点的颜色为黑色(可能被情况一变成了红色,需要变回黑色)
		_root->_col = BLACK;
		return true;
	}

	// 左单旋
	void RotateL(Node* parent)
	{
		++rotateSize;

		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;
			subR->_parent = nullptr;
		}
		else
		{
			if (ppnode->_left == parent)
			{
				ppnode->_left = subR;
			}
			else
			{
				ppnode->_right = subR;
			}
			subR->_parent = ppnode;
		}
	}

	// 右单旋
	void RotateR(Node* parent)
	{
		++rotateSize;

		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;
		}
	}

🌙红黑树的查找

红黑树的查找函数与二叉搜索树的查找方式一模一样,逻辑如下:

  • 若树为空树,则查找失败,返回nullptr。
  • 若key值小于当前结点的值,则应该在该结点的左子树当中进行查找。
  • 若key值大于当前结点的值,则应该在该结点的右子树当中进行查找。
  • 若key值等于当前结点的值,则查找成功,返回对应结点。
	// 查找元素
	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 NULL;
	}

🌙红黑树的验证

红黑树的检测分为两步:

  1. 检测其是否满足二叉搜索树(中序遍历是否为有序序列)
  2. 检测其是否满足红黑树的性质

步骤一代码:

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

		_InOrder(root->_left);
		cout << root->_kv.first << endl;
		_InOrder(root->_right);
	}
	void InOrder()
	{
		_InOrder(_root);
	}

步骤二代码:

	// 判断是否为红黑树
	bool Check(Node* cur, int blackNum, int refBlackNum)
	{
		if (cur == nullptr)
		{
			if (refBlackNum != blackNum)
			{
				cout << "黑色节点的数量不相等" << endl;
				return false;
			}

			//cout << blackNum << endl;
			return true;
		}

		if (cur->_col == RED && cur->_parent->_col == RED)
		{
			cout << cur->_kv.first << "存在连续的红色节点" << endl;
			return false;
		}

		if (cur->_col == BLACK)
			++blackNum;

		return Check(cur->_left, blackNum, refBlackNum)
			&& Check(cur->_right, blackNum, refBlackNum);
	}
	bool IsBalance()
	{
		if (_root && _root->_col == RED)
			return false;

		int refBlackNum = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
				refBlackNum++;

			cur = cur->_left;
		}

		return Check(_root, 0, refBlackNum);
	}

🌙红黑树与AVL树比较

红黑树与AVL树比较:

  1. 红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O( )

  2. 红黑树不追求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数

  3. 所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多

🌙全部代码

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

enum Color // 采用枚举定义颜色
{
	RED,   // 0 为红色
	BLACK, // 1 为黑色
};

// 定义红黑树结点
template<class K,class V>
struct RBTreeNode
{
	RBTreeNode<K, V>* _left;   // 左
	RBTreeNode<K, V>* _right;  // 右
	RBTreeNode<K, V>* _parent; // 父亲

	Color _col; 
	pair<K, V> _kv; //存储的键值对

	RBTreeNode(const pair<K,V>& kv) // 初始化列表
		:_kv(kv)
		,_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_col(RED)
	{}
};

// 主体
template<class K,class V>
class RBTree
{
	typedef RBTreeNode<K, V> Node;

public:

	// 插入结点
	bool Insert(const pair<K, V>& kv)
	{
		if (_root == nullptr) // 若红黑树为空叔,则插入结点直接作为根结点
		{
			_root = new Node(kv);
			_root->_col = BLACK; // 根结点必须是黑色
			return true;         // 插入成功
		}

		// 1.采用二叉搜索树的方法找插入位置
		Node* parent = nullptr;
		Node* cur = _root;
		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;
			}
		}

		// 2.将待插入结点插入到树中
		cur = new Node(kv); // 根据所给值构造一个结点(必须是红色)
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		cur->_parent = parent;

		// 3.若插入结点的父结点是红色的,则需要对红黑树进行调整
		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent; // parent是红色其父结点一定存在
			
			// parent为父的左孩子
			if (parent == grandfather->_left) 
			{
				Node* uncle = grandfather->_right;
				if (uncle && uncle->_col == RED)//情况一:叔叔存在且为红色
				{
					// 颜色调整
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					// 继续往上处理
					cur = grandfather;
					parent = cur->_parent;
				}
				else // 情况二:uncle不存在 + uncle存在且为黑
				{
					if (cur == parent->_left)// cur == parent->_left
					{
						RotateR(grandfather);  // 右单旋
						
						// 颜色调整
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else // cur == parent->_right
					{
						RotateL(parent); // 左单旋
						RotateR(grandfather); // 右单旋
						
						// 颜色调整
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break; // 子树旋转后,该子树的根变为黑色,无需往上处理
				}
			}
			else // parent是父的右孩子 
			{
				Node* uncle = grandfather->_left;
				
				// 情况一:叔叔存在且为红
				if (uncle && uncle->_col == RED)
				{
					// 颜色调整
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					// 继续往上处理
					cur = grandfather;
					parent = cur->_parent;
				}
				else // 情况二:叔叔不存在或者存在且为黑
				{
					
					if (cur == parent->_right)
					{
						// 右左双旋
						RotateL(grandfather);
						
						// 颜色调整
						parent->_col = BLACK;
						grandfather->_col = RED;
					}
					else 
					{
						RotateR(parent);
						RotateL(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
		}
		// 根结点的颜色为黑色(可能被情况一变成了红色,需要变回黑色)
		_root->_col = BLACK;
		return true;
	}

	// 左单旋
	void RotateL(Node* parent)
	{
		++rotateSize;

		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;
			subR->_parent = nullptr;
		}
		else
		{
			if (ppnode->_left == parent)
			{
				ppnode->_left = subR;
			}
			else
			{
				ppnode->_right = subR;
			}
			subR->_parent = ppnode;
		}
	}

	// 右单旋
	void RotateR(Node* parent)
	{
		++rotateSize;

		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;
		}
	}

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

		_InOrder(root->_left);
		cout << root->_kv.first << endl;
		_InOrder(root->_right);
	}
	void InOrder()
	{
		_InOrder(_root);
	}

	// 判断是否为红黑树
	bool Check(Node* cur, int blackNum, int refBlackNum)
	{
		if (cur == nullptr)
		{
			if (refBlackNum != blackNum)
			{
				cout << "黑色节点的数量不相等" << endl;
				return false;
			}

			//cout << blackNum << endl;
			return true;
		}

		if (cur->_col == RED && cur->_parent->_col == RED)
		{
			cout << cur->_kv.first << "存在连续的红色节点" << endl;
			return false;
		}

		if (cur->_col == BLACK)
			++blackNum;

		return Check(cur->_left, blackNum, refBlackNum)
			&& Check(cur->_right, blackNum, refBlackNum);
	}
	bool IsBalance()
	{
		if (_root && _root->_col == RED)
			return false;

		int refBlackNum = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
				refBlackNum++;

			cur = cur->_left;
		}

		return Check(_root, 0, refBlackNum);
	}

	// 计算红黑树结点个数
	size_t Size()
	{
		return _Size(_root);
	}
	size_t _Size(Node* root)
	{
		if (root == NULL)
			return 0;

		return _Size(root->_left)
			+ _Size(root->_right) + 1;
	}

	// 查找元素
	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 NULL;
	}

	// 计算红黑树高度
	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;
	}
	int Height()
	{
		return _Height(_root);
	}


	int GetRotateSize()
	{
		return rotateSize;
	}

private:
	Node* _root = nullptr;
	int rotateSize = 0;
};

void TestRBTree1()
{
	//int a[] = { 8, 3, 1, 10, 6, 4, 7, 14, 13 };
	int a[] = { 16, 3, 7, 11, 9, 26, 18, 14, 15 };
	//int a[] = { 4, 2, 6, 1, 3, 5, 15, 7, 16, 14 };
	RBTree<int, int> t;
	for (auto e : a)
	{
		t.Insert(make_pair(e, e));
	}
	t.InOrder();
	cout << t.IsBalance() << endl;
}

void TestRBTree2()
{
	srand(time(0));
	const size_t N = 100000;
	RBTree<int, int> t;
	for (size_t i = 0; i < N; i++)
	{
		size_t x = rand();
		t.Insert(make_pair(x, x));
	}
	cout << t.IsBalance() << endl;

}

🌟结束语

       今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Armv8状态寄存器

Processor state AArch64没有与ARMv7当前程序状态寄存器直接对应的寄存器(CPSR)。在AArch64中&#xff0c;传统CPSR的组件以字段的形式提供可独立访问。这些统称为处理器状态(PSTATE)。 在AArch64中&#xff0c;通过执行ERET指令从异常中返回&#xff0c;这会导致要拷贝到PSTAT…

软件测试相关内容第四弹 -- 测试用例与测试分类

写在前&#xff1a;我们已经掌握了关于软件测试的相关内容&#xff0c;知道了基本的测试过程&#xff0c;在做了一段时间的基础测试&#xff0c;熟悉了相关的业务后&#xff0c;测试人员会进行测试用例的编写&#xff0c;在日常测试中&#xff0c;也需要补充测试用例到现有的案…

PyTorch深度学习实战(39)——小样本学习

PyTorch深度学习实战&#xff08;39&#xff09;——小样本学习 0. 前言1. 小样本学习简介2. 孪生网络2.1 模型分析2.2 数据集分析2.3 构建孪生网络 3. 原型网络3. 关系网络小结系列链接 0. 前言 小样本学习 (Few-shot Learning) 旨在解决在训练集中只有很少样本的情况下进行分…

常见的十大网络安全攻击类型

常见的十大网络安全攻击类型 网络攻击是一种针对我们日常使用的计算机或信息系统的行为&#xff0c;其目的是篡改、破坏我们的数据&#xff0c;甚至直接窃取&#xff0c;或者利用我们的网络进行不法行为。你可能已经注意到&#xff0c;随着我们生活中越来越多的业务进行数字化&…

python知识点总结(三)

python知识点总结三 1、有一个文件file.txt大小约为10G&#xff0c;但是内存只有4G&#xff0c;如果在只修改get_lines 函数而其他代码保持不变的情况下&#xff0c;应该如何实现? 需要考虑的问题都有那些?2、交换2个变量的值3、回调函数4、Python-遍历列表时删除元素的正确做…

3/14/24数据结构、线性表

目录 数据结构 数据结构三要素 逻辑结构 存储结构 数据运算 时间复杂度 空间复杂度 线性表 线性表定义 静态分配 动态分配 线性表插入 线性表删除 十天的时间学完了C语言督学课程&#xff0c;最后终于是可以投入到408的科目学习当中。关于数据结构和算法的学习很多部…

智慧城市物联网建设:提升城市管理效率与居民生活品质

目录 一、智慧城市物联网建设的意义 1、提升城市管理效率 2、改善居民生活品质 3、促进城市可持续发展 二、智慧城市物联网建设面临的挑战 1、技术标准与互操作性问题 2、数据安全与隐私保护问题 3、投资与回报平衡问题 三、智慧城市物联网建设的实施策略 1、制定统一…

【Qt】Qt中的常用属性

需要云服务器等云产品来学习Linux可以移步/-->腾讯云<--/官网&#xff0c;轻量型云服务器低至112元/年&#xff0c;新用户首次下单享超低折扣。 目录 一、QWidget属性一览 二、属性enabled(可用状态) 三、属性geometry(修改位置和尺寸) 1、QRect类型的结构 2、geome…

实用工具推荐----Mocreak Win Office 自动部署(激活+安装)

Mocreak 该工具包含功能 一键快速下载、安装、激活最新版 Microsoft Office 软件。用户可在安装 Word、PPT、Excel 的同时&#xff0c;根据软件提示&#xff0c;自助安装其它组件&#xff0c;包括&#xff1a; Outlook、OneNote、Access、Visio、Project、Publisher、Teams、…

Python图像处理:3.七种图像分割方法

一、常见图像分割方法 (1)传统算法 阈值分割&#xff08;Thresholding&#xff09;&#xff1a;这是最简单也是应用最广泛的一种分割方法&#xff0c;通过选定一个阈值将图像转换为二值图像&#xff0c;从而分割出目标区域。这种方法适用于图像的前景和背景对比明显的情况。 …

PWM驱动舵机

PWM驱动舵机 接线图 程序结构图&#xff1a; pwm.c部分代码 #include "stm32f10x.h" // Device headervoid PWM_Init(void){// 开启时钟&#xff0c;这里TIM2是通用寄存器RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);// GPIO初始化代…

基于JavaWeb+SSM+Vue“鼻护灵”微信小程序系统的设计和实现

基于JavaWebSSMVue“鼻护灵”微信小程序系统的设计和实现 滑到文末获取源码Lun文目录前言主要技术系统设计功能截图 滑到文末获取源码 Lun文目录 摘 要 3 Abstract 1 1 绪 论 1 1.1研究背景 1 工作的效率。 1 1.2 研究意义 1 1.3研究现状 1 1.4本文组织结构 2 2 技术介绍 3 2…

华为配置终端定位基本实验配置

配置终端定位基本示例 组网图形 图1 配置终端定位基本服务示例 组网需求数据准备配置思路配置注意事项操作步骤配置文件 组网需求 如图1所示&#xff0c;某公司网络中&#xff0c;中心AP直接与RU连接。 管理员希望通过RU收集Wi-Fi终端信息&#xff0c;并提供给定位服务器进行定…

面试知识汇总——Redis高可用(主从、哨兵、集群)

我们在项目中使用Redis&#xff0c;肯定不会是单点部署Redis服务的。因为&#xff0c;单点部署一旦宕机&#xff0c;就不可用了。为了实现高可用&#xff0c;通常的做法是&#xff0c;将数据库复制多个副本以部署在不同的服务器上&#xff0c;其中一台挂了也可以继续提供服务。…

asp.net 作业星软件系统

asp.net 作业星软件系统 用户功能:分教师和家长&#xff08;学生) 注册登录:登录部分是用户名密码&#xff0c;以及教师和家长&#xff08;学生&#xff09;的勾选; 注册包括用户名密码确认密码再次确认密码(与上方输入的密码比对&#xff09;身份班级设置找回账号的问题和答案…

【前端】-css的详解

&#x1f496;作者&#xff1a;小树苗渴望变成参天大树&#x1f388; &#x1f389;作者宣言&#xff1a;认真写好每一篇博客&#x1f4a4; &#x1f38a;作者gitee:gitee✨ &#x1f49e;作者专栏&#xff1a;C语言,数据结构初阶,Linux,C 动态规划算法&#x1f384; 如 果 你 …

【WEEK3】 【DAY3】JSON Interaction Handling Part Two【English Version】

2024.3.13 Wednesday Continuing from 【WEEK3】 【DAY2】JSON Interaction Handling Part One 【English Version】 Contents 6.4 Code Optimization6.4.1 Unified Solution for Garbled Text6.4.2 Unified Solution for Returning JSON Strings 6.5 Testing Collection Out…

铸铁钳工工作台是一种专门使用工具,具有哪些特点和功能

铸铁钳工作台是一种专门用于加工和修理铸铁制品的工作台。它通常由坚固的钢铁结构构成&#xff0c;表面通常涂有耐腐蚀的涂层&#xff0c;以提高其使用寿命和耐久性。 铸铁钳工作台通常具有以下主要特点和功能&#xff1a; 高强度和稳定性&#xff1a;由于铸铁是一种坚固耐用的…

ConcurrentMap的相关特点和使用

概述 ConcurrentMap是Java中的一个接口&#xff0c;主要扩展了Map接口&#xff0c;用于在多线程环境中提供线程安全的Map实现&#xff0c;是Java.util.concurrent包中的一部分&#xff0c;提供了一些原子操作&#xff0c;这些操作不需要使用synchronized关键字&#xff0c;从而…

SAP前台处理:销售业务集成<VA03/VL03N/VLPOD/VF03) 01/02

一、背景&#xff1a; 从销售订单创建VA01>发货过账VL01N >POD确认>VF01开票 这个流程涉及的凭证流及各个节点如何查询上游下游凭证&#xff1b; 二、凭证流&#xff1a; 从销售订单查看销售凭证流 VA03 双击交货单&#xff1a;带出交货单对应行项目及分批次项目…