【高阶数据结构】封装unordered_map 和 unordered_set

news2024/9/9 0:53:55

🌈欢迎来到数据结构专栏~~封装unordered_map 和 unordered_set


  • (꒪ꇴ꒪(꒪ꇴ꒪ )🐣,我是Scort
  • 目前状态:大三非科班啃C++中
  • 🌍博客主页:张小姐的猫~江湖背景
  • 快上车🚘,握好方向盘跟我有一起打天下嘞!
  • 送给自己的一句鸡汤🤔:
  • 🔥真正的大师永远怀着一颗学徒的心
  • 作者水平很有限,如果发现错误,可在评论区指正,感谢🙏
  • 🎉🎉欢迎持续关注!
    在这里插入图片描述

请添加图片描述

文章目录

  • 🌈欢迎来到数据结构专栏~~封装unordered_map 和 unordered_set
    • 一. 模板参数控制
    • 二. String类型无法取模问题
    • 三. 默认成员函数实现
      • 🌏构造函数
      • 🌏析构函数
    • 四. 正向迭代器
    • [ ]的实现
    • 面试题
    • unordered_set的实现
    • unordered_map的实现
    • 哈希表代码
  • 📢写在最后

请添加图片描述

看了上两篇的文章:用红黑树封装出map和set,那么本文思路其实是大差不差的,虽然哈希表的实现比红黑树要简单,但是unordered_map 和 unordered_set 的封装却要更加复杂一点,往下看吧,发车咯

一. 模板参数控制

老规矩unordered_set 是 K 模型的容器,而 unordered_map 是 KV 模型的容器,如果要实现封装,那么参数就必须控制

template<class K, class T>
class HashTable

T模板参数可能只是键值Key,也可能是由Key和Value共同构成的键值对。如果是unordered_set容器,那么它传入底层哈希表的模板参数就是Key和Key:

template<class K>
class unordered_set
{
public:
	//...
private:
	HashTable<K, K> _ht; //传入K和K
};

但如果是unordered_map容器,那么它传入底层哈希表的模板参数就是Key和Key和Value构成的键值对:

template<class K, class V>
class unordered_map
{
public:
	//...
private:
	HashTable<K, pair<K, V>> _ht; //传入K以及K和V构成的键值对
};

这样一来就可以实现泛型了,当上层容器是unordered_set的时候,结点当中存储的是键值Key;当上层容器是unordered_map的时候,结点当中存储的就是<Key, Value>键值对

在这里插入图片描述

所以更改后的节点定义如下:

template<class T>
struct HashNode
{
	T _data;
	HashNode<T>* _next;

	//构造函数
	HashNode(const T& data)
		:_data(data)
		, _next(nullptr)
	{}
};

为了得到元素的键值,通过哈希计算出对应的哈希地址

我们插入的时候当然不能用data直接去比较

  • 对于unordered_set而言是Key,可以比较
  • 但是对于unordered_map是pair,那我们要取其中的first来比较,但是我们能取first吗?
  • 这个地方的data有可能是unordered_map;也有可能是unordered_map

所以要实现一个仿函数,如果是unordered_map那就是用于获取T当中的键值Key

template<class K, class V>
class unordered_map
{
	//仿函数
	struct MapKeyOfT
	{
		const K& operator() (const pair<K, V>& kv)
		{
			return kv.first;
		}
	};

private:
	Bucket::HashTable<K, pair<K, V, MapKeyOfT>> _ht;
};

当然unordered_set也必不可缺,直接返回key即可

template<class K>
class unordered_set
{
	//仿函数
	struct SetKeyOfT
	{
		const K& operator() (const K& key)
		{
			return key;
		}
	};
private:
	Bucket::HashTable<K, K, SetKeyOfT>> _ht;
};

所以在哈希表上也要加上这个仿函数

	template<class K, class T, class KeyOfT>
	struct HashTable

二. String类型无法取模问题

字符串无法取模,是哈希表中的老问题了吧

上文提到过了,我们都能想到的,写库的大佬还不能想到吗?早就预留了一个模板参数,此时无论上层容器是unordered_set还是unordered_map,我们都能够通过上层容器提供的仿函数获取到元素的键值

在这里插入图片描述

因为没有使得字符串与整型之间一一对应的方法,因为整型大小是有限的,最大的无符号整型是 4294967295,但是字符串的排列却是无穷的,因此我们提出了 BKDRHash算法

经过前辈们实验后发现,BKDRHash算法无论是在实际效果还是编码实现中,效果都是最突出的。该算法由于在Brian Kernighan与Dennis Ritchie的《The C Programing Language》一书被展示而得名,是一种简单快捷的hash算法,也是Java目前采用的字符串的hash算法

如果是在我们实现哈希表的时候,此仿函数就应该加在哈希表中,如今我们要把哈希封装起来,我们就根本不通过底层的哈希表来调用,而是上层的 unordered_map等,所以 仿函数加在上层!

	template<class K, class V, class Hash = Hashfunc<K>>
	class unordered_map

上层传入的数据:符合string类型的优先走string类型

template<class K>
struct Hashfunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

//特化版本
template<>
struct Hashfunc<string>
{
	//BKDR算法
	size_t operator()(const string& key)
	{
		size_t val = 0;
		for (auto ch : key)
		{
			val *= 131;
			val += ch;
		}
		return val;
	}
};

三. 默认成员函数实现

🌏构造函数

哈希表中有两个成员变量,当我们实例化一个对象时:

_table会自动调用vector的默认构造函数进行初始化
_size会根据我们所给的缺省值被设置为0

private:
	vector<Node*> _table;
	size_t _size = 0; //存储的有效数据个数

vector会自动去调用自己的构造,内置类型的size不处理,所以直接置0

这里默认构造已经完美完成了构造的任务,我们就不需要画蛇添足后期再去写一个构造函数,但是因为我们需要自己写拷贝构造函数,会使得初始化不使用默认构造,因此我们需要 default 关键字修饰来让他保持默认构造的属性

HashTable() = default; //显示指定默认构造函数

🌏析构函数

因为哈希表当中存储的结点都是new出来的,因此在哈希表被析构时必须进行结点的释放。在析构哈希表时我们只需要依次取出非空的哈希桶,遍历哈希桶当中的结点并进行释放即可

//析构 : 内置类型不处理 但是桶要析构掉
~HashTable()
{
	for (size_t i = 0; i < _table.size(); ++i)
	{
		Node* cur = _table[i];
		while (cur)
		{
			Node* next = cur->_next;
			delete(cur);
			cur = next;
		}
		_table[i] = nullptr;
	}
}

四. 正向迭代器

哈希表的正向迭代器实际上就是对哈希结点指针进行了封装,实现++运算符重载,使他可以访问下一个非空的桶,所以每个正向迭代器里面存的都是哈希表地址。

template<class K, class T, class Hash, class KeyOfT>
struct HashTable;

template<class K, class T, class Hash, class KeyOfT>
struct __HashIterator
{
	typedef HashNode<T> Node;//节点类型
	typedef HashTable<K, T, Hash, KeyOfT> HT;//哈希表类型
	typedef __HashIterator<K, T, Hash, KeyOfT> Self;//迭代器类型

	Node* _node;
	HT* _pht;
}

所以在构造迭代器的时候就需要知道节点的指针,以及节点所在哈希表的地址:

	__HashIterator(Node* node, HT* pht)
		:_node(node)
		,_pht(pht)
	{}

++ 的实现逻辑也非常简单,只需要注意一下如果当前元素是该桶最后一个元素,那么 ++ 就是跳到下一个非空桶

在这里插入图片描述

T& operator*()
{
	return _node->_data;//返回节点数据的引用
}

T* operator->()
{
	return &_node->_data;//返回节点数据的地址
}

Self& operator++()
{
	if (_node->_next)//如果结点不是最后一个结点
	{
		//当前桶中迭代
		_node = _node->_next;
	}
	else
	{
		//找下一个桶 : 先算当前的哈希地址
		Hash hash;
		KeyOfT kot;
		size_t i = hash(kot(_node->_data)) % _pht->_table.size();//计算出哈希地址
		++i;//从下一个位置开始找
		for (; i < _pht->_table.size(); ++i)
		{
			if (_pht->_table[i])
			{
				_node = _pht->_table[i];//找到了node就跳转
				break;
			}
		}

		//说明后面没有 有数据的桶了
		if (i == _pht->_table.size())
		{
			_node = nullptr;
		}
	}
	return *this;
}

bool operator!=(const Self& s) const
{ 
	return _node != s._node;
}

bool operator==(const Self& s) const
{
	return _node == s._node;
}
};

哈希表的迭代器类型是单向迭代器,没有反向迭代器,即没有实–运算符的重载,若是想让哈希表支持双向遍历,可以考虑将哈希桶中存储的单链表结构换为双链表结构

下面开始抠细节了:

  1. 由于 ++ 重载函数在寻找下一个结点时,会访问哈希表成员变量 _table,而 _table 是哈希表的私有成员,因此我们需要将正向迭代器类声明为哈希表类的友元
  2. 接下来就可以进行正向迭代器类型的 typedef,需要注意的是,为了让外部能够使用 typedef 后的正向迭代器类型 iterator,我们需要在 public 区域进行 typedef
template<class K, class T, class Hash, class KeyOfT>
struct HashTable
{
	typedef HashNode<T> Node;

	//模板的友元要带上声明
	template<class K, class T, class Hash, class KeyOfT>
	friend struct __HashIterator;
public: 

	typedef __HashIterator<K, T, Hash, KeyOfT> iterator;

	iterator begin()
	{
		for (int i = 0; i < _table.size(); ++i)
		{
			//遍历找到了,就返回 
			if (_table[i])
			{
				//this就是指向哈希表的指针
				return iterator(_table[i], this);
			}
		}
		return end();
	}

	iterator end()
	{
		return iterator(nullptr, this);
	}
private:
	vector<Node*> _table; //哈希表
	size_t _n = 0; //有效元素个数
}

[ ]的实现

首先把Insert的返回值改成pair<iterator, bool>,随后的返回值也要跟着改

如果表内有重复的元素就返回这个找到的ret的迭代器
没有重复的就返回newnode这个迭代器

iterator ret = Find(kot(data));
if (ret != end())
{
	return make_pair(ret, false);
}

//头插
Node* newnode = new Node(data);
newnode->_next = _table[hashi]; // _table[hashi]指向的就是第一个结点
_table[hashi] = newnode;

++_size;

return make_pair(iterator(newnode, this), true);

面试题

一个类型去做set和unordered_set的模板参数有什么要求?

  • set要求支持小于比较,或者显示提供比较的仿函数
  • unordered_set:
    • K类型对象可以转换整形取模 or 提供转成整形的仿函数
    • K类型对象可以支持等于比较 or 提供等于比较的仿函数

unordered_set的实现

#include"HashTable.h"

namespace ljj
{
	template<class K, class Hash = Hashfunc<K>>
	class unordered_set
	{
		//仿函数
		struct SetKeyOfT
		{
			const K& operator() (const K& key) //返回key
			{
				return key;
			}
		};

	public:
		//因为是调用的是内嵌类型,typename防止编译器认成静态变量, 是类型!!!
		typedef typename Bucket::HashTable<K, K, Hash, SetKeyOfT>::iterator iterator;

		iterator begin()
		{
			return _ht.begin();
		}

		iterator end()
		{
			return _ht.end();
		}

		pair<iterator, bool> Insert(const K& key)
		{
			return _ht.Insert(key);
		}

	private:
		Bucket::HashTable<K, K, Hash, SetKeyOfT> _ht;
	};
}

unordered_map的实现

#include"HashTable.h"

namespace ljj
{
	template<class K, class V, class Hash = Hashfunc<K>>
	class unordered_map
	{
		//仿函数
		struct MapKeyOfT
		{
			const K& operator() (const pair<K, V>& kv) //返回键值对当中的键值key
			{
				return kv.first;
			}
		};

	public:
		//因为是调用的是内嵌类型,typename防止编译器认成静态变量, 是类型!!!
		typedef typename Bucket::HashTable<K, pair<K, V>, Hash, MapKeyOfT>::iterator iterator;

		iterator begin()
		{
			return _ht.begin();
		}

		iterator end()
		{
			return _ht.end();
		}

		pair<iterator, bool> Insert(const pair<K, V>& kv)
		{
			return _ht.Insert(kv);
		}

		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
			return ret.first->second;
		}

	private:
		Bucket::HashTable<K, pair<K, V>, Hash, MapKeyOfT> _ht;
	};
}

哈希表代码

namespace Bucket
{
	template<class T>
	struct HashNode
	{
		T _data;
		HashNode<T>* _next;

		//构造函数
		HashNode(const T& data)
			:_data(data)
			, _next(nullptr)
		{}
	};

	//前置声明
	template<class K, class T, class Hash, class KeyOfT>
	struct HashTable;

	template<class K, class T, class Hash, class KeyOfT>
	struct __HashIterator
	{
		typedef HashNode<T> Node;//节点类型
		typedef HashTable<K, T, Hash, KeyOfT> HT;//哈希表类型
		typedef __HashIterator<K, T, Hash, KeyOfT> Self;//迭代器类型

		Node* _node;
		HT* _pht;

		__HashIterator(Node* node, HT* pht)
			:_node(node)
			,_pht(pht)
		{}

		T& operator*()
		{
			return _node->_data;//返回节点数据的引用
		}

		T* operator->()
		{
			return &_node->_data;//返回节点数据的地址
		}

		Self& operator++()
		{
			if (_node->_next)//如果结点不是最后一个结点
			{
				//当前桶中迭代
				_node = _node->_next;
			}
			else
			{
				//找下一个桶 : 先算当前的哈希地址
				Hash hash;
				KeyOfT kot;
				size_t i = hash(kot(_node->_data)) % _pht->_table.size();//计算出哈希地址
				++i;//从下一个位置开始找
				for (; i < _pht->_table.size(); ++i)
				{
					if (_pht->_table[i])
					{
						_node = _pht->_table[i];//找到了node就跳转
						break;
					}
				}

				//说明后面没有 有数据的桶了
				if (i == _pht->_table.size())
				{
					_node = nullptr;
				}
			}
			return *this;
		}

		bool operator!=(const Self& s) const
		{ 
			return _node != s._node;
		}

		bool operator==(const Self& s) const
		{
			return _node == s._node;
		}
	};


	template<class K, class T, class Hash, class KeyOfT>
	struct HashTable
	{
		typedef HashNode<T> Node;

		//模板的友元要带上声明
		template<class K, class T, class Hash, class KeyOfT>
		friend struct __HashIterator;
	public: 

		typedef __HashIterator<K, T, Hash, KeyOfT> iterator;

		iterator begin()
		{
			for (int i = 0; i < _table.size(); ++i)
			{
				//遍历找到了,就返回 
				if (_table[i])
				{
					//this就是指向哈希表的指针
					return iterator(_table[i], this);
				}
			}
			
			return end();
		}

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

		//析构 : 内置类型不处理 但是桶要析构掉
		~HashTable()
		{
			for (size_t i = 0; i < _table.size(); ++i)
			{
				Node* cur = _table[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete(cur);
					cur = next;
				}
				_table[i] = nullptr;
			}
		}

		pair<iterator, bool> Insert(const T& data)
		{
			Hash hash;
			KeyOfT kot;

			//去重
			iterator ret = Find(kot(data));
			if (ret != end())
			{
				return make_pair(ret, false);
			}

			//负载因子到1就扩容
			if (_size == _table.size())
			{
				size_t newsize = _table.size() == 0 ? 10 : 2 * _table.size();
				vector<Node*> newTable;
				newTable.resize(newsize);

				//旧表节点移动映射到新表中
				for (size_t i = 0; i < _table.size(); i++)
				{
					Node* cur = _table[i];
					while (cur)
					{
						Node* next = cur->_next; //记录cur的下一个节点

						size_t hashi = hash(kot(cur->_data)) % newTable.size();//计算哈希地址
						//头插
						cur->_next = newTable[hashi];
						newTable[hashi] = cur;

						cur = next;
					}
					_table[i] = nullptr;//原桶取完后置空
				}
				//交换
				_table.swap(newTable);
			}

			size_t hashi = hash(kot(data)) % _table.size();
			//头插
			Node* newnode = new Node(data);
			newnode->_next = _table[hashi]; // _table[hashi]指向的就是第一个结点
			_table[hashi] = newnode;

			++_size;

			return make_pair(iterator(newnode, this), true);
		}

		//查找
		iterator Find(const K& key)
		{
			if (_table.size() == 0)//哈希表为0,没得找
			{
				return end();
			}

			Hash hash;
			KeyOfT kot;

			size_t hashi = hash(key) % _table.size();//招牌先算出哈希地址
			Node* cur = _table[hashi];
			while (cur)//知道桶为空
			{
				if (kot(cur->_data) == key)
				{
					return iterator(cur, this);
				}

				cur = cur->_next;
			}
			return end();//遍历完桶,都没找到,返回空
		}

		bool Erase(const K& key)
		{
			if (_table.size() == 0)
			{
				return nullptr;
			}

			//1、通过哈希函数计算出对应的哈希桶编号hashi
			Hash hash;
			KeyOfT kot;
			size_t hashi = hash(key) % _table.size();
			Node* prev = nullptr;
			Node* cur = _table[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					//头删
					if (prev == nullptr)
					{
						_table[hashi] = cur->_next;//将第一个结点从该哈希桶中移除
						delete cur;
					}
					else //中间删除
					{
						prev->_next = cur->_next;//将该结点从哈希桶中移除
						delete cur;
					}
					--_size;
					return true;
				}

				prev = cur;
				cur = cur->_next;
			}

			return false;
		}

	private:
		vector<Node*> _table;
		size_t _size = 0; //存储的有效数据个数
	};
}

📢写在最后

鹅鹅鹅

在这里插入图片描述

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

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

相关文章

【北京理工大学-Python 数据分析-3.1Pandas库的基本使用】

Pandas库的引用&#xff0c;常用两大功能Series(一维&#xff09;和DataFrame&#xff08;二维和多维&#xff09; Pandas是Python第三方库&#xff0c;提供高性能易用数据类型的分析工具。 Pandas基于NumPy实现&#xff0c;常与NumPy和Matplotlib一起使用。 Numpy和Pandas的…

SpringCloud-Netflix学习笔记03——什么是Eureka

什么是Eureka Eureka&#xff1a;怎么读&#xff1f; Netflix 在设计Eureka 时&#xff0c;遵循的就是AP原则。 1、CAP原则又称CAP定理&#xff0c;指的是在一个分布式系统中 2、一致性&#xff08;Consistency&#xff09; 3、可用性&#xff08;Availability&#xff09; 4、…

详细实例说明+典型案例实现 对动态规划法进行全面分析 | C++

第三章 动态规划法 目录 ●第三章 动态规划法 ●前言 ●一、动态规划法是什么&#xff1f; 1.简要介绍 2.生活实例 ●二、动态规划法对斐波那契数列的优化 1.优化方法 2.优化核心代码片段 3.代码实现以及结果展示 ●三、动态规划法的典型案例——最短总距离 …

c语言文件操作(万字解析)

c语言文件操作一.文件的打开与关闭1.文件指针-FILE*2.文件的打开与关闭二.文件的顺序读写1.字符操作函数-fgetc和fputc2.字符串操作函数-fgets和fputs3.格式化函数-fprintf和fscanf4.二进制函数-fread和fwrite5.对比一组函数三.文件的随机读写1.fseek和ftell2.调整指针-rewind四…

Python NumPy 数组索引

前言NumPy&#xff08;Numerical Python的缩写&#xff09;是一个开源的Python科学计算库。使用NumPy&#xff0c;就可以很自然地使用数组和矩阵。NumPy包含很多实用的数学函数&#xff0c;涵盖线性代数运算、傅里叶变换和随机数生成等功能。本文主要介绍Python NumPy 数组索引…

动态内容管理

这期我们来看动态内存管理的相关知识&#xff0c;话不多说&#xff0c;我们来看今天的正题 目录 1.为什么要有动态内存管理&#xff1f; 2.动态内存函数的介绍 2.1.malloc和free 2.2.calloc 2.3.realloc 3. 常见的动态内存错误 3.1 对NULL指针的解引用操作 3.2 对动态开…

Pytorch DataLoader中的num_workers (选择最合适的num_workers值)

一、概念 num_workers是Dataloader的概念&#xff0c;默认值是0。是告诉DataLoader实例要使用多少个子进程进行数据加载(和CPU有关&#xff0c;和GPU无关) 如果num_worker设为0&#xff0c;意味着每一轮迭代时&#xff0c;dataloader不再有自主加载数据到RAM这一步骤&#xff0…

滑动列表中使用粒子特效层级问题

前言 前面几个月疯狂堆功能,现在开始疯狂加动效,每次一说到动效就脑壳痛,还不如让我写功能。这不,今天又遇到问题了。滑动列表中mask粒子特效问题遮挡。 情况1 步骤1:使用粒子特效的层级应该>当前ui层级。 例如:当前界面所在层级为2000,其上的粒子特效至少为2001。…

dp(八)买卖股票的最好时机 (一,二、三)

目录 买卖股票的最好时机(一)_牛客题霸_牛客网 买卖股票的最好时机(二)_牛客题霸_牛客网 买卖股票的最好时机(三)_牛客题霸_牛客网 假设你有一个数组prices&#xff0c;长度为n&#xff0c;其中prices[i]是股票在第i天的价格&#xff0c;请根据这个价格数组&#xff0c;返回买…

基于云的文档管理系统:DocuWare Cloud

云文档管理软件&#xff1a;DocuWare Cloud 一流的云文档管理软件和工作流自动化内容服务&#xff0c;适用于任何规模的团队和公司——在多租户云平台上交付。 DocuWare Cloud 可在订阅的基础上为不同规模的公司提供灵活的许可证。 每个订阅都涵盖全方位的服务&#xff0c;包…

dvwa中的爆破

环境&#xff1a;dvwa: 192.168.11.135 dvwa版本&#xff1a; Version 1.9 (Release date: 2015-09-19)kail机器&#xff1a;192.168.11.1561、Low级别代码&#xff1a;1、启动 burpsuite 开始抓包&#xff0c;然后点击 login&#xff0c;然后在 bp 里面就能看见抓包到的包。这…

Java集合常见面试题(二)

Collection 子接口之 List ArrayList 和 Vector 的区别? ArrayList 是 List 的主要实现类&#xff0c;底层使用 Object[]存储&#xff0c;适用于频繁的查找工作&#xff0c;线程不安全 &#xff1b;Vector 是 List 的古老实现类&#xff0c;底层使用Object[] 存储&#xff0…

谷粒学院复习

一、Mybatis Plus复习分布式系统唯一ID主键策略(面试)面试的时候就说知道有以下四种策略&#xff0c;分别介绍一下每一种&#xff0c;然后说一下项目中用的是雪花算法分类自动增长 AUTO INCREMENT就是自动增长&#xff0c;每次都会自动加一。缺点&#xff1a;如果在分库分表的场…

VUE: Vue3+TS的项目搭建及基础使用

简介 通过 Vue-cli4 创建的 Vue3TS 的项目&#xff0c;并进行一些基础使用的举例。 项目搭建 1. 进入命令提示符窗口 在要搭建项目的文件夹中&#xff0c;点击路径&#xff0c;输入CMD并按回车 2. 查看node版本、Vue-cli版本 2.1 node版本&#xff08;14.x以上&#xf…

基于域控的SSO单点登录

大家好&#xff0c;好久不见&#xff0c;今天老吕给大家来一篇偏冷门知识的文章。一、需求大型集团企业内部会有许多业务系统&#xff0c;工作人员也往往需要登录多个业务系统才能完成工作&#xff0c;这就可能会存在一些问题1、多套账号与密码需要记录或者记忆2、多次登录&…

14.live555mediaserver-setup请求与响应

live555工程代码路径 live555工程在我的gitee下&#xff08;doc下有思维导图、drawio图&#xff09;&#xff1a;https://gitee.com/lure_ai/live555/tree/master 学习demo live555mediaserver.cpp 学习线索和姿势 1.学习的线索和姿势 网络编程 流媒体的地基是网络编程&…

Git 的常用命令

Git 的常用命令 目录Git 的常用命令帮助初始化配置提交远程仓库管理版本控制删除分支管理查看文件提交、状态帮助 查看常用命令 git help查看某个命令的使用帮助 git help [命令]查看 git 使用指南&#xff08;这个命令会详细展示 git 的使用周期&#xff09; git help tut…

【BP靶场portswigger-客户端13】跨来源资源共享(CORS)-4个实验(全)

前言&#xff1a; 介绍&#xff1a; 博主&#xff1a;网络安全领域狂热爱好者&#xff08;承诺在CSDN永久无偿分享文章&#xff09;。 殊荣&#xff1a;CSDN网络安全领域优质创作者&#xff0c;2022年双十一业务安全保卫战-某厂第一名&#xff0c;某厂特邀数字业务安全研究员&…

2022.12 青少年机器人技术等级考试理论综合试卷(三级)

2022年12月 青少年机器人技术等级考试理论综合试卷&#xff08;三级&#xff09; 分数&#xff1a; 100 题数&#xff1a; 30 一、 单选题(共 20 题&#xff0c; 共 80 分) 1.舵机接到 Arduino UNO/Nano 主控板的 2 号引脚&#xff0c; 下列选项中&#xff0c; 实现舵机在 0 度…

4、字符串处理

目录 一、字符串的构造 二、字符串比较 三、字符串查找 四、字符串替换 五、字符串——数值转换 Matlab中的字符串函数有&#xff1a; 一、字符串的构造 字符串或字符串数组的构造可以通过直接给变量赋值来实现&#xff0c;具体表达式中字符串的内容需要写在单引号内。如…