红黑树的介绍与实现

前言

前面我们介绍了AVL树,AVL树是一棵非常自律的树,有着严格的高度可控制!但是正它的自律给他带来了另一个问题,即虽然他的查找效率很高,但是插入和删除由于旋转而导致效率没有那么高。我们上一期的结尾说过经常修改的话就不太适合AVL树了,而红黑树更加适合!OK,本期就来介绍一下赫赫有名的红黑树!

本期内容介绍

什么是红黑树

红黑树的实现

红黑树的效率分析以及应用

什么是红黑树?

红黑树是1972年由Rudolf Bayer发明的,当时被称为平衡二叉B树(symmetric binary B-trees)。后来,在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的 " 红黑树 "。

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

OK,这就是一棵红黑树:

最长节点的路径就是一黑一红的交替,最短的就是两黑:

红黑树的性质

1、每个结点要么是红色,要么是黑色

2、根节点是黑色

3、如果一个结点是红色,则它的两个孩子结点是黑色

4、对于任意一个节点,从该结点到其所有的后代叶子结点的简单路径上,均包含相同的黑色结点

5、每个叶子结点就是黑色的(此处的叶子结点指的是空节点)

上面的这5条性质就是限制红黑树平衡的规则!其中4最重要的下来是3,基本所有的操作都是围着4进行的!!!

红黑树的实现

OK,上面介绍了红黑树是一种二叉搜索树,只不过是在每个结点添加了一个存储颜色的颜色位,所以它的大框架还是和搜索树一样的,所以我们就先搭一个框架出来!

enum Col//颜色
{
	RED,
	BLACK
};

template<class K, class V>
struct RBTreeNode
{
	RBTreeNode<K, V>* _left;//左孩子
	RBTreeNode<K, V>* _right;//右孩子
	RBTreeNode<K, V>* _parent;//父结点
	pair<K, V> _kv;//数据域
	Col _col;//颜色

	RBTreeNode(const pair<K,V>& kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_kv(kv)
		,_col(RED)//新插入的节点默认是红色
	{}
};

template<class K, class V>
class RBTree
{
	typedef RBTreeNode<K, V> Node;
public:
	RBTree()
		:_root(nullptr)
		,_size(0)
	{}

private:
	Node* _root;//根节点
	size_t _size;//节点的数量
};

思考:新插入的节点应该是红色还是黑色?为什么?

新插入的节点一定是红色!

因为新插入的节点是红色可能违反性质3,但一定不违反性质4

如果新插入的是黑色一定违反性质4,也就是在部分子路径上增加了黑色节点。所以插入的新节点一定是红色,即使红色违反了性质3也是比较好控制的!

OK,这里依旧是采用的三叉链,原因是方便找父亲

红黑树的插入

上面介绍了,红黑树的本质是一种二叉搜索树,所以先不管它的颜色和高度如何调节,先把搜索树的那一套给整出来:

bool Insert(const pair<K, V>& kv)
{
	Node* cur = _root;//当前节点
	Node* parent = nullptr;//插入位置的父亲

	//第一次插入
	if (_root == nullptr)
	{
		_root = new Node(kv);
		_root->_col = BLACK;//根节点是黑色
		return true;
	}

	//寻找插入的位置
	while (cur)
	{
		if (cur->_kv.first < kv.first)//插入节点的key比当前节点的key大
		{
			parent = cur;
			cur = cur->_right;//去右边找
		}
		else if(cur->_kv.first > kv.first)//插入节点的key比当前节点的key小
		{
			parent = cur;
			cur = cur->_left;//去左边找
		}
		else
		{
			return false;//插入的节点存在
		}
	}

	//找到插入位置
	cur = new Node(kv);
	//链接
	if (parent->_kv.first > cur->_kv.first)
	{
		parent->_left = cur;
	}
	else
	{
		parent->_right = cur;
	}
	cur->_parent = parent;

	//颜色和高度调整
	//....

	return true;
}

OK,还是先来验证一下当前的逻辑对不对,所以走个中序看看是不是有序即可:由于中序要根节点,而类外面是无法访问的,所以我们还是和以前一样搞成子函数或提供get和set方法;这里就搞成子函数了:

中序遍历

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

OK,没问题,下面我们就来讨论一下红黑树的维持平衡的方式:变色和旋转!

红黑树的变色和旋转

由于新插入的节点一定是红色的,此时分为两种情况,1、父亲为黑  2、父亲为红

如果父亲为黑,不违反任何性质,插入结束;

如果父亲为红,看看叔叔,此时叔叔有三类情况:1、叔叔存在且为红  2、叔叔不存在  3、叔叔存在为黑

如果,叔叔存在且为红:变色(将父亲和叔叔变黑色,将爷爷变红色),继续更新

如果,叔叔不存在或存在且为黑,旋转 + 变色(如果孩子是父亲的左/右,先对孩子父亲进行左/右旋,在对爷爷进行左/右)

OK,这里看着可能会有些迷,看看下面的导图会很清楚:

OK,理解了上述的表达,下面我来画图解释一下:

parent是黑色插入结束

parent为红,叔叔存在且为红(变色)

父亲和叔叔变黑,爷爷变红,继续向上更新

这里只画了parent在爷爷的左边的情况,如果parent在爷爷的右边和这个是一样的!

parent为红,叔叔不存在会存在为黑(变色 + 旋转)

如果parent在爷爷的左,且cur在父亲的左,对爷爷进行右单旋;

如果parent在爷爷的左,且cur在父亲的右,先对parent左单旋,在对爷爷进行右单旋;

如果parent在爷爷的右,且cur在父亲的右,对爷爷进行左单旋;

如果parent在爷爷的右,且cur在父亲的左,先对parent右单旋,在对爷爷进行左单旋;

OK,废话不多说直接上代码:

bool Insert(const pair<K, V>& kv)
{
	Node* cur = _root;//当前节点
	Node* parent = nullptr;//插入位置的父亲

	//第一次插入
	if (_root == nullptr)
	{
		_root = new Node(kv);
		_size++;//插入成功节点数+1
		_root->_col = BLACK;//根节点是黑色
		return true;
	}

	//寻找插入的位置
	while (cur)
	{
		if (cur->_kv.first < kv.first)//插入节点的key比当前节点的key大
		{
			parent = cur;
			cur = cur->_right;//去右边找
		}
		else if(cur->_kv.first > kv.first)//插入节点的key比当前节点的key小
		{
			parent = cur;
			cur = cur->_left;//去左边找
		}
		else
		{
			return false;//插入的节点存在
		}
	}

	//找到插入位置
	cur = new Node(kv);
	//链接
	if (parent->_kv.first > cur->_kv.first)
	{
		parent->_left = cur;
	}
	else
	{
		parent->_right = cur;
	}
	cur->_parent = parent;

	//颜色和高度调整
	while (parent && parent->_col == RED)
	{
		Node* grandfather = parent->_parent;
		if (parent == grandfather->_left)//父亲在爷爷的左
		{
			Node* uncle = grandfather->_right;//叔叔就是父亲的右
			//父亲存在且为红 -> 变色
			if (uncle && uncle->_col == RED)
			{
				grandfather->_col = RED;
				parent->_col = uncle->_col = BLACK;
				//继续向上调整
				cur = grandfather;
				parent = cur->_parent;
			}
			else//叔叔不存在或存在但为黑 -> 变色 + 旋转
			{
				if (cur == parent->_left)//cur在父亲的左
				{
					//		   g
					//	   p		u
					// c
					RotateR(grandfather);//旋转
					parent->_col = BLACK;//变色
					grandfather->_col = RED;
				}
				else//cur在父亲的右
				{
					//		  g
					//	  p		  u
					//        c
					RotateL(parent);//旋转
					RotateR(grandfather);
					cur->_col = BLACK;//变色
					grandfather->_col = RED;
				}

				break;//旋转后不需要再向上更新了
			}
		}
		else//parent在爷爷的右
		{
			Node* uncle = grandfather->_left;//叔叔在父亲的左
			//叔叔存在且为红 -> 变色
			if (uncle && uncle->_col == RED)
			{
				grandfather->_col = RED;
				parent->_col = uncle->_col = BLACK;
				//继续向上调整
				cur = grandfather;
				parent = cur->_parent;
			}
			else//叔叔不存在或存在但为黑 -> 变色 + 旋转
			{
				if (cur == parent->_left)//cur在父亲的左 
				{
					//		  g
					//	  u		   p
					//        c
					RotateR(parent);//旋转
					RotateL(grandfather);
					cur->_col = BLACK;//变色
					grandfather->_col = RED;
				}
				else
				{
					//		  g
					//	  u		   p
					//                 c
					RotateL(grandfather);//旋转
					parent->_col = BLACK;//变色
					grandfather->_col = RED;
				}

				break;//旋转后不需要再向上更新了
			}
		}
	}

	_root->_col = BLACK;//保证根节点永远是黑色
	_size++;//插入成功节点数+1

	return true;
}

旋转

红黑树的旋转没有,AVL的复杂,只有左右单旋且没有平衡因子!整体的逻辑和AVL一样的,这里不在详细介绍了!

void RotateR(Node* parent)
{
	Node* subL = parent->_left;//父亲的左
	Node* subLR = subL->_right;//左子树的右
	Node* ppNode = parent->_parent;//parent的父节点,方便旋转后的链接

	parent->_left = subLR;//将左子树的右给父亲的做
	if (subLR)
		subLR->_parent = parent;

	subL->_right = parent;//parent做左子树的右
	parent->_parent = subL;

	if (parent == _root)//parent是根
	{
		_root = subL;//此时的新根就是subL
		ppNode = nullptr;
	}
	else//parent不是根
	{
		//将新的根连接到ppNode
		if (ppNode->_left == parent)
		{
			ppNode->_left = subL;
		}
		else
		{
			ppNode->_right = subL;
		}
		subL->_parent = ppNode;
	}
}

void RotateL(Node* parent)
{
	Node* subR = parent->_right;//父亲的右
	Node* subRL = subR->_left;//右子树的左
	Node* ppNode = parent->_parent;//parent的父节点,方便旋转后的链接

	parent->_right = subRL;//将右子树的左连接到parent的右
	if (subRL)
		subRL->_parent = parent;

	subR->_left = parent;//parent连接到subR的左
	parent->_parent = subR;

	if (parent == _root)//parent是根
	{
		_root = subR;//此时的新根就是subR
		ppNode = nullptr;
	}
	else//parent不是根
	{
		//将新的根连接到ppNode
		if (ppNode->_left == parent)
		{
			ppNode->_left = subR;
		}
		else
		{
			ppNode->_right = subR;
		}
		subR->_parent = ppNode;
	}
}

OK,我们验证一下,判断一下是否是平衡的:

是否平衡

先获取任意一条路径的黑色节点,然后通过dfs进行检查每个结点是不是符合红黑树的规则!

如果出现连续的红色节点,不符合!判断方式:当出现红色节点时,检查其父节点是否是红色的,如果是则不符合!

如走到空了,检查该条路径的黑色节点和一开始求出的是否一致,不一致则不符合!

当前节点符合,去检查其左右!

bool IsBalance()
{
	if (_root && _root->_col == RED)
	{
		return false;//根为红,一定不是红黑树
	}

	int black = 0;//获取任意一条路径的黑色节点(这里是最左路)
	Node* cur = _root;
	while (cur)
	{
		if (cur->_col == BLACK)
		{
			black++;
		}

		cur = cur->_left;
	}

	return Check(_root, black, 0);
}
bool Check(Node* root, const int black, int num)
{
	if (root == nullptr)
	{
		//当走到叶子节点的时候和其他路径的黑色节点的个数不一样
		if (black != num)
		{
			return false;
		}

		return true;
	}

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

	//遇到黑色节点++
	if (root->_col == BLACK)
	{
		num++;
	}

	//当前节点符合红黑树,它的左右子树也要都符合
	return Check(root->_left, black, num) && Check(root->_right, black, num);
}

OK,验证一下:

OK,么有问题!下面把其他的接口补一下!

Size

由于我们提前记录了_size所以直接返回成员_size即可!

size_t Size()
{
	return _size;
}

Find

和以前的搜索树一样,大了去右边找,小了去左边找!

Node* Find(const K& key)
{
	Node* cur = _root;
	while (cur)
	{
		if (cur->_kv.first < key)//插入节点的key比当前节点的key大
		{
			cur = cur->_right;//去右边找
		}
		else if (cur->_kv.first > key)//插入节点的key比当前节点的key小
		{
			cur = cur->_left;//去左边找
		}
		else
		{
			return cur;//找到了
		}
	}

	return nullptr;//没找到
}

再来一组随机的测试用例:插入1亿个随机值,看看时间和是否平衡(注意这里一亿个节点在32位debug下可能内存空间不够,可以把他改成64的release地址空间大一点)

void Test()
{
	const int N = 100000000;
	vector<int> v;
	v.reserve(N);
	srand(time(0));

	for (size_t i = 0; i < N; i++)
	{
		v.push_back(rand() + i);
		//cout << v.back() << endl;
	}

	size_t begin2 = clock();
	RBTree<int, int> t;
	for (auto e : v)
	{
		t.Insert(make_pair(e, e));
		//cout << "Insert:" << e << "->" << t.IsBalance() << endl;
	}
	size_t end2 = clock();

	cout << "time :" << end2 - begin2 << endl;
	cout << t.IsBalance() << endl;
}

红黑树的删除:请参考这篇博客 :红黑树的删除

全部源码

#pragma once

enum Col//颜色
{
	RED,
	BLACK
};

template<class K, class V>
struct RBTreeNode
{
	RBTreeNode<K, V>* _left;//左孩子
	RBTreeNode<K, V>* _right;//右孩子
	RBTreeNode<K, V>* _parent;//父结点
	pair<K, V> _kv;//数据域
	Col _col;//颜色

	RBTreeNode(const pair<K,V>& kv)
		:_left(nullptr)
		,_right(nullptr)
		,_parent(nullptr)
		,_kv(kv)
		,_col(RED)//新插入的节点默认是红色
	{}
};

template<class K, class V>
class RBTree
{
	typedef RBTreeNode<K, V> Node;
public:
	RBTree()
		:_root(nullptr)
		,_size(0)
	{}

	bool Insert(const pair<K, V>& kv)
	{
		Node* cur = _root;//当前节点
		Node* parent = nullptr;//插入位置的父亲

		//第一次插入
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_size++;//插入成功节点数+1
			_root->_col = BLACK;//根节点是黑色
			return true;
		}

		//寻找插入的位置
		while (cur)
		{
			if (cur->_kv.first < kv.first)//插入节点的key比当前节点的key大
			{
				parent = cur;
				cur = cur->_right;//去右边找
			}
			else if(cur->_kv.first > kv.first)//插入节点的key比当前节点的key小
			{
				parent = cur;
				cur = cur->_left;//去左边找
			}
			else
			{
				return false;//插入的节点存在
			}
		}

		//找到插入位置
		cur = new Node(kv);
		//链接
		if (parent->_kv.first > cur->_kv.first)
		{
			parent->_left = cur;
		}
		else
		{
			parent->_right = cur;
		}
		cur->_parent = parent;

		//颜色和高度调整
		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent;
			if (parent == grandfather->_left)//父亲在爷爷的左
			{
				Node* uncle = grandfather->_right;//叔叔就是父亲的右
				//父亲存在且为红 -> 变色
				if (uncle && uncle->_col == RED)
				{
					grandfather->_col = RED;
					parent->_col = uncle->_col = BLACK;
					//继续向上调整
					cur = grandfather;
					parent = cur->_parent;
				}
				else//叔叔不存在或存在但为黑 -> 变色 + 旋转
				{
					if (cur == parent->_left)//cur在父亲的左
					{
						//		   g
						//	   p		u
						// c
						RotateR(grandfather);//旋转
						parent->_col = BLACK;//变色
						grandfather->_col = RED;
					}
					else//cur在父亲的右
					{
						//		  g
						//	  p		  u
						//        c
						RotateL(parent);//旋转
						RotateR(grandfather);
						cur->_col = BLACK;//变色
						grandfather->_col = RED;
					}

					break;//旋转后不需要再向上更新了
				}
			}
			else//parent在爷爷的右
			{
				Node* uncle = grandfather->_left;//叔叔在父亲的左
				//叔叔存在且为红 -> 变色
				if (uncle && uncle->_col == RED)
				{
					grandfather->_col = RED;
					parent->_col = uncle->_col = BLACK;
					//继续向上调整
					cur = grandfather;
					parent = cur->_parent;
				}
				else//叔叔不存在或存在但为黑 -> 变色 + 旋转
				{
					if (cur == parent->_left)//cur在父亲的左 
					{
						//		  g
						//	  u		   p
						//        c
						RotateR(parent);//旋转
						RotateL(grandfather);
						cur->_col = BLACK;//变色
						grandfather->_col = RED;
					}
					else
					{
						//		  g
						//	  u		   p
						//                 c
						RotateL(grandfather);//旋转
						parent->_col = BLACK;//变色
						grandfather->_col = RED;
					}

					break;//旋转后不需要再向上更新了
				}
			}
		}

		_root->_col = BLACK;//保证根节点永远是黑色
		_size++;//插入成功节点数+1

		return true;
	}

	Node* Find(const K& key)
	{
		Node* cur = _root;
		while (cur)
		{
			if (cur->_kv.first < key)//插入节点的key比当前节点的key大
			{
				cur = cur->_right;//去右边找
			}
			else if (cur->_kv.first > key)//插入节点的key比当前节点的key小
			{
				cur = cur->_left;//去左边找
			}
			else
			{
				return cur;//找到了
			}
		}

		return nullptr;//没找到
	}

	void InOrder()
	{
		return _InOrder(_root);
	}

	size_t Size()
	{
		return _size;
	}

	bool IsBalance()
	{
		if (_root && _root->_col == RED)
		{
			return false;//根为红,一定不是红黑树
		}

		int black = 0;//获取任意一条路径的黑色节点(这里是最左路)
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
			{
				black++;
			}

			cur = cur->_left;
		}

		return Check(_root, black, 0);
	}

private:
	void _InOrder(Node* root)
	{
		if (root == nullptr)
			return;
		
		_InOrder(root->_left);
		cout << root->_kv.first << ":" << root->_kv.second << endl;
		_InOrder(root->_right);
	}
	
	void RotateR(Node* parent)
	{
		Node* subL = parent->_left;//父亲的左
		Node* subLR = subL->_right;//左子树的右
		Node* ppNode = parent->_parent;//parent的父节点,方便旋转后的链接

		parent->_left = subLR;//将左子树的右给父亲的做
		if (subLR)
			subLR->_parent = parent;

		subL->_right = parent;//parent做左子树的右
		parent->_parent = subL;

		if (parent == _root)//parent是根
		{
			_root = subL;//此时的新根就是subL
			ppNode = nullptr;
		}
		else//parent不是根
		{
			//将新的根连接到ppNode
			if (ppNode->_left == parent)
			{
				ppNode->_left = subL;
			}
			else
			{
				ppNode->_right = subL;
			}
			subL->_parent = ppNode;
		}
	}

	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;//父亲的右
		Node* subRL = subR->_left;//右子树的左
		Node* ppNode = parent->_parent;//parent的父节点,方便旋转后的链接

		parent->_right = subRL;//将右子树的左连接到parent的右
		if (subRL)
			subRL->_parent = parent;

		subR->_left = parent;//parent连接到subR的左
		parent->_parent = subR;

		if (parent == _root)//parent是根
		{
			_root = subR;//此时的新根就是subR
			ppNode = nullptr;
		}
		else//parent不是根
		{
			//将新的根连接到ppNode
			if (ppNode->_left == parent)
			{
				ppNode->_left = subR;
			}
			else
			{
				ppNode->_right = subR;
			}
			subR->_parent = ppNode;
		}
	}

	bool Check(Node* root, const int black, int num)
	{
		if (root == nullptr)
		{
			//当走到叶子节点的时候和其他路径的黑色节点的个数不一样
			if (black != num)
			{
				return false;
			}

			return true;
		}

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

		//遇到黑色节点++
		if (root->_col == BLACK)
		{
			num++;
		}

		//当前节点符合红黑树,它的左右子树也要都符合
		return Check(root->_left, black, num) && Check(root->_right, black, num);
	}


private:
	Node* _root;//根节点
	size_t _size;//节点的数量
};

红黑树的效率分析以及应用

红黑树和AVL树的比较

红黑树和AVL树都是高效的平衡二叉树,增删改查的时间复杂度都是O(log_2 N),红黑树不追 求绝对平衡,其只需保证最长路径不超过最短路径的2倍,相对而言,降低了插入和旋转的次数, 所以在经常进行增删的结构中性能比AVL树更优,而且红黑树实现比较简单,所以实际运用中红黑树更多。

红黑树的应用

C++的STL库中的map/set/multimap/multiset底层都是红黑树实现的

一些Java的库;例如: TreeMap和TreeSet等

一些Linux的内核,例如:进程调度等

OK,本期分享就到这里,好兄弟,我们下期再见!

结束语:简单的事重复做,重复的是坚持做!

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

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

相关文章

深度学习复盘与论文复现C

文章目录 4、Distributed training4.1 GPU architecture 5、Recurrent neural network5.1 The basic structure of RNN5.2 Neural networks without hidden states5.3 Recurrent neural networks with hidden states5.4 summary 6、Language Model Dataset (lyrics from Jay Ch…

从反向传播过程看激活函数与权重初始化的选择对深度神经网络稳定性的影响

之前使用深度学习时一直对各种激活函数和权重初始化策略信手拈用&#xff0c;然而不能只知其表不知其里。若想深入理解为何选择某种激活函数和权重初始化方法卓有成效还是得回归本源&#xff0c;本文就从反向传播的计算过程来按图索骥。 为了更好地演示深度学习中的前向传播和…

网站调用Edge浏览器API:https://api-edge.cognitive.microsofttranslator.com/translate

Edge浏览器有自带的翻译功能&#xff0c;在运行pc项目可能会遇到疯狂调用Edge的API https://api-edge.cognitive.microsofttranslator.com/translate 这个URL&#xff08;https://api-edge.cognitive.microsofttranslator.com/translate&#xff09;指向的是微软服务中的API接…

单片机数码管时钟电路的设计

5 调试 数码管的引脚1&#xff5e;4&#xff0c;a&#xff5e;g以及小数点的排列都不是连续的&#xff0c;这就意味着难免需要飞线。数码管是分共阴和共阳的&#xff0c;起初我错把原理图中的共阳数码管当成了共阴数码管&#xff0c;焊上去了之后才发现&#xff0c;为了避免拆卸…

手写mybatis-预编译前的sql语句

sql表 mybatis数据库中的gxa_user表 /*Navicat Premium Data TransferSource Server : rootSource Server Type : MySQLSource Server Version : 80028Source Host : localhost:3306Source Schema : mybatisTarget Server Type : MySQLTarget…

C++ Easyx案例实战:Cookie Maker工作室1.0V

前言 //制作属于自己的工作室&#xff01; 注&#xff1a;运行效果以及下载见Cookie Maker 工作室成立程序。 关于Cookie Maker工作室成立的信息&#xff0c;I am very happy&#xff08;唔……改不过来了&#xff09;。 OKOK&#xff0c;第一次用图形库写程序&#xff08;图形…

一分钟有60秒,这个有趣的原因你知道吗?

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

【JavaEE】Spring IoCDI详解

一.基本概念 1.Ioc基本概念 Ioc: Inversion of Control (控制反转), 也就是说 Spring 是⼀个"控制反转"的容器. 什么是控制反转呢? 也就是控制权反转. 什么的控制权发发了反转? 获得依赖对象的过程被反转了也就是说, 当需要某个对象时, 传统开发模式中需要自己通…

minio的一个基础使用案例:用户头像上传

文章目录 一、minio下载安装&#xff08;Windows&#xff09;二、案例需求分析三、后端接口开发 一、minio下载安装&#xff08;Windows&#xff09; 1. 下载minio服务端和客户端 minio下载地址 2. 手动搭建目录 /minio/binmc.exeminio.exe/data/logs手动创建minio应用程序目…

66. UE5 RPG 实现远程攻击武器配合角色攻击动画

在制作游戏中&#xff0c;我们制作远程攻击角色&#xff0c;他们一般会使用弓箭&#xff0c;弩&#xff0c;弹弓等武器来进行攻击。比如你使用弓箭时&#xff0c;如果角色在播放拉弓弦的动画&#xff0c;但是弓箭武器没有对应的表现&#xff0c;会显得很突兀。所以&#xff0c;…

GDPU Java 天码行空15 数据库编程

一、实验目的 1、 了解数据库的基础知识。 2、 掌握MySQL的下载、安装与配置。 3、 掌握MySQL可视化工具的使用。 4、 了解SQL语言。 5、 掌握JDBC中的API&#xff0c;并能进行简单的数据库操作。 二、实验内容 1、 安装MySQL &#x1f468;‍&#x1f3eb; 视频教程 2、建…

私有云和多云管理平台 | Cloudpods v3.11.4 正式发布

本次 3.11.4 更新亮点为&#xff1a;系统镜像引入社区镜像&#xff0c;用户可以一键导入各主流开源操作系统镜像&#xff0c;方便用户上手使用。持续迭代共享 LVM&#xff0c;支持快照&#xff0c;主备机等特性&#xff0c;修复迁移删除镜像缓存等 BUG。 功能优化 【费用】费…

linux动态调试 dev_dbg

动态调试使用方法 打开内核动态调试开关&#xff0c;make menuconfig选中CONFIG_DYNAMIC_DEBUG以及CONFIG_DEBUG_FS Linux启动后&#xff0c;使用命令行挂载上dbgfs 1. mkdir /mnt/dbg 2. mount -t debugfs none /mnt/dbg 1.控制某个文件所有dev_dbg()&#xff0c; echo -n &q…

mongodb总概

一、mongodb概述 mongodb是最流行的nosql数据库&#xff0c;由C语言编写。其功能非常丰富&#xff0c;包括: 面向集合文档的存储:适合存储Bson(json的扩展)形式的数据;格式自由&#xff0c;数据格式不固定&#xff0c;生产环境下修改结构都可以不影响程序运行;强大的查询语句…

MSPM0l1306——配置滴答定时器

我们配置好了滴答定时器之后&#xff0c;还要手动编写滴答定时器的中断服务函数&#xff0c;因为我们开启的滴答定时器的中断&#xff0c;当滴答定时器的计数值从我们设置的值减到0时&#xff0c;就会触发一次中断&#xff0c;触发中断就会执行中断服务函数。各个中断的中断服务…

144、二叉树的前序递归遍历

题解&#xff1a; 递归书写三要素&#xff1a; 1&#xff09;确定递归函数的参数和返回值。要确定每次递归所要用到的参数以及需要返回的值 2&#xff09;确定终止条件。操作系统也是用栈的方式实现递归&#xff0c;那么如果不写终止条件或者终止条件写的不对&#xff0c;都…

Here Doucument

一、Here Document概述 1.概念 使用I/0重定向的方式将命令列表提供给交互式程序 标准输入的一种替代品 2.语法格式 命令 <<标记 标记 3.注意事项 标记可以使用任意合法字符&#xff08;通常为EOF&#xff09; 结尾的标记一定要顶格写&#xff0c;前面不能有任何字符…

【iOS】内存泄漏检查及原因分析

目录 为什么要检测内存泄漏&#xff1f;什么是内存泄漏&#xff1f;内存泄漏排查方法1. 使用Zombie Objects2. 静态分析3. 动态分析方法定位修改Leaks界面分析Call Tree的四个选项&#xff1a; 内存泄漏原因分析1. Leaked Memory&#xff1a;应用程序未引用的、不能再次使用或释…

Java数据结构准备工作---常用类

文章目录 前言1.包装类1.1.包装类基本知识1.2.包装类的用途1.3.装箱和拆箱1.3.1.装箱&#xff1a;1.3.2.拆箱 1.4 包装类的缓存问题 2.时间处理类2.1.Date 时间类(java.util.Date)2.2.DateFormat 类和 SimpleDateFormat 类2.3.Calendar 日历类 3.其他常用类3.1.Math类3.2.Rando…

嵌入式中C语言经典的面试题分享

#error的作用是什么? #error 指令让预处理器发出一条错误信息,并且会中断编译过程。下面我们从Linux代码中抽取出来一小段代码并做修改得到示例代码: 这段示例代码很简单,当RX_BUF_IDX宏的值不为0~3时,在预处理阶段就会通过 #error 指令输出一条错误提示信息: "…