数据结构(六)——红黑树及模拟实现

news2025/4/21 2:46:48

目录

前言

红黑树的概念及性质

红黑树的效率

红黑树的结构

红黑树的插入

变色不旋转

单旋+变色

双旋+变色

插入代码如下所示:

红黑树的查找

红黑树的验证

红黑树代码如下所示:

小结


前言

在前面的文章我们介绍了AVL这一棵完全二叉搜索树,我们分析后发现AVL树查找的时间复杂度是O(logN),说明这是一个很优秀的数据结构,但是在底层实现我们发现,AVL树为了维持它的性质,是需要进行频繁的旋转的,这样会造成不必要的消耗,那么有没有一种数据结构既可保证它的时间复杂度为O(logN),又能避免频繁的旋转呢?这就是我们今天要介绍的红黑树,学习了红黑树之后我们将用红黑树作为底层,去封装实现一个模拟的map和set,下面让我们一起来学习吧。

参考文章:

据结构(五)——AVL树(平衡二叉搜索树)

红黑树的概念及性质

首先我们要知道什么是红黑树,与AVL树相比,红黑树的节点多了一个标志符号,这个标志符号表示节点的颜色,我们这里用枚举来表示,通过控制节点的颜色来判平衡以及判断是否需要旋转。由于红黑树的旋转不像AVL树那么频繁,所以它是一颗不完全平衡二叉搜索树。

下面我们引入路径的概念,我们把从根节点开始,一直到空节点的一条路线称为路径

它的规则如下所示:

规则一:根节点必须是黑色,其余节点的颜色不是黑色就是红色

规则二:每个红色节点的左右子树必须是红色的,即:不存在连续的红色节点

规则三:每条路径的黑色节点数量是相同的,其中我们把空节点视为黑色节点

规则四:最长路径的节点数量不超过最长路径节点数量的二倍

分析上面的规则我们发现,其中最关键也是最难维护的就是规则三,我们发现,在极端场景下,最短路径就是全为黑色节点的路径,最长路径就是红黑间隔的路径,所以维护好了规则三,规则四自然就实现了

红黑树的效率

由于有规则四的存在,我们设红黑树最短路径的节点数量是h,那么最长路径的节点数量就是2*h,我们设每条路径上黑色节点的数量是N,那么他们应该满足:2^{h}-1<=N<2^{2*h}-1,也就是说,如果我们要在红黑树当中进行增删查改,最少要走的长度是2^{h}-1,查找的次数就是log(N),最坏也就是查找2*log(N),分析下来,它的时间复杂度还是log(N)。

红黑树的结构

从上面的分析中我们知道,红黑树的大致框架与AVL树相似,只不过将平衡因子改成了用枚举表示的颜色,后续我们也是通过控制颜色来判平衡,因此红黑树的结构如下所示:

enum Colour
{
	BLACK,
	RED
};
template<class K, class V>
struct RBTreeNode
{
	pair<K, V> _kv;
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	Colour _col;

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

template<class K, class V>
class RBTree
{
	typedef RBTreeNode<K, V> Node;
public:
private:
	Node* _root = nullptr;
};

红黑树的插入

我们来分析红黑树的插入过程,首先由于它是一颗搜索树,所以按照搜索树的规则进行插入,更改插入结点的颜色,然后我们向上进行调整。

对于插入节点的颜色,我们发现,如果我们插入黑色,那么就在当前路径上增加了一个黑色节点,不符合每条路径上黑色节点数量相同这一规则,所以我们插入节点的颜色应该为红色。代码如下所示:

bool Insert(const pair<K, V>& kv)
{
	if (_root == nullptr)
	{
		_root = new Node(kv);
		_root->_col = BLACK;
		return true;
	}

	Node* cur = _root;
	Node* parent = nullptr;
	while (cur)
	{
		if (cur->_kv.first > kv.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (cur->_kv.first < kv.first)
		{
			parent = cur;
			cur = cur->_right;
		}
		else
		{
			cout << "插入值已经存在,请重新插入" << endl;
			return false;
		}
	}

	cur = new Node(kv);
	cur->_col = RED;
	if (parent->_kv.first < kv.first)
	{
		parent->_right = cur;
	}
	else
	{
		parent->_left = cur;
	}
	cur->_parent = parent;
}

当我们插入红色节点后,会出下面几种情况:

情况一:父节点为黑色,不违反红黑树的规则,那么就停止向上调整

情况二:父节点为红色,违反规则,那么就需要向上调整

接下来我们重点分析情况二。由于不能有两个连续的红色节点,所以我们应当把父节点变成黑色,但是将父节点变成黑色后我们就在当前路径上凭空增加了一个黑色节点,这样破坏了黑色节点数量相同这一规则,那么我们就需要调整其他路径上黑色节点的数量,如果是这样的话,是不符合我们的预期的,我们再仔细观察红黑树的结构,我们把父节点的父节点定义为grandfather(祖父节点),那么祖父节点的另一个子节点称为uncle(叔节点),我们发现,无论什么情况,只要涉及到需要向上调整,祖父节点总是黑色的,此时,如果叔节点存在且为红色的话,我们就只需要将父节点和叔节点颜色变为黑色,再将祖父节点变为红色,这样就解决了这个问题;如果叔节点不存在或者存在且为空节点,那么此时我们就需要做特殊处理。

通过上述分析,我们总结一下红黑树的插入过程:

第一步:插入节点。按照二叉搜索树的规则插入节点,链接父节点,将插入节点的颜色改成红色,如果是根节点,就变为黑色。

第二步:向上调整黑色节点的数量,让其满足红黑树的规则。

接下来我们逐步来分析向上调整。

根据上面的分析我们知道,我们可以根据uncle节点的有无以及颜色,可以分为下面两种情况:

情况一:uncle节点存在且颜色为红色

情况二:uncle节点存在且为黑色或者uncle不存在

如下图所示:

对于情况一,我们只需要将父节点和uncle节点颜色变为黑色,然后再将祖父节点颜色变为红色,然后向上更新parent节点和grandfather节点,因此我们需要一个循环,循环结束的条件是:parent为空或者parent的颜色为黑色。

对于情况二,我们发现这种情况又需要分为两种场景,一种是插入节点是parent的左节点,另一种是插入节点是parent的右节点,这两种情景无论我们怎么变色都没有办法满足红黑树的规则,为此我们需要寻找一种新的解决办法。

回到红黑树的定义,我们知道,红黑树是一颗不完全平衡二叉搜索树,从前面的AVL树我们知道,要成为一颗平衡树,我们要做的就是旋转,而旋转分为左旋和右旋,那么我们尝试将AVL树的旋转带入红黑树当中,那么对于parent与cur都位于同一侧节点的这种情况,我们使用单旋,对于parent与cur位于不同一侧节点的这种情况,我们使用双旋。

综上所述,红黑树的插入可以分为以下三种不同的情况:

变色不旋转

条件:grandfather为黑,parent与cur为红色,uncle存在且为红。

解决办法:parent与cur变为黑色,grandfather变为红色,然后再将grandfather当成新的cur,继续向上更新。

分析:由于cur与parent都是红色,所以我们应该把parent变为黑色,这样就在当前节点凭空增加了一个黑色节点,从而导致黑色节点失衡;由于uncle是红色,为了让黑色节点数量保持平衡,所以我们需要把uncle也变成黑色,然后让grandfather变为红色。

单旋+变色

条件:grandfather为黑色,parent与cur为红色,uncle不存在或者uncle存在且为黑,并且parent与cur位于同一侧

解决办法:以grandfather节点为旋转点,如果位于左侧则右旋;如果位于右侧则左旋,旋转结束后让parent变为黑色,grandfather变成红色

分析:由于uncle是黑色节点,如果我们直接按照第一种情况进行变色,那么就会导致uncle这条路径上始终少一个节点,这样导致节点失衡,所以我们要先进行旋转,旋转结束后parent成为了cur与grandfather的父节点,为了让黑色节点数量保持平衡,我们需要让parent变成黑色,让grandfather变成红色

双旋+变色

条件:grandfather为黑色,parent与cur为红色,uncle不存在或者uncle存在且为黑,并且parent与cur位于不同侧

解决办法:首先以parent为旋转点进行一次旋转,然后再以grandfather为旋转点进行一次旋转,旋转结束后让cur变成黑色,让grandfather变成红色。

分析:由于uncle是黑节点,cur与parent位于相反的两侧,如果我们旋转一次,结果还是这种情况,所以我们需要进行两次旋转,旋转结束后,为了保持黑色节点数量的平衡,我们需要将cur变成黑色,让grandfather变成红色

插入代码如下所示:

bool Insert(const pair<K, V>& kv)
{
	if (_root == nullptr)
	{
		_root = new Node(kv);
		_root->_col = BLACK;
		return true;
	}

	Node* cur = _root;
	Node* parent = nullptr;
	while (cur)
	{
		if (cur->_kv.first > kv.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (cur->_kv.first < kv.first)
		{
			parent = cur;
			cur = cur->_right;
		}
		else
		{
			cout << "插入值已经存在,请重新插入" << endl;
			return false;
		}
	}

	cur = new Node(kv);
	cur->_col = RED;
	if (parent->_kv.first < kv.first)
	{
		parent->_right = cur;
	}
	else
	{
		parent->_left = cur;
	}
	cur->_parent = parent;

	while (parent && parent->_col == RED)
	{
		Node* grandfather = parent->_parent;

		if (grandfather->_left == parent)
		{
			Node* uncle = grandfather->_right;
			if (uncle && uncle->_col == RED)
			{
				parent->_col = uncle->_col = BLACK;
				grandfather->_col = RED;

				cur = grandfather;
				parent = cur->_parent;
			}
			else
			{
				if (parent->_left == cur)
				{
					RotateR(grandfather);
					grandfather->_col = RED;
					parent->_col = BLACK;
				}
				else
				{
					RotateL(parent);
					RotateR(grandfather);

					cur->_col = BLACK;
					grandfather->_col = RED;
				}
				break;
			}
		}
		else
		{
			Node* uncle = grandfather->_left;
			if (uncle && uncle->_col == RED)
			{
				parent->_col = uncle->_col = BLACK;
				grandfather->_col = RED;

				cur = grandfather;
				parent = cur->_parent;
			}
			else
			{
				if (parent->_right == cur)
				{
					RotateL(grandfather);
					grandfather->_col = RED;
					parent->_col = BLACK;
				}
				else
				{
					RotateR(parent);
					RotateL(grandfather);

					cur->_col = BLACK;
					grandfather->_col = RED;
				}
				break;
			}
		}
	}

	_root->_col = BLACK;
	return true;
}
void RotateL(Node* parent)
{
	Node* subR = parent->_right;
	Node* subRL = subR->_left;

	parent->_right = subRL;
	subR->_left = parent;

	if (subRL)
		subRL->_parent = parent;
	Node* grandfather = parent->_parent;
	parent->_parent = subR;

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

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

	parent->_left = subLR;
	subL->_right = parent;

	if (subLR)
		subLR->_parent = parent;
	Node* grandfather = parent->_parent;
	parent->_parent = subL;
	if (_root == parent)
	{
		_root = subL;
		subL->_parent = nullptr;
	}
	else
	{
		if (grandfather->_left == parent)
		{
			grandfather->_left = subL;
		}
		else
		{
			grandfather->_right = subL;
		}
		subL->_parent = grandfather;
	}
}

旋转逻辑在这篇文章介绍:

数据结构(五)——AVL树(平衡二叉搜索树)

红黑树的查找

红黑树的查找与二叉搜索树相同,都是通过遍历比较节点值与目标值的大小知道找到目标值所在的节点然后返回或者直到遍历完这颗树为止,其代码如下所示:

Node* Find(const pair<K, V>& kv)
{
	Node* cur = _root;
	Node* parent = nullptr;
	while (cur)
	{
		if (cur->_kv.first > kv.first)
		{
			parent = cur;
			cur = cur->_left;
		}
		else if (cur->_kv.first < kv.first)
		{
			parent = cur;
			cur = cur->_right;
		}
		else
		{
			return cur;
		}
	}
	cout << "目标值不存在" << endl;
	return nullptr;
}

红黑树的验证

前面我们验证红黑树时使用的方法是验证平衡因子以及左右子树高度差,实际上我们是按照AVL树的规则进行验证的,那么对于红黑树而言我们要通过它的规则进行验证,检查最长路径不超过最短路径的二倍吗?这样显然是不现实的,那么我们是通过什么方法去验证呢?我们还是通过递归去验证,不同的是我们引入了一个新的参数——参考值,这个值是用来记录从根节点到空节点这一整条路径中黑色节点的数量的,我们用这个值作为参考去比较其他路径上黑色节点的数量是否与这个值相同。其代码如下所示:

bool Check(Node* root, int blackNum, const int refNum)
{
	if (root == nullptr)
	{
		// 前序遍历⾛到空时,意味着⼀条路径⾛完了
		//cout << blackNum << endl;
		if (refNum != blackNum)
		{
			cout << "存在⿊⾊结点的数量不相等的路径" << endl;
			return false;
		}
		return true;
	}
	// 检查孩⼦不太⽅便,因为孩⼦有两个,且不⼀定存在,反过来检查⽗亲就⽅便多了
	if (root->_col == RED && root->_parent->_col == RED)
	{
		cout << root->_kv.first << "存在连续的红⾊结点" << endl;
		return false;
	}
	if (root->_col == BLACK)
	{
		blackNum++;
	}
	return Check(root->_left, blackNum, refNum)
		&& Check(root->_right, blackNum, refNum);
}
bool IsBalance()
{
	if (_root == nullptr)
		return true;
	if (_root->_col == RED)
		return false;
	// 参考值
	int refNum = 0;
	Node* cur = _root;
	while (cur)
	{
		if (cur->_col == BLACK)
		{
			++refNum;
		}
		cur = cur->_left;
	}
	return Check(_root, 0, refNum);
}

红黑树代码如下所示:

enum Colour
{
	BLACK,
	RED
};
template<class K, class V>
struct RBTreeNode
{
	pair<K, V> _kv;
	RBTreeNode<K, V>* _left;
	RBTreeNode<K, V>* _right;
	RBTreeNode<K, V>* _parent;
	Colour _col;

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

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

		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				cout << "插入值已经存在,请重新插入" << endl;
				return false;
			}
		}

		cur = new Node(kv);
		cur->_col = RED;
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}
		cur->_parent = parent;

		while (parent && parent->_col == RED)
		{
			Node* grandfather = parent->_parent;

			if (grandfather->_left == parent)
			{
				Node* uncle = grandfather->_right;
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					if (parent->_left == cur)
					{
						RotateR(grandfather);
						grandfather->_col = RED;
						parent->_col = BLACK;
					}
					else
					{
						RotateL(parent);
						RotateR(grandfather);

						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
			else
			{
				Node* uncle = grandfather->_left;
				if (uncle && uncle->_col == RED)
				{
					parent->_col = uncle->_col = BLACK;
					grandfather->_col = RED;

					cur = grandfather;
					parent = cur->_parent;
				}
				else
				{
					if (parent->_right == cur)
					{
						RotateL(grandfather);
						grandfather->_col = RED;
						parent->_col = BLACK;
					}
					else
					{
						RotateR(parent);
						RotateL(grandfather);

						cur->_col = BLACK;
						grandfather->_col = RED;
					}
					break;
				}
			}
		}

		_root->_col = BLACK;
		return true;
	}
	void RotateL(Node* parent)
	{
		Node* subR = parent->_right;
		Node* subRL = subR->_left;

		parent->_right = subRL;
		subR->_left = parent;

		if (subRL)
			subRL->_parent = parent;
		Node* grandfather = parent->_parent;
		parent->_parent = subR;

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

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

		parent->_left = subLR;
		subL->_right = parent;

		if (subLR)
			subLR->_parent = parent;
		Node* grandfather = parent->_parent;
		parent->_parent = subL;
		if (_root == parent)
		{
			_root = subL;
			subL->_parent = nullptr;
		}
		else
		{
			if (grandfather->_left == parent)
			{
				grandfather->_left = subL;
			}
			else
			{
				grandfather->_right = subL;
			}
			subL->_parent = grandfather;
		}
	}
	Node* Find(const pair<K, V>& kv)
	{
		Node* cur = _root;
		Node* parent = nullptr;
		while (cur)
		{
			if (cur->_kv.first > kv.first)
			{
				parent = cur;
				cur = cur->_left;
			}
			else if (cur->_kv.first < kv.first)
			{
				parent = cur;
				cur = cur->_right;
			}
			else
			{
				return cur;
			}
		}
		cout << "目标值不存在" << endl;
		return nullptr;
	}

	bool Check(Node* root, int blackNum, const int refNum)
	{
		if (root == nullptr)
		{
			// 前序遍历⾛到空时,意味着⼀条路径⾛完了
			//cout << blackNum << endl;
			if (refNum != blackNum)
			{
				cout << "存在⿊⾊结点的数量不相等的路径" << endl;
				return false;
			}
			return true;
		}
		// 检查孩⼦不太⽅便,因为孩⼦有两个,且不⼀定存在,反过来检查⽗亲就⽅便多了
		if (root->_col == RED && root->_parent->_col == RED)
		{
			cout << root->_kv.first << "存在连续的红⾊结点" << endl;
			return false;
		}
		if (root->_col == BLACK)
		{
			blackNum++;
		}
		return Check(root->_left, blackNum, refNum)
			&& Check(root->_right, blackNum, refNum);
	}
	bool IsBalance()
	{
		if (_root == nullptr)
			return true;
		if (_root->_col == RED)
			return false;
		// 参考值
		int refNum = 0;
		Node* cur = _root;
		while (cur)
		{
			if (cur->_col == BLACK)
			{
				++refNum;
			}
			cur = cur->_left;
		}
		return Check(_root, 0, refNum);
	}
private:
	Node* _root = nullptr;
};

小结

本篇文章我们着重介绍了红黑树的概念以及插入逻辑的实现,红黑树作为一颗不完全平衡二叉搜索树有着独特的地方,它是通过节点的颜色来判断是否平衡以及是否需要旋转操作,它省去了频繁旋转所带来的消耗,从而更加追求效率,总的来说,红黑树是一颗非常优秀的数据结构

以上就是本篇博客的主要内容,如果对您有所帮助的话,记得点赞、评论、关注加转发,您的支持就是我创作的最大动力

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

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

相关文章

解决 Vue 中 input 输入框被赋值后,无法再修改和编辑的问题

目录 需求&#xff1a; 出现 BUG&#xff1a; Bug 代码复现 解决问题&#xff1a; 解决方法1&#xff1a; 解决方法2 关于 $set() 的补充&#xff1a; 需求&#xff1a; 前段时间&#xff0c;接到了一个需求&#xff1a;在选择框中选中某个下拉菜单时&#xff0c;对应的…

【差分隐私相关概念】瑞丽差分隐私(RDP)-瑞丽散度约束了贝叶斯因子后验变化

分步解释和答案&#xff1a; 在Rnyi差分隐私&#xff08;RDP&#xff09;框架中&#xff0c;通过贝叶斯因子和Rnyi散度的关系可以推导出关于后验变化的概率保证。以下是关键步骤的详细解释&#xff1a; 1. 贝叶斯因子的定义与分解 设相邻数据集 D D D 和 D ′ D D′&#x…

前端快速入门——JavaScript函数、DOM

1.JavaScript函数 函数是一段可重复使用的代码块&#xff0c;它接受输入(参数)、执行特定任务&#xff0c;并返回输出。 <scricpt>function add(a,b){return ab;}let cadd(5,10);console.log(c); </script>2.JavaScript事件 JavaScript绑定事件的方法&#xff1…

10【模块学习】LCD1602(二):6路温度显示+实时时钟

项目&#xff1a;6路温度显示实时时钟 1、6路温度显示①TempMenu.c文件的代码②TempMenu.h文件的代码③main.c文件的代码④Timer.c文件的代码⑤Delay.c文件的代码⑥Key.c文件的代码 2、实时时钟显示①BeiJingTime.c文件的代码②BeiJingTime.h文件的代码③main.c文件的代码如下④…

PDF处理控件Aspose.PDF指南:使用 C# 从 PDF 文档中删除页面

需要从 PDF 文档中删除特定页面&#xff1f;本快速指南将向您展示如何仅用几行代码删除不需要的页面。无论您是清理报告、跳过空白页&#xff0c;还是在共享前自定义文档&#xff0c;C# 都能让 PDF 操作变得简单高效。学习如何以编程方式从 PDF 文档中选择和删除特定页面&#…

如何在不同版本的 Elasticsearch 之间以及集群之间迁移数据

作者&#xff1a;来自 Elastic Kofi Bartlett 当你想要升级一个 Elasticsearch 集群时&#xff0c;有时候创建一个新的独立集群并将数据从旧集群迁移到新集群会更容易一些。这让用户能够在不冒任何停机或数据丢失风险的情况下&#xff0c;在新集群上使用所有应用程序测试其所有…

Day08【基于预训练模型分词器实现交互型文本匹配】

基于预训练模型分词器实现交互型文本匹配 目标数据准备参数配置数据处理模型构建主程序测试与评估总结 目标 本文基于预训练模型bert分词器BertTokenizer&#xff0c;将输入的文本以文本对的形式&#xff0c;送入到分词器中得到文本对的词嵌入向量&#xff0c;之后经过若干网络…

npm和npx的作用和区别

npx 和 npm 是 Node.js 生态系统中两个常用的工具&#xff0c;它们有不同的作用和使用场景。 1. npm&#xff08;Node Package Manager&#xff09; 作用&#xff1a; npm 是 Node.js 的包管理工具&#xff0c;主要用于&#xff1a; 安装、卸载、更新项目依赖&#xff08;包&a…

C++学习之金融类安全传输平台项目git

目录 1.知识点概述 2.版本控制工具作用 3.git和SVN 4.git介绍 5.git安装 6.工作区 暂存区 版本库概念 7.本地文件添加到暂存区和提交到版本库 8.文件的修改和还原 9.查看提交的历史版本信息 10.版本差异比较 11.删除文件 12.本地版本管理设置忽略目录 13.远程git仓…

CCF CSP 第36次(2024.12)(1_移动_C++)

CCF CSP 第36次&#xff08;2024.12&#xff09;&#xff08;1_移动_C&#xff09; 解题思路&#xff1a;思路一&#xff1a; 代码实现代码实现&#xff08;思路一&#xff09;&#xff1a; 时间限制&#xff1a; 1.0 秒 空间限制&#xff1a; 512 MiB 原题链接 解题思路&…

7.thinkphp的路由

一&#xff0e;路由简介 1. 路由的作用就是让URL地址更加的规范和优雅&#xff0c;或者说更加简洁&#xff1b; 2. 设置路由对URL的检测、验证等一系列操作提供了极大的便利性&#xff1b; 3. 路由是默认开启的&#xff0c;如果想要关闭路由&#xff0c;在config/app.php配置…

(五)机器学习---决策树和随机森林

在分类问题中还有一个常用算法&#xff1a;就是决策树。本文将会对决策树和随机森林进行介绍。 目录 一.决策树的基本原理 &#xff08;1&#xff09;决策树 &#xff08;2&#xff09;决策树的构建过程 &#xff08;3&#xff09;决策树特征选择 &#xff08;4&#xff0…

【项目管理】第16章 项目采购管理-- 知识点整理

项目管理-相关文档&#xff0c;希望互相学习&#xff0c;共同进步 风123456789&#xff5e;-CSDN博客 &#xff08;一&#xff09;知识总览 项目管理知识域 知识点&#xff1a; &#xff08;项目管理概论、立项管理、十大知识域、配置与变更管理、绩效域&#xff09; 对应&…

从图像“看出动作”

&#x1f4d8; 第一部分&#xff1a;运动估计&#xff08;Motion Estimation&#xff09; &#x1f9e0; 什么是运动估计&#xff1f; 简单说&#xff1a; &#x1f449; 给你一段视频&#xff0c;计算机要“看懂”里面什么东西动了、往哪动了、有多快。 比如&#xff1a; 一…

鸿蒙案例---生肖抽卡

案例源码&#xff1a; Zodiac_cards: 鸿蒙生肖抽奖卡片 效果演示 初始布局 1. Badge 角标组件 此处为语雀内容卡片&#xff0c;点击链接查看&#xff1a;https://www.yuque.com/kevin-nzthp/lvl039/rccg0o4pkp3v6nua 2. Grid 布局 // 定义接口 interface ImageCount {url:…

宿舍管理系统(servlet+jsp)

宿舍管理系统(servletjsp) 宿舍管理系统是一个用于管理学生宿舍信息的平台&#xff0c;支持超级管理员、教师端和学生端三种用户角色登录。系统功能包括宿舍管理员管理、学生管理、宿舍楼管理、缺勤记录、添加宿舍房间、心理咨询留言板、修改密码和退出系统等模块。宿舍管理员…

驱动-兼容不同设备-container_of

驱动兼容不同类型设备 在 Linux 驱动开发中&#xff0c;container_of 宏常被用来实现一个驱动兼容多种不同设备的架构。这种设计模式在 Linux 内核中非常常见&#xff0c;特别 是在设备驱动模型中。linux内核的主要开发语言是C&#xff0c;但是现在内核的框架使用了非常多的面向…

MySQLQ_数据库约束

目录 什么是数据库约束约束类型NOT NULL 非空约束UNIQUE 唯一约束PRIMARY KEY主键约束FOREIGN KEY外键约束CHECK约束DEFAULT 默认值(缺省)约束 什么是数据库约束 数据库约束就是对数据库添加一些规则&#xff0c;使数据更准确&#xff0c;关联性更强 比如加了唯一值约束&#…

责任链设计模式(单例+多例)

目录 1. 单例责任链 2. 多例责任链 核心区别对比 实际应用场景 单例实现 多例实现 初始化 初始化责任链 执行测试方法 欢迎关注我的博客&#xff01;26届java选手&#xff0c;一起加油&#x1f498;&#x1f4a6;&#x1f468;‍&#x1f393;&#x1f604;&#x1f602; 最近在…

林纳斯·托瓦兹:Linux系统之父 Git创始人

名人说&#xff1a;路漫漫其修远兮&#xff0c;吾将上下而求索。—— 屈原《离骚》 创作者&#xff1a;Code_流苏(CSDN)&#xff08;一个喜欢古诗词和编程的Coder&#x1f60a;&#xff09; 林纳斯托瓦兹&#xff1a;Linux之父、Git创始人 一、传奇人物的诞生 1. 早年生活与家…