C++ - 红黑树 介绍 和 实现

news2024/12/24 20:42:05

前言

 前面 学习了 AVL树,AVL树虽然在 查找方面始终拥有 O(log N )的极高效率,但是,AVL 树在插入 ,删除等等 修改的操作当中非常的麻烦,尤其是 删除操作,在实现当中细节非常多,在实现上非常难掌控。具体可以看以下两篇文章:
C++ - AVL 树 介绍 和 实现 (上篇)_chihiro1122的博客-CSDN博客

C++ - AVL树实现(下篇)- 调试小技巧_chihiro1122的博客-CSDN博客

 而 红黑树 在构建就下个对容易一些了。

红黑树 介绍 

红黑树和 AVL树一样,都是 二叉平衡搜索树,红黑树的构建是在 每个结点除了 孩子的链接关系,和值以外,加一个位置存储该结点的颜色,一个结点的颜色只有   red 和 black 两种。红黑树通过对 任意一条路径上的各个结点的颜色限制确保没有一条路径会比其他路径长出两倍

 相比于 AVL树,红黑树对于平衡的要求更加宽松,他只是要求最长路径最多是 最短路径的两倍;

而 AVL树 是严格限死了 左右子树高度不能超过 2 。

也就是说,红黑树在高度上 要比 AVL树要高一些。

虽然 红黑树 在平衡上更加的宽松,但是实际的效率却依旧非常的高,并不比 AVL树差,至于为什么我们在下述来验证。

而且 ,红黑树不会进行 旋转,尽管在 AVL树当中旋转是常量级的,但是,数量多了之后,还是有很多的消耗。

在AVL树当中,插入和删除都要经过很多次的旋转。也就造成不小的消耗。

而 AVL树 相比于 红黑树 多出来的消耗在 红黑树看来是没有必要的。

 如下图当中的分析:

红黑树树的性质(规则)

  • 每个结点不是红色就是黑色
  •  根节点是黑色的
  • 如果一个节点是红色的,则它的两个孩子结点必须是黑色的 。(每一条从根结点开始的路径,没有重复的红色结点)
  • 对于每个结点,从该结点到其所有后代叶结点的简单路径上,均 包含相同数目的黑色结点。(红黑树当中每一条路径上,黑色结点数是相同的)(在一条路径当中,黑色结点的占比一定 大于 或 等于 1 /2 )
  • 每一个 “叶子结点” 都是黑色的,注意:此处的叶子结点不是我们以前理解的 最后一个结点,在红黑树当中的 这里的 “叶子结点” 实际上是最后一个 NULL 结点(所有的 NIL 结点都是 黑色的)。如下图所示:
     

 在红黑树当中,对 这个 NULL 结点进行了重新定义,取名为 NIL 结点而且我们上述所说的路径,是指:从根结点到最后的 NIL 结点,为一条路径。

 就比如上述,如果不加上 NULL ,那么计算出来的路径数目就是 5 条;但是如果加上 NULL ,来计算的话就是 11 条,而11 跳才是正确。

 也就是说,如果你不加上最后一个 NULL 结点来用 红黑树的规则来判断 一颗树是不是红黑树的话,机会出错,比如下述例子:
 

 如果,你按照我们以前认为,最后一个结点就是叶子结点,而且在算上路径时候没有加上最后的NULL。那么你就会认为,这是一颗红黑树;

但,实际上这不是一颗红黑树。 

 其实,按照上述 的 第五条性质,其实就是在每一条路径的后面都加上了 一个 黑色结点。

insert()插入函数 

 首先我们来想一下,当一颗红黑树当中已经有结点了,如下所示,那么当我们插入一个结点是插入红色的结点还是 插入一个黑色的结点好呢?

 如果插入 黑色结点就会违反 条件4,;如果插入 红色结点就会违反 条件3;

如果要违反条件的话,可能是违反 条件3更好。

因为 ,如果违反条件4,就会影响全部路径;而如果 违反条件3 就只会 影响一个路径,不会是全部路径。

如下所示,我们插入一个黑色结点:
 

 在插入A结点之后,到A结点的这条路径的 黑色结点的个数就 +1了,但是其他路径上都没有变化,此时就影响到其他 路径了。

但是如果插入的是一个红色的结点,只影响一个路径,所以,当发生错误的时候我们可以修正:

 上述插入的B结点,在 22 的下面,22 是红色结点,这个位置就不行但是如果是插入在 15 的下面,15 是一个黑色的结点,是可以的。

 如果 红色结点 B 插入到 22 的下面,此时 B 的父亲结点是 红色的,说明B 一定会有 父亲的父亲,也就是说 grandfather。因为 整棵树只有根结点才没有父亲,而根结点必须是 黑色的:

 如上所示,红色结点一定有 父亲。也就是说,出现上述情况的话,grandfather 结点是肯定存在的。

 红黑树的插入,修改过程

 我们上述讨论了,插入的结点必须是红色的结点,插入红色的结点的话,就会影响到父亲(新插入结点的路径)。

 具体例子理解

所以,此时我们要从该路径上进行修改。

 此时违法的规则就是 B 和 22 两个红色结点连在一起了,所以我们就想着把 22 边黑,但是变黑的话,就会影响这棵树当中的黑节点个数。

所以,在更新完 25 这颗子树之后,可能会像 AVL 树当中的平衡因子一样往上更新的。

如上述图,单次对于子树的修改:
需要先找到 新插入结点 父亲的 亲兄弟(uncle)。

如果 uncle 存在且为 红,那么就把 parent 和 uncle 都变 黑:

 但是此时我们发现,变黑之后,对于 22 和 27 这两条路径来说,黑色结点个数相比于其他路径,在增多了一个,所以,此时我们在让 grandfather 结点变红,就可以解决黑色结点个数问题:

 但是,对于 grandfather 结点不一定都是这种的处理方法:

  • 如果 grandfather 结点没有父亲,说明此时 grandfather 结点是 整棵树的根结点,那么把 grandfather 结点变红就行。
  • 如果 grandfather 有父亲,grandfather 是 黑色,就结束了。
  • 如果 grandfather 有父亲,grandfather 是 红色,有和上述问题一样了,需要找 uncle。

 我们继续来看上述例子:
 

 在上述修改之后, 发现 17 和 25 又是两个红色结点链接在一起了,此时就要对这个链接关系进行修改,此时就好比是 新插入了 25 结点。

找 uncle(8),uncle 为红,就把 parent 和 uncle 一起改为黑,此时的 grandfather 没有父亲结点了,就不用再变黑了:

 如果按照上述的过程来修改的话,我们发现,相当于是在每一条路径当中都增加了一个 黑色结点。

黑色结点其实是有好处的,比如上述的例子的那种,我们插入的新结点一定是 红色的,那么插入在黑色结点后面就不用像上述一样进行修改,如上图所示,我们只有在 6 后面和在 B 后面插入结点才会像上述一样进行修改。

 上述是 有uncle的情况,如果 parent 没有亲兄弟,也就是 cur 没有 uncle的话,应该怎么办呢?

 如上述所示,cur 没有 uncle,我们不能直接把 parent 变黑,因为会多出一个 黑色结点;有人又说,多出来一个黑色结点的话,把 grandfather 在变 红不就行了,但是 如果把 grandfather 变红的话,指向 uncle 的路径上就少一个 黑色结点了

而且我们发现,如果在 cur 位置插入的话,13 的右子树的上的路径长度已经比 左子树 上的路径长度多出 2 倍了。

所以,不管是否 多出 2 倍,在没有 uncle的情况下,我们应该进行旋转来调整位置

判断旋转方式还是和在 AVL树当中判断方式一样,旋转方式请看一下博客:

C++ - AVL 树 介绍 和 实现 (上篇)_chihiro1122的博客-CSDN博客

 像上述,就是发生了的右单旋,但是旋转之后发现,颜色上还是 违反了规则。

所以此时要变色。

所以,当uncle 不存在的时候,处理规则就是 旋转 + 变色。 

 uncle存在且为红的情况,在修改之后也有可能会出现 最长路径和最短路径 长度超过两倍的情况(uncle 为黑的情况):
 

 修改:

 此时我们发现,如果我们单纯的把 17 变黑,已经不能解决问题了,此时也是需要旋转的。

13 的右子树已经明显的高了,要对右子树进行旋转,此时发生左单旋。如果是下述情况就是 双旋:
 

 cur , parent , grandfather 构成折线,就要发生双旋。

但是,在 AVL 树当中我们判断旋转的方式是利用 平衡因子的方式来判断的,但是在红黑树当中没有平衡因子,我们需要判断  cur , parent , grandfather 三者的链接关系来 确认要哪一种旋转。

 我没发现,红黑树优势不仅仅在于旋转变少了,他是在修改过程当中就会把很多结点变黑,那么在以后插入当中,插入到黑节点后面就不必再进行修改了。

 小总结

红黑树的插入,关键看 uncle。

  • 如果 uncle 存在且为红,变色+ 往上进行处理
  • 如果 uncle存在且为黑,旋转+变色+往上进行处理
  • 如果 uncle不存在,旋转 + 变色 + 往上进行处理

 抽象图理解 红黑树插入过程

 按照上述说明,出现需要修改的情况,都是 红红黑的结构,也就是 cur 为红, parent 为红,grandfather 为黑的情况,而修改过程当中,判别不用的修改方式,是看 uncle 的。

我们先来总结一下 解决方案

  • 情况一: cur为红,p为红,g为黑,u存在且为红。                                                                                                                                                                                                                                       解决方式:将p,u改为黑,g改为红,然后把g当成cur,继续向上调整。                                  
  • 情况二: cur为红,p为红,g为黑,u不存在/u存在且为黑。                                                                                                                                                                                                                         解决方法:p为g的左孩子,cur为p的左孩子,则进行右单旋转;                                                               p为g的右孩子,cur为p的右孩子,则进行左单旋转。                                                               p为g的左孩子,cur为p的右孩子,则针对p做左单旋转;                                                           p为g的右孩子,cur为p的左孩子,则针对p做右单旋转

具体情况分析:

情况一:

 比如上述是 的抽象图,他可能是 一颗子树,也可能是 整棵树。

那么,当我们在 a ,b 后面插入的话,可能就会应发 情况一

当 c/d/e 子树每条路径当中只有 一个 黑色结点

 如上述情况我们就要使用 情况一的处理方式了。

 对于情况一,因为不旋转,对于新结点的插入位置,是不管的,主要是 在 a 和 b 的四个位置插入都是引发情况一,或者不引发。

至于 cur 可能本来就是 grandfather 的黑色结点,只是在 a,b当中没有个进行修改,把 grandfather 修改为了 红色。

当然,还有 cur 本来就是 新增结点的情况,也就是 a  和 b 两个子树都是 空:

 但是处理方式还是情况一的方式。


 当 c/d/e 子树每条路径当中有两个黑色结点:

 

 c/d/e 子树每条路径当中有两个黑色结点的情况太多了,上图当中只是简单列出一些。

在 上述 只有一个黑结点的路径当中,只用一层就修改,就修改到了 cur 结点;而在当前 路径当中有两个黑结点,就是修改两层,才修改到 cur 结点的颜色的。(这里的层,一层就是 一个 cur parent grandfather 组合子树)

 反正就是 路径当中有两个黑色结点的情况,要比 只有一个黑结点的情况复杂得多,但是最终还是属于情况一,都是使用 情况一的方式来进行解决。

 情况二:

 情况二当中的uncle有两种情况:
一种是 uncle不存在:

 如果 uncle 不存在,那么 cur 一定是新插入的结点,因为如果 cur 不是新插入结点,那么 cur 和 p 当中一定至少有一个是 黑色的。

 另一种是 uncle 的颜色是 黑色:

 如果 uncle 是黑色的话,cur 这个结点一定不是 新插入的结点,因为,假设在插入之前,也就相当于是没有 cur 这个结点,g - > p -> null 路径 和  g -> u -> ········ 路径 两个路径上 黑色结点个数不一样。所以,插入之前的树不可能是红黑树。

所以,uncle 为黑的这种情况,一定是  cur 当中的子树发生了修改,往上修改的时候影响到了 cur :

 修改过程如下:都是旋转 + 变色的方式。只不过旋转当中有四种方式,具体要看 cur parent grandfather 三者之间的链接关系

  像上述这种情况, c 子树的路径当中每条之后一个黑结点,d 和 e 可能为空,也可能有一个 红结点。

 同样的,c  d  e 三个子树当中的黑色结点可以按照上述的规律增加,那么就是不同的具体例子。

比如,c是 两个黑色结点的红黑树,那么 d 和 e 就是一个结点的红黑树。

 红黑树的结果是无穷无尽的,每一种结构也相对复杂,但是上述都是属于 情况二,解决方式都是一样的。

  • 情况二当中,旋转+变色之后,就不需要网上进行处理了,为在旋转+变色之后,这棵子树的根结点一定是 黑色的,不会是 红色的,那么黑色的结点不管和 红色的结点还是和 黑色的结点链接都是不会出现问题的。
  • 在情况一当中之所以要继续往上更新,是因为,情况一修改出来的根结点是红色的。

 红黑树的验证

 红黑树的检测分为两步:

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

 检测是否满足红黑树性质:
 


	bool CheckColor(Node* root, int blacknum, int benchamark)
	{
		// 当走到叶子结点的 null 指针处,也就是 NIL结点处
		if (root == nullptr)
		{
			// 如果计算出的路径黑色结点长度 和 外部计算的不一样
			// 说明不是红黑树
			if (blacknum != benchamark)
			{
				cout << "路径黑色结点个数不一样" << endl;
				return false;
			}
			return true;
		}

		// 用于递归计算 路径的黑色结点个数
		if (root->_color == BLACK)
			blacknum++;

		// 如果当前结点为 红色,且当前结点的父亲也是红色,就不是红黑树
		if (root->_parent && root->_parent->_color == RED && root->_color == RED)
		{
			cout << "有连续红色" << endl;
			return false;
		}

		// 左右子树递归
		return CheckColor(root->_left, blacknum, benchamark)
			&& CheckColor(root->_right , blacknum, benchamark);
	}

	// 外部调用接口
	bool isBalance()
	{
		return isBalance(_root);
	}

	// 内部封装函数
	bool isBalance(Node* root)
	{
		if (root == nullptr)
			return true;

		// 如果整棵树的 根结点不是 黑色的就不是红黑树
		if (root->_color != BLACK)
		{
			cout << "根结点不是黑色" << endl;
			return false;
		}

		// 基准值
		// 在递归外部计算出左路第一条路径的 黑色结点值
		int benchmark = 0;
		Node* cur = root;
		while(cur)
		{
			if (cur->_color == BLACK)
				benchmark++;
			cur = cur->_left;
		}

		return CheckColor(root, 0, benchmark);
	}

我们使用两个函数相互调用来实现,外层函数判断整棵树的根结点是不是 黑色的;

而且计算左边第一条路径的黑色结点个数。其实可以随便找一条路径,因为这个只是一个基准值,不需要是正确的,因为红黑树当中的每一条路径都需要黑色结点数相同。

然后用内层函数递归遍历红黑树。

内层函数当中,计算出每一条黑色结点的个数;判断是否有连续的红色结点;

检测其是否满足二叉搜索树,就直接中序遍历打印就行了,这里就不实现了。

 红黑树的删除

红黑树的删除和 AVL树一样,都比插入要复杂,可以看下面这个博客来了解:
红黑树 - _Never_ - 博客园 (cnblogs.com)

完整代码实现

 

#pragma once

// 节点的颜色
enum Color { RED, BLACK };

// 红黑树节点的定义
template<class K, class V>
struct RBTreeNode
{
	RBTreeNode(pair<K ,V> kv  , Color color = RED)
		: _left(nullptr), _right(nullptr), _parent(nullptr)
		, _kv(kv), _color(color)
	{}
	RBTreeNode<K, V>* _left; // 节点的左孩子
	RBTreeNode<K, V>* _right; // 节点的右孩子
	RBTreeNode<K, V>* _parent; // 节点的双亲(红黑树需要旋转,为了实现简单给出该字段)
	pair<K, V> _kv; // 节点的值域
	Color _color; // 节点的颜色
};

template<class K ,class V>
class RBTree
{
	typedef RBTreeNode<K,V> Node;
public:
	bool insert(pair<K , V> kv)
	{
		// 搜索二叉树的插入逻辑
		// 
		// 如果当前树为空,直接用头指针只想新结点
		if (_root == nullptr)
		{
			_root = new Node(kv);
			_root->_color = BLACK;
			return true;
		}

		// 不为空接着走
		Node* cur = _root;    // 用于首次插入时候指针的迭代
		Node* parent = nullptr;

		while (cur)
		{
			// 如果当前新插入的 key 值比 当前遍历的结点 key 值大
			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;
			}
		}

		// 此时已经找到 应该插入的位置
		cur = new Node(kv);
		cur->_color = RED;
		// 再次判断大小,判断 cur应该插入到 parent 的那一边
		if (parent->_kv.first < kv.first)
		{
			parent->_right = cur;
		}
		else
		{
			parent->_left = cur;
		}

		// 链接 新插入结点 cur 的_parent 指针
		cur->_parent = parent;

		// 红黑树调整高度(平衡高度)的逻辑
		while (parent && parent->_color == RED)
		{
			// parent 为 红,parent->_parent 一定不为空
			Node* grandfather = parent->_parent;
			// 如果父亲是在 祖父的左
			if (parent == grandfather->_left)
			{
				Node* uncle = grandfather->_right;

				// 叔叔存在且为红
				if (uncle && uncle->_color == RED)
				{
					// 变色
					parent->_color = uncle->_color = BLACK;
					grandfather->_color = RED;

					// 向上迭代
					cur = grandfather;
					parent = cur->_parent;
				}
				// uncle 不存在 或者 存在且为黑
				else
				{
					if (cur == parent->_left)
					{
						//       g
						//     p
						//  c
						RotateR(grandfather);
						parent->_color = BLACK;
						grandfather->_color = RED;
					}
					else  // cur == parent->_right
					{
						//       g
						//     p
						//         c
						RotateL(parent);
						RotateR(grandfather);

						cur->_color = BLACK;
						grandfather->_color = RED;
					}

					break; // 不需要再往上更新
				}
				
			}

			else  // parent = grandfather->_right
			{
				Node* uncle = grandfather->_left;
				// uncle 存在 且 uncle 的颜色是红色
				if (uncle && uncle->_color == RED)
				{
					parent->_color = uncle->_color = BLACK;
					grandfather->_color = RED;

					cur = grandfather;
					parent = cur->_parent;
				}
				else // 不存在 或者 存在且为黑色
				{
					if (cur == parent->_right)
					{
						//  g
						//     p
						//        c
						RotateL(grandfather);
						parent->_color = BLACK;
						grandfather->_color = RED;
					}
					else // cur = parent->_left
					{
						//   g
						//     p
						//  c
						RotateR(parent);
						RotateL(grandfather);

						cur->_color = BLACK;
						grandfather->_color = RED;
					}

					break; // 不用再往上更新
				}
			}
		}

		// 不管上述如何修改,红黑树的根结点永远是黑的
		// 所以我们这里既直接硬性处理
		_root->_color = BLACK;
		return true;

	}

	void RotateL(Node* parent)
	{
		Node* cur = parent->_right; // 存储 parent 的右孩子
		Node* curleft = cur->_left; // 存储 cur 的左孩子

		parent->_right = curleft;
		if (curleft)                // 判断 cur 的左孩子是否为空
		{
			curleft->_parent = parent;  // 不为空就 修改 cur 的左孩子的_parent 指针
		}

		cur->_left = parent;
		// 留存一份 根结点指针
		Node* ppnode = parent->_parent;

		parent->_parent = cur;

		// 如果parent 是根结点
		if (parent == _root)
		{
			_root = cur;
			cur->_parent = nullptr;
		}
		else
		{
			if (ppnode->_left == parent)
			{
				ppnode->_left = cur;
			}
			else
			{
				ppnode->_right = cur;

			}

			cur->_parent = ppnode;
		}
	}

	void RotateR(Node* parent)
	{
		Node* cur = parent->_left;
		Node* curRight = cur->_right;

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

		cur->_right = parent;
		Node* ppnode = parent->_parent;
		parent->_parent = cur;

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

			cur->_parent = ppnode;
		}
	}

	bool CheckColor(Node* root, int blacknum, int benchamark)
	{
		// 当走到叶子结点的 null 指针处,也就是 NIL结点处
		if (root == nullptr)
		{
			// 如果计算出的路径黑色结点长度 和 外部计算的不一样
			// 说明不是红黑树
			if (blacknum != benchamark)
			{
				cout << "路径黑色结点个数不一样" << endl;
				return false;
			}
			return true;
		}

		// 用于递归计算 路径的黑色结点个数
		if (root->_color == BLACK)
			blacknum++;

		// 如果当前结点为 红色,且当前结点的父亲也是红色,就不是红黑树
		if (root->_parent && root->_parent->_color == RED && root->_color == RED)
		{
			cout << "有连续红色" << endl;
			return false;
		}

		// 左右子树递归
		return CheckColor(root->_left, blacknum, benchamark)
			&& CheckColor(root->_right , blacknum, benchamark);
	}

	// 外部调用接口
	bool isBalance()
	{
		return isBalance(_root);
	}

	// 内部封装函数
	bool isBalance(Node* root)
	{
		if (root == nullptr)
			return true;

		// 如果整棵树的 根结点不是 黑色的就不是红黑树
		if (root->_color != BLACK)
		{
			cout << "根结点不是黑色" << endl;
			return false;
		}

		// 基准值
		// 在递归外部计算出左路第一条路径的 黑色结点值
		int benchmark = 0;
		Node* cur = root;
		while(cur)
		{
			if (cur->_color == BLACK)
				benchmark++;
			cur = cur->_left;
		}

		return CheckColor(root, 0, benchmark);
	}
private:
	Node* _root = nullptr;
};

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

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

相关文章

3、靶场——Pinkys-Place v3(3)

文章目录 一、获取flag41.1 关于SUID提权1.2 通过端口转发获取setuid文件1.3 运行pinksecd文件1.4 利用nm对文件进行分析1.5 构建payload1.6 Fire 二、获取flag52.1 生成ssh公钥2.2 免密登录ssh2.3 以pinksecmanagement的身份进行信息收集2.4 测试程序/usr/local/bin/PSMCCLI2.…

Vue的详细教程--Vue路由与nodejs

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于Vue的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一.Vue路由是什么 二.使用Vue路由的步骤 1、…

【无标题】显示TIFF格式文件

显示TIF文件 运行结果 package src;import com.sun.media.jai.codec.*;import com.sun.media.jai.codec.FileSeekableStream; import com.sun.media.jai.codec.ImageDecoder; import com.sun.media.jai.codec.ImageCodec; import com.sun.media.jai.codec.TIFFEncodeParam; imp…

2010-2017年WIND分省政府性债务余额面板数据

2010-2017年WIND分省政府性债务余额面板数据 1、时间&#xff1a;2010-2017年 2、指标&#xff1a;债务余额 3、范围&#xff1a;30个省 4、来源&#xff1a;wind 5、指标解释&#xff1a;地方政府债务分为一般债务和专项债务。 一般债务对应的是一般公共预算&#xff0c…

操作系统权限提升(三十)之数据库提权-SQL Server sp_oacreate+sp_oamethod(dba权限)提权

SQL Server sp_oacreate+sp_oamethod(dba权限)提权 sp_oacreate+sp_oamethod介绍 在xp_cmdshell被删除或不能利用是可以考虑利用sp_oacreate,利用前提需要sqlserver sysadmin账户服务器权限为system(sqlserver2019默认被降权为mssql)。sp_oacreate 是一个存储过程,可以…

Kubernetes 部署 nfs-subdir-external-provisioner

概述 官方GitHub及参考文档:GitHub - kubernetes-sigs/nfs-subdir-external-provisioner: Dynamic sub-dir volume provisioner on a remote NFS server. 部署nfs-subdir-external-provisioner提供StorageClass服务 步骤 nfs 服务器准备 /etc/exports # cat /etc/exports…

数据链路层--以太网

文章目录 以太网1. 以太网帧格式2. mac地址与IP地址 代表协议:以太网. 以太网 以太网" 不是一种具体的网络&#xff0c;而是一种技术标准&#xff1b;既包含了数据链路层的内容&#xff0c;也包含了一些物理层的内容。例如&#xff1a;规定了网络拓扑结构&#xff0c;访…

laravel框架 - 消息队列如何使用

业务场景&#xff1a;项目里边有很多视频资源需要上传到抖音资源库&#xff0c;通过队列一条一条上传。 参考实例&#xff1a;发送邮件&#xff0c;仅供参考 (1)创建任务【生成任务类】 在你的应用程序中&#xff0c;队列的任务类都默认放在 app/Jobs 目录下。如果这个目录不存…

一款好用的汇编学习工具【compile explore在线编译调试】

登录网址&#xff1a;Compiler Explorer 然后编写代码如下&#xff1a;可以看到&#xff0c;最左边是源代码&#xff0c;中间是汇编&#xff0c;可以选择编程语言和编译链工具&#xff0c;最右边是打印的输出结果&#xff0c;对于汇编指令可右键会弹出汇编指令的解释说明。

十四、ADDA数模转换

十四、AD&DA转换 介绍XTP2046介绍模块代码 模数转换数模转换 介绍 AD&#xff1a;模数转换&#xff0c;将模拟信号转换为计算机可操作的数字信号DA&#xff1a;数模转换&#xff0c;将数字信号转换为模拟信号 XTP2046 介绍 时序 模块代码 #define XPT2046_VBAT 0xAC /…

大数据学习1.0-Centos8虚拟机安装

1.创建新的虚拟机 2.选择稍后安装OS 3.选择Linux的CentOS8 4.选择安装路径 5.分配20g存储空间 6.自定义硬件 7.分配2g内存 8.分配2核处理器 9.选择镜像位置 10.开启虚拟机安装 推荐密码设置为root

全国职业技能大赛云计算--高职组赛题卷②(容器云)

全国职业技能大赛云计算--高职组赛题卷②&#xff08;容器云&#xff09; 第二场次题目&#xff1a;容器云平台部署与运维任务1 Docker CE及私有仓库安装任务&#xff08;5分&#xff09;任务2 基于容器的web应用系统部署任务&#xff08;15分&#xff09;任务3 基于容器的持续…

洛谷刷题入门篇:分支结构

今天又来了&#xff0c;刷题刷题&#xff0c;我爱刷题&#xff0c;题单链接如下&#xff1a; https://www.luogu.com.cn/training/101#problems 一、【深基1-2】小学数学 N 合一 题目如下&#xff1a;https://www.luogu.com.cn/problem/P2433 题目描述 问题 1 请输出 I lov…

[Linux入门]---git命令行的基本使用

文章目录 1.git使用gitee仓库创建git使用测试ignore文件 1.git使用 git是一款对文件进行版本控制的软件&#xff0c;gitee、github是基于git软件搭建的网站&#xff0c;是可以对代码进行托管的平台&#xff1b;github是国外的网站&#xff0c;访问慢&#xff0c;不稳定&#xf…

Linux学习之Redis使用

搭建Redis服务器 在主机redis64运行redis服务 #安装redis服务 [rootredis64 ~]# yum install -y redis # 启动redis服务并开机启动 [rootredis64 ~]# systemctl enable redis --now # 查看redis端口 [rootredis64 ~]# ss -tnlp | grep redis-server LISTEN 0 128 …

异步通讯技术之RabbitMQ

前言: 📕作者简介:热爱编程的小七,致力于C、Java、Python等多编程语言,热爱编程和长板的运动少年! 📘相关专栏Java基础语法,JavaEE初阶,数据库,数据结构和算法系列等,大家有兴趣的可以看一看。 😇😇😇有兴趣的话关注博主一起学习,一起进步吧! 一、初识MQ …

算法通过村第八关-树(深度优先)黄金笔记|寻找祖先

文章目录 前言最近公共祖先问题总结 前言 提示&#xff1a;生活就是一场有很多规则&#xff0c;却没有裁判的比赛。 --约瑟夫布罗茨基《悲伤与理智》 最近公共祖先问题 参考题目地址&#xff1a;236. 二叉树的最近公共祖先 - 力扣&#xff08;LeetCode&#xff09; 如果将搜索…

金属铬 铬含量的测定 硫酸亚铁铵滴定法

声明 本文是学习GB-T 4702.1-2016 金属铬 铬含量的测定 硫酸亚铁铵滴定法. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 GB/T4702 的本部分规定了硫酸亚铁铵滴定法测定金属铬中铬含量。 本部分适用于金属铬(钒≤0.20%)中铬含量的测定&…

【小沐学Python】网络爬虫之urllib

文章目录 1、简介2、功能介绍2.1 urllib库和requests库2.2 urllib库的模块2.2.1 urllib.request2.2.2 urllib.error2.2.3 urllib.parse2.2.4 urllib.robotparser 2.3 入门示例 3、代码示例3.1 urlib 获取网页(1)3.2 urlib 获取网页(2) with header3.3 urllib post请求 4、urlli…

LeetCode【1. 两数之和】

穷通有命无须卜&#xff0c;富贵何时乃济贫&#xff1b;角逐名场今已久&#xff0c;依然一幅旧儒巾。 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个 整数&#xff0c;并返回它们的数组下标。 你可以假设每种输…