C++ 红黑树(更新中)

news2024/11/20 6:17:49

前言

上篇博客学习了平衡二叉搜索树(AVLTree),了解到AVL树的性质,二叉搜索树因为其独特的结构,查找、插入和删除在平均和最坏情况下都是O(logn)。AVL树的效率就是高在这个地方。
但是在AVL树中插入或者删除结点,使得高度差的绝对值大于1。此时,AVL树的平衡状态就被破坏,它就不再是一棵平衡二叉树;为了让它重新维持在一个平衡状态,就需要对其进行旋转处理,但是因为每个结点的高度差的绝对值都要小于1,这个条件较为的严格,所以导致多数情况的插入和删除都需要旋转调整,导致插入和删除的效率降低。
这时红黑树应运而生,并且因为其接近平衡的结构,使其查找效率也很高效,同时因为没有AVL树那样的严格要求,所以其插入和删除效率有时还高于AVL树,使其综合性能高于AVL树,所以红黑树的应用十分的广泛,比如在 Java 的集合框架 (HashMap、TreeMap、TreeSet),C++ 的 STL中都有以红黑树为底层结构实现的容器
那红黑树到底是什么呢?又是如何实现的呢?

在这里插入图片描述

文章目录

  • 前言
  • 一. 什么是红黑树
  • 二. 红黑树的效率
  • 三. 红黑树的构建
    • (1). 结点结构体
    • (2). 结点的插入
      • 1. 情况一
      • 2. 情况二
      • 3. 情况三
      • 4. 情况四
      • 5. 小总结
      • 6. 代码实现
    • (3). 查找
    • (4). 红黑树的销毁
    • (5). 测试
  • 四. 迭代器
    • (1). 迭代器的结构体
    • (2). 迭代器的访问
      • 运算符重载
  • 结束语

一. 什么是红黑树

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

AVL树是依靠每个结点的左右高度差小于1来完成整棵树的平衡,而不会出现歪脖子树的场景
但是这样的要求较为的严格,导致插入和删除效率降低,而红黑树保持接近平衡是依靠以下几个规则:

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

从3规则我们可以推导出,红黑树中没有连续的红色结点
同时,红黑树中,最短路径最少是全是黑色结点的路径,而最长路径最多就是红黑相间的结点
而每个路径的黑色结点个数相同,最长路径最多是黑红相间,所以最长路径最多是 黑+红=2黑

二. 红黑树的效率

红黑树的最短路径:全黑
红黑树的最长路径:一黑一红,红黑相间

假设总共有N个结点
那么最短路径就是以2为底的logN
最长路径就是2logN
所以红黑树的效率最差就是2logN
而AVL树的查找效率是logN,二者的效率仅是2倍
因为以2为底的logN已经是不大的数了,所以2倍差距并不大。
但是红黑树的调整没有AVL树频繁,所以综合效率红黑树更胜一筹

在这里插入图片描述
像这样一棵树,如果是AVL树,则右边高度比左边高2,需要旋转,但是符合红黑树的条件,不用旋转

三. 红黑树的构建

(1). 结点结构体

红黑树结点的结构体大致与AVL树相同,但不需要平衡因子,而需要一个标记颜色的存储位
我们可以使用枚举体定义这个颜色的存储位。
代码如下:

//颜色的枚举体
enum Colour
{
	RED,
	BLACK,
};

//三叉链
//结点的结构体
template<class K, class V>
struct RBTreeNode
{
	RBTreeNode<K, V>*_left;//左指针
	RBTreeNode<K, V>*_right;//右指针
	RBTreeNode<K, V>*_parent;//双亲指针
	Colour _col;//颜色标记位
	pair<K, V>_kv;//KV值

	//构造函数
	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;
private:
	//根节点
	Node*_root = nullptr;
};

(2). 结点的插入

结点的插入,首先我们面临的一个问题是,默认插入的结点为什么是红色的呢?
新插入的结点是红色还是黑色,本质是要违反规则3,还是规则4
如果新插入的结点为黑色的话,一定改变路径上的黑色结点的个数,因为一次只能插入一个结点;但是如果插入红色结点,如果父亲节点是黑色,那插入就没有问题,如果为红色,那也只是影响最多两条路径,所以新增结点默认为红色。

而红黑树也是二叉搜索树,所以最开始时,结点的插入和二叉搜索树一样,都是通过kv值找到应插入的位置
在这里插入图片描述


那么接下来,我们就来分析结点插入的几种可能

1. 情况一

在这里插入图片描述
cur是新增结点,因为parent是黑色,所以插入结束


2. 情况二

在这里插入图片描述

结点15是新增结点,但是parent是红色结点,违反了规则3,出现了连续的红色结点,所以我们需要调整,又因为规则4,每个路径的黑色结点的个数相同,所以我们可能会修改隔壁路径。所以我们将这几个结点命名一下
调整的方式是这样的:
为了同时满足3,4规则,我们要改变结点颜色的同时,路径上的黑色结点个数还要相同,那么其实就是一直改变颜色,并且往上更新,最后到达两个路径的公共结点,然后在公共结点更改颜色,则不会影响这两个路径。
具体方法:如果存在uncle结点,且uncle结点为红色,则将parent结点和unclude结点都变成黑色,然后让grandfather结点变成红色
在这里插入图片描述

然后因为grandfather当前为根结点,所以再将grandfather结点变成黑色

在这里插入图片描述

我们发现,改变后的红黑树依然满足5条规则

如果结点数更多一点呢?
比如这样一棵红黑树
在这里插入图片描述
28是新增的结点
我们同样使用上述方法调节颜色
在这里插入图片描述
可以看到以25为根结点的子树完成了调整
但是这只是完成了一颗子树,所以还需要继续向上调整
所以我们让 cur=grandfather ,parent=cur->parent
在这里插入图片描述
这时parent有三种情况:

  1. 为整棵树的根结点。调整结束
  2. parent结点为黑色。调整结束
  3. parent结点为红色,继续调整

此处parent结点是红色,所以我们还需继续调整
在这里插入图片描述
最后因为根节点需要是黑色的,我们再将grandfather变成黑色

在这里插入图片描述
这样就完成了调整

这就是情况二:

插入结点的父亲结点是红色的,uncle结点存在且为红
则将parent和uncle都变黑grandfather变红,同时还需要继续向上更新


3. 情况三

在这里插入图片描述

当我们在6的右边新增结点
此时没有uncle结点,不满足情况二。

我们将grandfather这棵子树拎出来
在这里插入图片描述
我们会发现,这棵树的高度很不均衡,我们可以采用旋转的方式,降低这棵树的高度
图中这棵树的右边高度较高,所以我们要采用左单旋的方式

先将grandfather -> right = parent -> left,将parent的左结点给grandfather的右
再 parent -> left = grandfather,将grandfather给parent的左

在这里插入图片描述
这样子,这棵子树的高度就被我们降低了,并且没有破坏二叉搜索树的结构
但是还不满足红黑树,所以我们还需要调色
将parent变黑,grandfather变红
在这里插入图片描述

然后整棵树也就符合规则
在这里插入图片描述


单旋即后续的双旋,更详细的讲解可以阅读【C++STL】AVL树
简单来说
当出现这样的情况时,线性使用单旋
在这里插入图片描述
前者左边较高,使用右单旋平衡高度
后着右边较高,使用左单旋平衡高度

出现以下情况,折线型使用双旋
在这里插入图片描述

前者,左右双旋:先以拐点(红点)为轴点,先进行左单旋,变成左边高,再以黑点为轴点进行右单旋
后者,右左双旋:先以拐点(红点)为轴点,先进行右单旋,变成右边高,再以黑点为轴点进行左单旋
在这里插入图片描述
这里只作大致讲解,详细讲解可以阅读上面提到的博客


红黑树右单旋的情况和左单旋类似,读者可自己先尝试一下
在这里插入图片描述

旋转过程如下:
在这里插入图片描述

然后再调色
在这里插入图片描述


4. 情况四

情况二是uncle结点是红色,情况三是不存在uncle结点
情况四则是uncle结点为黑色
首先,什么时候会出现uncle结点为黑色
比如这样一棵红黑树
在这里插入图片描述
我们在4的左结点新增结点
那么首先看到parent和uncle都是红色,这是情况二
我们将parent和uncle变成黑色,grandfather变成红色
在这里插入图片描述

grandfather这棵子树完成调色,但是还需要继续向上调色
在这里插入图片描述

这时发现parent是黑色,插入成功,本次插入就结束了
我们在再在11的右子树新增结点
在这里插入图片描述

同样调整颜色,然后继续向上调整
在这里插入图片描述

而此时我们发现parent是红色,但是uncle却是黑色。
因为在之前的插入,uncle位置从红色变成了黑色,而parent在之后的一次插入中是grandfather,变成了红色
导致两个兄弟节点的颜色不同。
并且此时处于折线型
在这里插入图片描述
则我们需要使用双旋

这就是我们的情况四:parent为红,uncle存在且为黑色


双旋的过程如下:
在这里插入图片描述
我们先以parent为轴点,进行左单旋
在这里插入图片描述
先将parent->right=cur->left; 将cur的左给parent的右
在这里插入图片描述

再将cur->left=parent; 将parent变成cur的左

第一步旋转变成这样
在这里插入图片描述

第二步旋转,再以grandfather为轴点,进行一次右单旋
在这里插入图片描述
先将grandfather->left=cur->right;将cur的右给grandfather的左

在这里插入图片描述
再将cur->right=grandfather;将grandfather变成cur的右

最后将cur变成黑色,grandfather变成红色
在这里插入图片描述
这样,红黑树的左右双旋就完成了

5. 小总结

上述的四种情况,对应的是代码中的分类条件及旋转后的调色方法
而单旋或双旋的使用情况,是依照树的结构:线性,则使用单旋折线型,则使用双旋

6. 代码实现

	//插入
	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->_right;
			}
			else if(cur->_kv.first>kv.first)
			{
				//往左边走
				parent = cur;
				cur = cur->_left;
			}
			else
			{
				//相等则插入失败
				return false;
			}
		}
		cur = new Node(kv);
		//链接
		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;
				//uncle结点存在且为红
				//调色,并继续向上调整
				if (uncle&&uncle->_col == RED)
				{
					//调色
					parent->_col = BLACK;
					uncle->_col = BLACK;
					grandfather->_col = RED;
					//继续向上调色
					cur = grandfather;
					parent = cur->_parent;
				}
				else//unlce不存在,或者uncle存在且为黑
				{

					//     g
					//   p
					// c
					//右单旋
					if (cur == parent->_left)
					{
						//以parent为轴点,进行右单旋
						RotateR(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
						break;
					}
					else
					{
						//     g
						//   p   u
						//     c
						//左右双旋
						RotateL(parent);
						RotateR(grandfather);
						cur->_col = BLACK;
						grandfather->_col=RED;
					}

					break;
				}
			}
			else  //parent是grandfather的右边
			{
				Node*uncle = grandfather->_left;
				//uncle结点存在且为红
				//调色,并继续向上调整
				if (uncle&&uncle->_col == RED)
				{
					//调色
					parent->_col = BLACK;
					uncle->_col = BLACK;
					grandfather->_col = RED;
					//继续向上调色
					cur = grandfather;
					parent = cur->_parent;
				}
				else//unlce不存在,或者uncle存在且为黑
				{

					//     g
					//   u   p
					//         c
					//左单旋
					if (cur == parent->_right)
					{
						//以parent为轴点,进行左单旋
						RotateL(grandfather);
						parent->_col = BLACK;
						grandfather->_col = RED;
						break;
					}
					else
					{
						//     g
						//   u   p
						//     c
						//右左双旋
						RotateR(parent);
						RotateL(grandfather);
						cur->_col = BLACK;
						grandfather->_col = RED;
					}

				}
			}
		}

		//将根的颜色变成黑色
		_root->_col = BLACK;
	}


	//左单旋
	void RotateL(Node*parent)
	{
		Node*subR = parent->_right;
		Node*subRL = subR->_left;

		//1. 将subRL变成parent的右节点
		parent->_right = subRL;
		if (subRL)
			subRL->_parent = parent;


		//记录当前子树的父节点
		Node*ppnode = parent->_parent;

		//2. 将parent变成subR的左节点
		subR->_left = parent;
		parent->_parent = subR;

		//3. 链接ppnode
		if (ppnode == nullptr)
		{
			//如果是ppnode是空,代表parent是根节点

			//更新根
			_root = subR;
			_root->_parent = nullptr;
		}
		else
		{
			if (ppnode->_left == parent)
			{
				ppnode->_left = subR;
			}
			else
			{
				ppnode->_right = subR;
			}
			subR->_parent = ppnode;
		}

	}


	//右单旋
	void RotateR(Node*parent)
	{
		Node*subL = parent->_left;
		Node*subLR = subL->_right;
		//1.将subLR变成parent的左节点
		parent->_left = subLR;
		//subLR可能是NULL,不是NULL才链接
		if (subLR)
			subLR->_parent = parent;

		//2.再将parent变成subL的右节点
		Node*ppnode = parent->_parent;//因为parent不一定是根节点,所以需要记录爷爷节点
		subL->_right = parent;
		parent->_parent = subL;

		//3.链接parent指针
		if (ppnode == nullptr)
		{
			//如果是根节点
			_root = subL;
			_root->_parent = nullptr;
		}
		else
		{
			//反之不是
			if (ppnode->_left == parent)
			{
				ppnode->_left = subL;
			}
			else
			{
				ppnode->_right = subL;
			}
			subL->_parent = ppnode;
		}

	}

(3). 查找

红黑树的查找本质就是二叉搜索树的查找,根据二叉搜索树的性质:右子树的所有结点都比当前结点的值小,左子树的所有结点都比当前结点的值大
这样我们使用循环,比较大小,就可以决定接下来要去往左子树遍历还是右子树遍历
代码如下:

//查找
	Node* Find(const K&key)
	{
		assert(_root);

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

(4). 红黑树的销毁

销毁我们可以使用析构函数,在当前函数结束时,就自动销毁红黑树,而析构函数内部其实调用一个后续遍历依次释放每个节点
代码如下:

//析构函数
	~RBTree()
	{
		_Destroy(_root);
		_root = nullptr;
	}

//销毁
	void _Destroy(Node*root)
	{
		//后续递归销毁
		if (root == nullptr)
			return;

		_Destroy(root->_left);
		_Destroy(root->_right);
		free(root);
	}

(5). 测试

红黑树的测试可以分两步
第一步检查是否满足二叉搜索树的性质:我们使用中序遍历验证
第二步根据红黑树的这三个性质:1. 根结点是黑色的 2. 每个路径的黑色结点个数相同 3. 不存在连续的红色结点 检查当前红黑树是否异常

中序遍历的代码如下:

//中序遍历接口
	void InOrder()
	{
		_InOrder(_root);
		cout << endl;
	}

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

		_InOrder(root->_left);
		cout << root->_kv.first << " ";
		_InOrder(root->_right);
	}

中序遍历较为简单,这里不作解释


检查红黑树性质代码如下:

	//检查是否满足红黑树
	bool IsBalance()
	{
		//检查黑色结点的个数
		//最左路径的黑色结点个数
		int benchMark = 0;

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

			cur = cur->_left;
		}

		return _Check(_root,0,benchMark);
	}


	//递归检查
	bool _Check(Node*root,int blackNum,int benchMark)
	{
		//检查三个条件
		//1.根结点是否是黑色
		//2.是否有连续的红色结点
		//3.是否每条路径的黑色结点个数相同

		if (root == nullptr)
		{
			if (blackNum == benchMark)
				return true;
			else
			{
				cout << "黑色结点个数异常" << endl;
				return false;
			}
		}

		//1.根结点如果是红色的,代表编写异常
		if (root == _root && root->_col == RED)
		{
			cout << "根结点颜色异常" << endl;
			return false;
		}

		//2.如果当前结点是红色的
		//则需要检查其父亲结点是否是红色
		if (root->_col == RED && root->_parent->_col == RED)
		{
			cout << "出现连续的红色结点" << endl;
			return false;
		}

		//如果是黑色结点,则要记录
		if (root->_col == BLACK)
		{
			blackNum++;
		}

		return _Check(root->_left, blackNum, benchMark) && 
		_Check(root->_right, blackNum, benchMark);
	}
  1. 如果根结点是红色的,异常
  2. 如果当前结点是红色的,并且父亲结点也是红色,即出现连续的红色结点,异常
    检查父亲的原因:因为一个结点,一定有父亲结点,但是不一定有孩子结点
  3. blackNum记录黑色结点的个数,我们在检查前,先遍历最左路径,计算出黑色结点个数,然后拿着这个值去每一条路径比较,如果有不一样的,要么最左路径出问题,要么该路径出问题

四. 迭代器

红黑树因其特殊结构和高效查找,插入,删除效率而应用广泛。C++STL中的map和set的底层就是使用的红黑树。所以我们要为红黑树提供访问的各种接口,迭代器就是其中最为重要的访问方式


迭代器本质就是结点的指针,但是是将结点指针和方法结合起来的结构体。
但是因为红黑树和线性表并不同,并不是连续的存储结构,结点之间是依靠指针连接的,所以迭代器的实现并不容易。那么接下来,我们就来一点点设计红黑树的迭代器

(1). 迭代器的结构体

首先,迭代器需要应用于抽象数据,所以我们需要使用模板,同时迭代器是一个结构体,所以我们需要重载一些运算符,来实现其效果
以下是迭代器结构体的代码:

//迭代器
template<class T,class Ref,class Ptr>
struct _RBTreeIterator
{
	typedef RBTreeNode<T> Node;
	typedef _RBTreeIterator<T, Ref, Ptr> Self;
public:
	//构造函数
	_RBTreeIterator(const Node*node)
		:_node(node)
	{}
private:
	Node*_node;
};

在这里插入图片描述

模板参数:
class T:红黑树结点的数据类型
class Ref:返回迭代器内部指针解引用的成员变量类型的引用
适配*运算符的重载
class Ptr:返回迭代器内部的指针
适配->运算符的重载

(2). 迭代器的访问

我们预期的迭代器的使用方式是这样的:我们拿set作例子

void test_set1()
	{
		set<int> s;
		s.Insert(1);
		s.Insert(2);
		s.Insert(3);

		set<int>::iterator it = s.begin();
		while (it != s.end())
		{
			cout << *it << " ";
			++it;
		}
		cout << endl;
		
	}

需要可以一个结点一个结点的遍历set中的元素,因为底层红黑树是树结构,中序遍历是一次性全访问完,而迭代器实现的就是像线性结构那样,一个结点一个结点的访问。

set和map的大部分功能实现,其实都是直接使用红黑树的功能


begin()
首先使用迭代器遍历,我们需要确定红黑树遍历的起始位置,因为中序遍历出来的是升序,所以我们第一个访问的结点就是最小结点。而最小结点在哪呢?其实红黑树的最左结点就是最小结点,所以我们只需要遍历到最左结点就好了。代码如下:

	//返回最左结点
	iterator begin()
	{
		Node*cur = _root;
		//可能是空树,所以要判断一下
		while (cur&&cur->_left)
			cur = cur->_left;

		return iterator(cur);
	}

需要注意的点是当前树可能是空树,所以不能一开始就解引用,可能出现空指针解引用的情况
begin()返回的位置如下
在这里插入图片描述


end()
end直接返回空就好了,因为各结点的链接关系都完整,遍历结束后,会访问到最右结点的下一个结点,也就是空结点

	iterator end()
	{
		return iterator(nullptr);
	}

接下来是几个运算符重载,因为迭代器不是内置类型,所以对于迭代器的!=*++,编译器都是无法识别的

运算符重载

!=运算符重载
因为迭代器内部就一个指针类型,所以判断是否相同,其实就是用指针比较
代码如下:

	//判断
	//Self是重命名的迭代器类型
	bool operator!=(const Self&s)
	{
		return _node != s._node;
	}

*运算符重载
因为*的作用是解引用,返回存储的数据,并且支持修改,所以我们需要使用引用返回
代码如下:

	//返回解引用的数据
	//Ref是T&或者const T&
	Ref operator*()
	{
		return _node->_data;
	}

++运算符重载
在这里插入图片描述
比如这样一棵树,begin()返回的位置如图,我们要使用++来遍历这棵树,分为以下几种情况

  1. 当前结点的右结点不为空,如果接下来要访问的就是右子树的最左节点
  2. 右结点为空,再分两类

(1). 如果当前访问的结点是父亲结点的右结点,那么父亲结点就一定访问过了,我们访问父亲结点的父亲结点,也就是祖父结点,同时也需要判断父亲结点是否是祖父结点的右结点
(2). 如果当前访问的结点是父亲结点的左结点,那么父亲结点就是下一个访问的结点

代码如下:

	//迭代器++
	Self&operator++()
	{
		//右不为空,下一个就是右子树的最左结点
		if (_node->_right)
		{
			Node*subLeft = _node->_right;
			while (subLeft->_left)
				subLeft = subLeft->_left;

			_node=subLeft;
		}
		else
		{
			//右为空,向上找,找孩子是父亲结点的左孩子
			Node*parent = _node->_parent;
			while (parent&&parent->_right == _node)
			{
				_node = parent;
				parent = parent->_parent;
			}

			_node = parent;
		}

		return *this;
	}

--运算符

--运算符的大逻辑跟++运算符相同,只不过逻辑条件不一样

  1. 如果当前结点的左结点不为空,则下一个访问的是左结点的最右结点
  2. 如果当前结点的左结点为空,则同样分为两类

(1). 结点是父亲结点的左结点,那么父亲结点已经访问过了
(2). 结点是父亲结点的右结点,那么父亲结点就是下一个访问的结点

代码如下:

	//迭代器--
	Self&operator--()
	{
		//左结点不为空,找左结点的最右结点
		if (_node->_left)
		{
			Node*subRight = _node->_left;
			while (subRight->_right)
			{
				subRight = subRight->_right;
			}
			_node = subRight;
		}
		else
		{
			//左结点为空,向上找结点是其父亲结点的右孩子
			Node*cur = _node;
			Node*parent = cur->_parent;
			//找结点是其父亲结点的右孩子
			while (parent&&cur == parent->_left)
			{
				cur = parent;
				parent = parent->_parent;
			}

			_node = parent;
		}

		return *this;
	}

结束语

本篇博客内容暂时到此,红黑树结点的删除,较为复杂,后续再继续补充

本篇内容到此就结束了,感谢你的阅读!

如果有补充或者纠正的地方,欢迎评论区补充,纠错。如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。
在这里插入图片描述

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

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

相关文章

学系统集成项目管理工程师(中项)系列22c_信息化知识(下)

1. 新一代信息技术对产业的推动 1.1. 加快建设宽带、泛在、融合、安全的信息网络基础设施&#xff0c;推动新一代移动通信、下一代互联网核心设备和智能终端的研发及产业化&#xff0c;加快推进三网融合&#xff0c;促进物联网、云计算的研发和示范应用 1.2. 大数据 1.2.1. …

申请GPT-4插件,等待GPT-4插件候补全过程

前言 GPT4相信大家都知道它的升级是带来更多惊喜的&#xff0c;目前GPT4已经推出了网页和插件功能&#xff0c;这些插件是专门为语言模型设计的工具。插件可以帮助 ChatGPT 访问最新信息、运行计算或使用第三方服务。写文记录一下&#xff0c;如果你现正好有需要GPT-4 插件的需…

基于springboot就业信息管理系统

开发技术与环境配置 以Java语言为开发工具&#xff0c;利用了当前先进的springboot框架&#xff0c;以MyEclipse10为系统开发工具&#xff0c;MySQL为后台数据库&#xff0c;开发的一个就业信息管理系统。 SpringBoot框架 SpringBoot是一个全新开源的轻量级框架。基于Spring…

chatgpt赋能Python-python3_8怎么打开

Python 3.8&#xff1a;如何下载和打开 Python 3.8是一种高级编程语言&#xff0c;被广泛应用于人工智能、数据分析和网络编程等领域。本文将介绍如何下载和打开Python 3.8以及其优点和用途。 下载Python 3.8 Python 3.8的下载地址为官方网站。根据不同操作系统的需求&#…

系统清理优化工具:CCleaner

哈喽&#xff0c;大家好。今天带各位小伙伴们学习一款系统清理优化工具——CCleaner。 CCleaner是一款系统优化和隐私保护工具。它可以用来清除Windows系统不再使用的垃圾文件&#xff0c;以腾出更多硬盘空间。也可以清除使用者的上网记录。它的体积小&#xff0c;运行速度快&…

java+springboot高校校友校园信息管理系统

本高校校友信息管理系统使用Web开发&#xff0c;运行在Internet环境之上&#xff0c;系统的后台编程语言使用JAVA&#xff0c;数据库使用MySQL。完成了两个用户角色的功能&#xff0c;管理员管理所有信息&#xff0c;前台学生用户登录后查看公告&#xff0c;在线捐赠申请&#…

docker是怎么决定容器内容存储到哪个目录的?(存储驱动决定的)(乱七八糟的)

文章目录 docker是怎么决定容器内容存储到哪个目录的&#xff1f;docker对我/var这个目录有没有什么要求&#xff0c;比如要求它的文件系统是指定的类型如果我Docker的默认存储驱动是overlay2&#xff0c;但是我/var目录的文件系统不是overlay2&#xff0c;这没影响吗&#xff…

chatgpt赋能Python-python3_7_1如何使用

Python 3.7.1使用指南 Python自从出现以来&#xff0c;已经成为了一个非常流行的编程语言&#xff0c;每年都会更新版本以满足不断变化的市场需求。其中最新版本是Python 3.7.1&#xff0c;它与其前身相比提供了许多改进和新功能&#xff0c;同时也解决了一些已知的问题。本文…

MobileNetV3详解及在pytorch下基于CIFAR10数据集的实现

1 MobileNetV3介绍 MobileNetV3 是由 google 团队在 2019 年提出的轻量化网络模型&#xff0c;传统的卷积神经网络&#xff0c;内容需求大&#xff0c;运算量大&#xff0c;无法再移动设备以及嵌入式设备上运行&#xff0c;为了解决这一问题&#xff0c;MobileNet网络应运而生。…

chatgpt赋能Python-python3_8下载numpy

Python3.8下载numpy&#xff1a;安装步骤与常见问题解决方案 Python3.8是最新版的Python编程语言&#xff0c;它提供了丰富的库和框架支持&#xff0c;包括科学计算库numpy。然而&#xff0c;有些用户可能会在安装numpy时遇到一些麻烦&#xff0c;本文将教你如何下载numpy&…

海康机器视觉工业相机客户端MVS-常用功能CCM

什么是CCM? CCM是一种功能。 CCM矩阵是通过对每一个RGB分量乘以一个校正矩阵来实现色彩校正。当图像经过白平衡处理后,图像整 体会显得比较黯淡,同时多种颜色可能存在不同程度地偏离其标准值。此时需要对图像的色彩乘以校正 矩阵来修正各颜色至其标准值,使图像的整体色彩更…

【智能算法1】模拟退火算法_Python实现

一、模拟退火算法&#xff08;SA&#xff09; 1.1 固体退火的原理 加热使得固体融化&#xff0c;然后缓慢地降低温度&#xff0c;以此来让固体内部的粒子排布更加均匀。 分为四个阶段&#xff1a; 升温阶段、降温阶段、等温阶段、达到目标温度退火完成 等温阶段就是在塑造…

chatgpt赋能Python-python3_8安装scrapy

Python3.8 安装 Scrapy 如果你是 Python 开发者&#xff0c;你可能已经听说过 Scrapy&#xff1a;一个开源框架&#xff0c;用于快速高效地抓取和提取网页数据。在本篇文章中&#xff0c;我们将介绍如何在 Python3.8 环境下安装 Scrapy&#xff0c;并解释该过程的每一个步骤。…

chatgpt赋能Python-python3_6怎么算

Python 3&6怎么算&#xff1f;—— Python版本的比较 Python是一款广泛使用的高级编程语言&#xff0c;已经有好几个版本了&#xff0c;其中比较常用的是Python 3和Python 2.7。近年来&#xff0c;Python 3越来越受欢迎&#xff0c;那么Python 3和6怎么算呢&#xff1f;本…

Doxygen源码分析: 根目录文件简要介绍

2023-05-18 22:54:02 ChrisZZ imzhuofoxmailcom Hompage https://github.com/zchrissirhcz 文章目录 1. doxygen 版本2. 文件介绍DockerfileLICENSE.dockerignore.codedocsVERSION.editorconfigLANGUAGE.HOWTOBUILD.txtINSTALL.gitignoreREADME.mdCMakeLists.txt 1. doxygen 版…

一图看懂 chardet 模块:字符编码检测器,兼容 Python2 和 Python3,资料整理+笔记(大全)

本文由 大侠(AhcaoZhu)原创&#xff0c;转载请声明。 链接: https://blog.csdn.net/Ahcao2008 一图看懂 chardet 模块&#xff1a;字符编码检测器&#xff0c;兼容 Python2 和 Python3&#xff0c;资料整理笔记&#xff08;大全&#xff09; &#x1f9ca;摘要&#x1f9ca;模块…

【1++的C++初阶】之内存管理

&#x1f44d;作者主页&#xff1a;进击的1 &#x1f929; 专栏链接&#xff1a;【1的C初阶】 文章目录 一&#xff0c;C/C的内存分布二&#xff0c;malloc&#xff0c;realloc&#xff0c;calloc的区别三&#xff0c;C的内存管理- -new和delete初识new和deletenew和delete操作…

宝塔面板webhook 使用教程

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 背景1、介绍一下Webhook2、使用步骤1.安装git2.安装WebHook3.添加WebHook4.配置git 钩子 &#xff08;码云示例&#xff09;5.私有项目还需要做以下操作 背景 最近…

C语言算法--桶排序

1-什么是桶排序法 什么是桶排序法&#xff1f;其实说白了就是把需要排列的元素分到不同的桶中&#xff0c;然后我们对这些桶里的元素进行排序的一种方式&#xff0c;然后我们在根据桶的顺序进行元素的合并。&#xff08;不过前提是要确定桶的数量以及大小&#xff09; 按照稍…

[数字图像处理]第四章 频率域滤波

文章目录 第四章 频率域滤波笔记&#xff1a;4.1 背景4.1.1 傅里叶级数和变换简史 4.2 基本概念4.2.1 复数4.2.2 傅里叶级数4.2.3 冲激及其取样特性4.2.5 卷积 4.3 取样和取样函数的傅里叶变换4.3.1 取样4.3.2 取样函数的傅里叶变换4.3.3 取样定理4.3.4 混淆4.3.5 有取样后的数…