【C++学习】unordered_map和unordered_set的使用和封装

news2024/11/25 10:33:45

🐱作者:一只大喵咪1201
🐱专栏:《C++学习》
🔥格言:你只管努力,剩下的交给时间!
图

unordered_map和unordered_set

  • 🧢unordered_map/set
    • 🔮性能比较
    • 🔮成员函数
  • 🧢改造哈希表
    • 🔮增加迭代器
      • 类互相typedef时的前置声明
      • 友元声明
    • 🔮修改哈希表
  • 🧢封装哈希表
  • 🧢unordered_map的operator[]
  • 🧢const迭代器
    • 🔮单独定义const迭代器的原因
  • 🧢源码
  • 🧢总结

🧢unordered_map/set

tu
unordered_map和unorder_set是C++11中才有的,如上图中所示。

  • 可以看到,它和我们前面学习的map和set几乎一模一样,只是多了前面的unordered。

正如它的名字一样,unordered_map/set和map/set比起来,unordered_xxx系列的容器打印出来的数据是无序的。

图
如上图所示,以set和unordered_set为例,乱序插入的数据,set打印出来的数据是升序,而unordered_set打印出来的是无序的。

  • set和unordered_set都可以去重。

🔮性能比较

除了这点区别外,unordered_xxx系列容器当然还它自己的优点。

图

  • 生成的随机数是一百万个。
  • 比较unordered_set和set对这些数据进行插入,查找,删除花费的时间。

使用三种类型的数据比较:

  • 随机数:由于在VS2019下,最多生成的三万多个随机数,所以这一百万个随机数中就会有大量的重复数据,所以在插入到容器中这些数据时会发生降重。
  • 几乎没有重复的随机数:在生成随机数的基础上加上1,此时得到是随机数几乎没有重复的,插入到两个容器中。
  • 升序数据:插入两个容器中的数据是升序的,比较这种极端情况下两个容器的效率。

图
无论是哪种类型的数据,无论是插入,查找还是删除,unordered_set的效率都比set高,花费的时间都要少。

  • 插入时,升序数据对于set的效率是最高的,因为只需要进行简单的左旋就可以,但是仍然没有unordered_set的效率高。
  • 同样,unordered_map的效率也比map的效率要高。

unordered_map/set容器除了不能进行排序外,其他的效率都比map/set效率要高,所以在乱序的场景下,unordered_map/set的使用频率更高。

unordered_map/set的底层是哈希桶

🔮成员函数

unordered_map/set的使用和map/set的使用一模一样,这么本喵就不重复介绍了,只说明一些不一样的地方。

图
迭代器只有单向迭代器,没有反向迭代器,因为哈希桶的链表是单链表,所以不支持反向迭代器。
图
这是一些和挂的桶有关的成员函数,比如获取桶的个数,单个桶的大小等等,但是很少用的上。

图
这是一些和负载因子等有关的接口,比如获取负载因子,改变负载因子等等,同样很少用的上。

unordered_map/set和map/set不一样的接口主要就是体现在桶的单链表结构上,以及负载因子上。

图
同样可以用来统计水果个数。

🧢改造哈希表

unordered_map/set的底层使用的是哈希桶,我们需要在本喵的文章哈希表——闭散列 | 开散列(哈希桶)中的哈希桶基础上进行改造。

和map和set的封装一样,为了能让unordered_set和unordered_map使用同一个哈希桶,节点中存放的数据只有一个类型。

图

将节点中,原本两个模板参数K,V变成一个模板参数T,并且在哈希桶中将节点的类型也做相应的改变。

🔮增加迭代器

迭代器是所有容器必须有的,所以需要给哈希桶增加迭代器以供unordered_set和unordered_map使用。

先来看迭代器的++是如何实现的:

图
如上图所示,一个哈希表,其中有四个哈希桶,迭代器是it。

++it操作:

  • 如果it不是某个桶的最后一个元素,则it指向下一个节点。
  • 如果it是桶的最后一个元素,则it指向下一个桶的头节点。

要想实现上面的操作,迭代器中不仅需要一个_node来记录当前节点,还需要一个哈希表的指针,以便找下一个桶,代码如下:

图

接下来看++it的具体实现:

图

  • it不是处于某个桶的末尾,直接指向下一个节点。
  • 当it是某个桶的末尾时,指向下一个桶。
  • 首先需要确定当前桶的位置:

使用KeyOfT仿函数获取当前数据的key值(因为不知道是map还是set在调用)。
*再使用Hash仿函数将key值转换成可以模的整形(因为不知道key是整形还是字符串再或者其他自定义类型)。

  • 然后开始寻找下一个桶:

从当前哈希表下标开始向后寻找,直到找到下一个桶,将桶的头节点地址赋值给_node。
如果始终没有找到,说明没有桶了,也就是没有数据了,it指向end,这里使用空指针来代替end。

  • 将++后的迭代器返回。

上面代码中,获取key值的仿函数方法在map和set的封装中详细讲解过,将key值转换成可以模的整形在哈希表——闭散列 | 开散列(哈希桶)中详细讲解过,这里只是在复用。

这里的两个仿函数在模板参数中都不使用缺省值,因为在封装迭代器的上一层,也就是HashTable中会给它们传参。

类互相typedef时的前置声明

图
迭代器中有一个成员变量是哈希表的指针,如上图所示,所以在迭代器中typedef了HashTable成为 HT,方便我们使用。

根据我们前面实现迭代器的经验,迭代器其实是封装在Hashtable中的,也就是说,在HashTable中也会typedef迭代器:

图

  • 此时HashTable和HashIterator就构成了相互typedef的关系。

哈希表和迭代器类的定义势必会有一个先后顺序,本喵在定义的时候,在代码顺序上就是先定义迭代器,再定义的哈希表。

此时迭代器在typedef的时候就找不到哈希表的定义,因为编译器只会向上寻找而不会向下寻找。所以必须在HashIterator类前面先声明一下HashTable类,这种操作被叫做前置声明。

  • 前置声明一定要放在类外面,如果放在迭代器类里面,编译器只会在迭代器的命名空间中寻找哈希表的定义,这样是找不到的。
  • 前置声明放在类外面的时候,编译器会在整个命名空间中寻找哈希表的定义,就可以找到。

友元声明

图

在++迭代器的时候,会使用到哈希表指针,哈希表指针又会使用到HashTable中的_tables。

  • HashTable中的_tables是私有成员,在类外是不能访问的。

解决这个问题可以在HashTable中写一个公有的访问函数,也可以采用友元,本喵这里就是使用的友元的方式。

  • 类模板的友元声明需要写模板参数,在类名前面加friend关键字,如上图绿色框中所示。

迭代器中的其他操作,如解引用,箭头,以及相等等运算符的重载本喵就不再详细介绍了,后面本喵会附源码,直接看代码即可。

🔮修改哈希表

图

  • 哈希表的模板参数增加两个仿函数,如上图所示,仍然不使用缺省值,因为会在哈希表的上一层,也就是unordered_set和unordered_set中进行传参。
  • 使用typedef封装迭代器,并且给迭代器传对应的模板参数。

图

还需要在哈希表中增加获取迭代器起始位置和结束位置的接口,如上图代码所示。

  • 在获取其实位置时,需要从头开始遍历哈希表项,寻找到第一个桶的头节点作为起始位置。
  • 使用空指针代替迭代器的结束位置。
  • 在构造迭代器时,直接传this指针去定义迭代器中的哈希表指针。

图
在插入中,凡是使用到key值以及用key取模的地方,都要用仿函数取获得。包括删除中也是。

🧢封装哈希表

哈希表作为unordered_set和unordered_map的底层结构,是被封装在这两容器中的。

封装比较简单,直接看代码:

unordered_set:

#include "HashTable.h"

namespace wxf
{
	template <class K, class Hash = HashFunc<K>>
	class unordered_set
	{
		//获取key值仿函数
		struct KeyOfSet
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};
	public:
		typedef typename HashBucket::HashTable<K, K, Hash, KeyOfSet>::iterator iterator;
		//获取begin
		iterator begin()
		{
			return _ht.begin();
		}
		//获取end
		iterator end()
		{
			return _ht.end();
		}
		//插入
		bool insert(const K& key)
		{
			return _ht.Insert(key);
		}
		//查找
		bool find(const K& key)
		{
			return _ht.Find(key);
		}
		//删除
		bool erase(const K& key)
		{
			return _ht.Erase(key);
		}
	private:
		HashBucket::HashTable<K, K, Hash, KeyOfSet> _ht;
	};
}
  • 获取键值的仿函数在这里实现,并且传给HashTable,转换成可以模的整形的仿函数使用缺失值,具体实现在哈希表的头文件中。
  • 在这里也要封装迭代器,要记得加typename,告诉编译器这是一个类型而不是静态变量。
  • 迭代器的begin和end包括插入查找删除等操作直接复用HashTable的即可。
  • 成员变量是封装的哈希桶。

unordered_map:


#include "HashTable.h"

namespace wxf
{
	template <class K, class T, class Hash = HashFunc<K>>
	class unordered_map
	{
		//获取key值仿函数
		struct KeyOfMap
		{
			const K& operator()(const pair<const K, T>& kv)
			{
				return kv.first;
			}
		};
	public:
		typedef typename HashBucket::HashTable<K, pair<const K, T>, Hash, KeyOfMap>::iterator iterator;
		//获取begin
		iterator begin()
		{
			return _ht.begin();
		}
		//获取end
		iterator end()
		{
			return _ht.end();
		}
		//插入
		bool insert(const pair<const K,T>& kv)
		{
			return _ht.Insert(kv);
		}
		//查找
		bool find(const K& key)
		{
			return _ht.Find(key);
		}
		//删除
		bool erase(const K& key)
		{
			return _ht.Erase(key);
		}
	private:
		HashBucket::HashTable<K, pair<const K, T>, Hash, KeyOfMap> _ht;
	};
}

代码如上,原理和unordered_set的一模一样,只是这里的数据是键值对,是一个pair类型。

🧢unordered_map的operator[]

再来实现一下unordered_map中的operator[]。

图
首先,修改哈希表中的Find,让其返回迭代器,如果存在,返回key所在位置的迭代器,如果不存在,返回末尾的迭代器。

图
然后修改哈希表的Inerst,返回由迭代器和布尔值组成的键值对。

  • 先进行查找,如果存在,则返回key所在位置的迭代器和false组成的键值对。
  • 查找结构不存在,则返回插入新节点后key所在位置的迭代器和true组成的键值对。

图
最后修改unordered_map中的insert和find,find返回的是key所在位置的迭代器,复用哈希表的Find,insert返回的是key所在位置的迭代器和bool值组成的键值对,复用哈希表的Insert。

图
可以看到,使用operator[]成功统计出了水果的个数。

🧢const迭代器

图
从STL源码中可以看到,const迭代器又重新创建了一个类,并不是和我们之前一样是复用一个类的,这是为什么呢?先来看看和之前一样复用一个类会发生什么?

图

迭代器中增加const版本的解引用和箭头重载。
图

在哈希表中定义const迭代器,像之前一样,T&和T*的模板参数都是const类型,再增加两个得到起始和末尾迭代器的接口,如上图代码所示。

图
在unordere_set中也进行类似的定义,如上图代码所示。

图
然后就会出现这么长一串错误。

🔮单独定义const迭代器的原因

unordered_set/map的底层是哈希表,所以会用到vector容器,当一个const对象在使用vector的时候,尤其是operator[]接口:

图
const类型的this指针在调用该接口后,返回的是一个conset类型的引用。在unordered_set/map中返回的就是哈希表中桶节点的const指针。

  • 此时桶节点的指针和哈希表的指针都是const类型。

迭代器初始化时,需要的就是桶节点的指针和this指针:

图
已经存在的是用普通桶节点指针和普通this指针初始化的迭代器,由于现在两个指针都是const类型的,所以我们可以重载构造函数:

HashIterator(const Node* node, const HT* ht)
			:_node(node)
			,_ht(ht)
		{}

但是仍然不行,虽然构造函数的形参和两个const类型的指针一致了,但是,迭代器的成员变量_node和_ht都是普通类型的,用两个const类型的指针初始化两个普通类型的指针,会因为权限放大而报错。

所以需要将迭代器的成员变量_node和_ht也改成const类型的

图
此时构造函数就可以正常执行了,但是由于成员变量都变了,const迭代器和普通迭代器势必就不可以使用同一个类了,const迭代器必须单独定义一个类

图

  • 新建的const迭代器类和普通迭代器类是两个类,所以模板参数中的Ref和Ptr就不需要了,在两个类中单独除了解引用和箭头重载就可以。

图
普通迭代器中这两个接口是T&和T*,没有const修饰。

同样,和map/set那里一样,需要一个用普通迭代器构造const迭代器的构造函数,因为普通对象的const迭代器使用begin得到的是普通迭代器,需要转换成const迭代器。

普通对象也可以使用const迭代器,此时就需要有普通迭代器向const迭代器转换的构造函数

const对象必须使用const迭代器

🧢源码

namespace HashBucket
{
	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>
	class 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* _ht;//哈希表指针

		HashIterator(Node* node, HT* ht)
			:_node(node)
			,_ht(ht)
		{}

		HashIterator()
			:_node(nullptr)
			, _ht(nullptr)
		{}

		//解引用重载
		T& operator*()
		{
			return _node->_data;
		}
		//箭头重载
		T* operator->()
		{
			return &_node->_data;
		}
		//!=重载
		bool operator!=(const Self& it) const
		{
			return _node != it._node;
		}
		//==重载
		bool operator==(const Self& it) const
		{
			return _node == it._node;
		}
		//前置++重载
		Self& operator++()
		{
			//直接指向单链表中的下一个
			if (_node->_next)
			{
				_node = _node->_next;
			}
			//一个桶已经结束,需要寻找下一个桶
			else
			{
				//确定当前桶在哈希表的位置
				KeyOfT kot;
				Hash hash;
				size_t Hashi = hash(kot(_node->_data)) % _ht->_tables.size();
				//寻找下一个桶
				++Hashi;
				while (Hashi < _ht->_tables.size())
				{
					//找到下一个桶,it指向头节点
					if (_ht->_tables[Hashi])
					{
						_node = _ht->_tables[Hashi];
						break;//++结束
					}
					++Hashi;
				}
				//跳出循环有两种情况
				//1.所有桶都遍历完了
				if (Hashi == _ht->_tables.size())
				{
					//用空指针充当迭代器的end
					_node = nullptr;
				}
			}
			//找到下一个节点,返回迭代器
			return *this;
		}
		//后置++重载
		Self operator++(int)
		{
			Self* ret = this;//记录当前迭代器位置
			operator++();
			return *ret;
		}
	};
	template <class K, class T, class Hash, class KeyOfT>
	struct ConstHashIterator
	{
		typedef HashNode<T> Node;
		typedef HashTable<K, T, Hash, KeyOfT> HT;
		typedef ConstHashIterator<K, T, Hash, KeyOfT> Self;
		typedef HashIterator<K, T, Hash, KeyOfT> iterator;//普通迭代器

		const Node* _node;//数据节点
		const HT* _ht;//哈希表指针

		ConstHashIterator(const Node* node, const HT* ht)
			:_node(node)
			, _ht(ht)
		{}

		ConstHashIterator()
			:_node(nullptr)
			, _ht(nullptr)
		{}

		ConstHashIterator(const iterator& it)
			:_node(it._node)
			,_ht(it._ht)
		{}

		//解引用重载
		const T& operator*() const
		{
			return _node->_data;
		}
		//箭头重载
		const T* operator->() const
		{
			return &_node->_data;
		}
		//!=重载
		bool operator!=(const Self& it) const
		{
			return _node != it._node;
		}
		//==重载
		bool operator==(const Self& it) const
		{
			return _node == it._node;
		}
		//前置++重载
		Self& operator++()
		{
			//直接指向单链表中的下一个
			if (_node->_next)
			{
				_node = _node->_next;
			}
			//一个桶已经结束,需要寻找下一个桶
			else
			{
				//确定当前桶在哈希表的位置
				KeyOfT kot;
				Hash hash;
				size_t Hashi = hash(kot(_node->_data)) % _ht->_tables.size();
				//寻找下一个桶
				++Hashi;
				while (Hashi < _ht->_tables.size())
				{
					//找到下一个桶,it指向头节点
					if (_ht->_tables[Hashi])
					{
						_node = _ht->_tables[Hashi];
						break;//++结束
					}
					++Hashi;
				}
				//跳出循环有两种情况
				//1.所有桶都遍历完了
				if (Hashi == _ht->_tables.size())
				{
					//用空指针充当迭代器的end
					_node = nullptr;
				}
			}
			//找到下一个节点,返回迭代器
			return *this;
		}
		//后置++重载
		Self operator++(int)
		{
			Self* ret = this;//记录当前迭代器位置
			operator++();
			return *ret;
		}
	};
	template <class K, class T, class Hash, class KeyOfT>
	class HashTable
	{
		typedef HashNode<T> Node;
		//友元声明
		template <class K, class T, class Hash, class KeyOfT>
		friend struct HashIterator;
		template <class K, class T, class Hash, class KeyOfT>
		friend struct ConstHashIterator;
	public:
		typedef HashIterator<K, T, Hash, KeyOfT> iterator;//普通迭代器
		typedef ConstHashIterator<K, T, Hash, KeyOfT> const_iterator;//const迭代器
		//构造函数
		HashTable()
			:_n(0)
		{
			//哈希表10个位置,每个为空
			_tables.resize(__stl_next_prime(0), nullptr);
		}
		//析构函数
		~HashTable()
		{
			for (auto& e : _tables)
			{
				//释放桶
				Node* cur = e;
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				e = nullptr;
			}
		}
		//获取迭代器begin
		iterator begin()
		{
			//寻找第一个桶
			for (size_t i = 0; i < _tables.size(); ++i)
			{
				if (_tables[i])
				{
					//使用第一个桶的头节点和哈希表指针构造迭代器
					return iterator(_tables[i], this);
				}
			}
			//空哈希表
			return iterator(nullptr, this);
		}
		//获取迭代器end
		iterator end()
		{
			//用空节点代替end
			return iterator(nullptr, this);
		}
		//获取迭代器begin
		const_iterator begin() const
		{
			//寻找第一个桶
			for (size_t i = 0; i < _tables.size(); ++i)
			{
				if (_tables[i])
				{
					//使用第一个桶的头节点和哈希表指针构造迭代器
					return const_iterator(_tables[i], this);
				}
			}
			//空哈希表
			return const_iterator(nullptr, this);
		}
		//获取迭代器end
		const_iterator end() const
		{
			//用空节点代替end
			return const_iterator(nullptr, this);
		}
		//插入
		pair<iterator, bool> Insert(const T& data) 
		{
			KeyOfT kot;
			//禁止重复节点插入
			iterator ret = Find(kot(data));
			if (ret != end())
			{
				return make_pair(ret, true);
			}
			//if (Find(kot(data)))
			//	return false;
			//负载因子为1的时候发生扩容
			if (_n == _tables.size())
			{
				新哈希表是原来的二倍
				//HashTable newHash;
				//newHash._tables.resize(2 * _tables.size(), nullptr);
				将旧表中数据插入到表
				//for (auto& e : _tables)
				//{
				//	Node* cur = e;
				//	while (cur)
				//	{
				//		newHash.Insert(cur->_kv);
				//		cur = cur->_next;
				//	}
				//}
				现代写法
				//_tables.swap(newHash._tables);
				//直接创建新的vector,不再创建新的哈希桶
				vector<Node*> newTables;
				newTables.resize(__stl_next_prime(_tables.size()), nullptr);
				for (auto& e : _tables)
				{
					Node* cur = e;
					while (cur)
					{
						Node* next = cur->_next;//记录下一个节点
						//哈希重映射
						size_t Hashi = Hash()(kot(cur->_data)) % newTables.size();
						//旧表中数据头插到桶中
						cur->_next = newTables[Hashi];
						newTables[Hashi] = cur;
						//迭代cur
						cur = next;
					}
					//将旧表置空,否则会析构掉桶
					e = nullptr;
				}
				//现代写法
				_tables.swap(newTables);
			}
			//哈希映射
			size_t Hashi = Hash()(kot(data)) % _tables.size();
			//头插
			Node* newnode = new Node(data);
			newnode->_next = _tables[Hashi];
			_tables[Hashi] = newnode;
			++_n;

			return make_pair(iterator(newnode, this), true);
		}
		//查找
		iterator Find(const K& key)
		{
			//根据哈希函数直接定位哈希表
			size_t Hashi = Hash()(key) % _tables.size();
			Node* cur = _tables[Hashi];
			//在桶中寻找
			while (cur)
			{
				if (KeyOfT()(cur->_data) == key)
				{
					//返回key值所在位置
					//return cur;
					return iterator(cur, this);
				}
				cur = cur->_next;
			}
			//return nullptr;
			return iterator(nullptr, this);
		}
		//删除
		bool Erase(const K& key)
		{
			//Node* ret = Find(key);
			该值存在,进行删除
			//if (ret)
			//{
			//	//删除单链表中的节点
			//}
			size_t Hashi = Hash()(key) % _tables.size();
			Node* cur = _tables[Hashi];
			Node* prev = nullptr;
			while (cur)
			{
				//找到
				if (KeyOfT()(cur->_data) == key)
				{
					//key是头节点
					if (cur == _tables[Hashi])
					{
						//直接指向下一个节点
						_tables[Hashi] = cur->_next;
					}
					//key不是头节点
					else
					{
						//key的前一个节点指向key的下一个节点
						prev->_next = cur->_next;
					}
					//删除key所在的节点
					delete cur;
					--_n;

					return true;
				}
				//继续寻找
				else
				{
					//记录prev
					prev = cur;
					cur = cur->_next;
				}
			}
			//key不存在,删除失败
			return false;
		}
		//获取素数
		inline unsigned long __stl_next_prime(unsigned long n)
		{
			static const int __stl_num_primes = 28;
			static const unsigned long __stl_prime_list[__stl_num_primes] =
			{
				53, 97, 193, 389, 769,
				1543, 3079, 6151, 12289, 24593,
				49157, 98317, 196613, 393241, 786433,
				1572869, 3145739, 6291469, 12582917, 25165843,
				50331653, 100663319, 201326611, 402653189, 805306457,
				1610612741, 3221225473, 4294967291
			};

			for (int i = 0; i < __stl_num_primes; ++i)
			{
				if (__stl_prime_list[i] > n)
				{
					return __stl_prime_list[i];
				}
			}

			return __stl_prime_list[__stl_num_primes - 1];
		}
	private:
		vector<Node*> _tables;//指针数组
		size_t _n;
	};
};

🧢总结

在unordered_set/map封装哈希表时,首先就是迭代器,要知道迭代器++是怎么移动的,通过计算当前桶的位置去寻找下一个桶,以及反向迭代器需要单独创建一个类的原因和实现。还要知道两个类在互相typedef时,需要有类的前置声明。

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

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

相关文章

Python采集二手车数据信息实现数据可视化展示

前言 嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 环境使用&#xff1a; Python 3.8 Pycharm 专业版是付费的 <文章下方名片可获取魔法永久用~> 社区版是免费的 模块使用&#xff1a; 第三方模块 需要安装的 requests >>> pip install requests p…

分享5款让生活和工作变得更加高效的软件

随着科技的发展,我们的生活和工作变得越来越数字化和自动化。许多实用软件应运而生,它们不仅简化了我们的生活,也使工作变得更加高效。这里我们来介绍5款非常实用的软件工具。 1.安全擦除工具——Secure Eraser Secure Eraser是一款可以安全删除数据的工具&#xff0c;它使用…

基于铜锁,在前端对登录密码进行加密,实现隐私数据保密性

本文将基于 铜锁&#xff08;tongsuo&#xff09;开源基础密码库实现前端对用户登录密码的加密&#xff0c;从而实现前端隐私数据的保密性。 首先&#xff0c;铜锁密码库是一个提供现代密码学算法和安全通信协议的开源基础密码库&#xff0c;在中国商用密码算法&#xff0c;例…

京东T7架构师用470页就把微服务架构原理与开发实战文档讲完了

前言 最近几年软件开发方法层出不穷&#xff0c;微服务作为一种主流的架构模式一直热度不减。 为了帮助广大程序员们更好更快地理解微服务的概念&#xff0c;学习微服务在项目中的实践&#xff0c;本文全面阐述了微服务架构模式的特点、架构思路、设计理念、技术框架及具体的…

根据cadence设计图学习硬件知识day07 了解一些芯片

1.LC0502N &#xff08;ESD静电保护元件&#xff09; 1.1 LC0502N 介绍 应用 ● USB 2.0电源和数据线 ● 机顶盒和数字电视 ● 数字视频接口&#xff08;DVI&#xff09; ● 笔记型电脑 ● SIM端口 ● 10/100以太网 1.2 LC0502N 引脚介绍 &#xff08;无语&#xff0…

1。C语言基础知识回顾

学习嵌入式的C基础知识&#xff0c;主要包括几个核心知识点&#xff1a;三大语法结构、常用的数据类型、函数、结构体、指针、文件操作。 一、顺序结构 程序自上而下依次执行、没有分支、代码简单。 常见顺序结构有&#xff1a;四则运算&#xff1a;&#xff0c;-&#xff0…

通达信顾比倒数线指标公式,信号不漂移

顾比倒数线是由技术派大师戴若顾比发明的&#xff0c;该指标利用三个重要的价格来判断入场或离场时机&#xff0c;可用于盘后制定下一个交易日的操作计划。此外&#xff0c;顾比倒数线还可以用于补充验证其他指标。 在编写顾比倒数线选股公式之前&#xff0c;需要先了解顾比倒…

vue3之vite创建h5项目之2 (sass公共样式、声明组件、路由配置和layout组件 )

目录 vue3之vite创建h5项目之2 &#xff08; &#xff09;1&#xff1a;安装sass1-1 使用sass引入公共样式11-1-1 main.ts 引入公共样式方式 1-2 vite.config.ts 引入公共样式方式21-3样式文件1-3-1 src / style / index.scss ( 适配iphonex等还有引入其他公共的样式 )1-3-2 sr…

CRM部署Always on 后 CRM报无法更新数据库,数据库只读,且读写分离不正常

CRM部署Always on 后 CRM报无法更新数据库&#xff0c;数据库只读&#xff0c;读写分离不正常 问题描述背景信息问题原因解决方案 问题描述 CRM部署Always on 后 CRM报无法更新数据库&#xff0c;数据库只读 读写分离不正常,出现错乱链接。 背景信息 1.2个节点配置SQL serve…

从安全气囊到标配EDR,TOP10控制器供应商领跑市场

2022年1月1日开始&#xff0c;国内新生产新乘用车都必须标配EDR&#xff08;Event Data Recorder&#xff09;&#xff0c;也就是俗称的汽车“黑匣子”&#xff0c;也称为汽车事件数据记录系统&#xff0c;记录的数据可重现事故过程&#xff0c;用于汽车事故分析。 在此之前&am…

【常用算法】进制转换

目录 1. 二进制数、八进制数、十六进制数转换为十进制数 2. 十进制数转换为二进制数、八进制数、十六进制数 3. 二进制数和十六进制数的相互转换 4. 使用电脑计算器进行进制转换 1. 二进制数、八进制数、十六进制数转换为十进制数 十进制数的每一位都是10的指数幂。如&…

基于趋动云的 Stable Diffusion Webui 环境搭建

Stable Diffusion Webui 环境搭建&#xff0c;首先新建一个项目&#xff1a; 然后&#xff0c;选择镜像。注意点公开的&#xff0c;已近做好的这个镜像&#xff0c;superx创建&#xff0c;集成了miniconda3的镜像。 然后选择添加数据源&#xff0c;一样&#xff0c;还是点公开&…

Epinio:Kubernetes 的应用程序开发引擎

王海龙&#xff0c;Rancher 中国社区技术经理&#xff0c;Linux Foundation APAC Evangelist&#xff0c;负责 Rancher 中国技术社区的维护和运营。拥有 9 年的云计算领域经验&#xff0c;经历了 OpenStack 到 Kubernetes 的技术变革&#xff0c;无论底层操作系统 Linux&#x…

A100单机多卡大模型训练踩坑记录(CUDA环境、多GPU卡住且显存100%)

踩坑1&#xff1a;服务器只装了 CUDA Driver 没装 CUDA Toolkit 系统&#xff1a;Ubuntu-18.04 用 deepspeed 跑百亿模型训练时&#xff0c;报关于 CUDA_HOME 的错误。 AssertionError: CUDA_HOME does not exist, unable to compile CUDA op(s)执行 echo $CUDA_HOME 和 nvcc…

HTB-Nineveh

HTB-Nineveh 信息收集80端口443端口80端口-新443端口-新 立足www-data -> amroisamrois -> root其他有意思的地方knock knock - Whos there?socket type 信息收集 80端口 目录扫描 /info.php目录 443端口 目录扫描 这完全没头绪&#xff0c;估计是信息收集漏了东西…

NCR被攻击后服务中断!原是BlackCat勒索软件作祟

近日&#xff0c;在遭到BlackCat勒索软件攻击后&#xff0c;NCR 的 Aloha 销售点平台出现中断。 NCR公司是全球关系管理技术解决方案领导供应商&#xff0c;为全球零售、金融、传讯、制造、旅游、交通及保安等客户提供服务。凭著累积多年的业界知识、专业顾问经验、专业增值应用…

手把手教你本地CPU环境部署清华大模型ChatGLM-6B,利用量化模型,本地即可开始智能聊天,达到ChatGPT的80%

大家好&#xff0c;我是微学AI&#xff0c;今天教你们本地CPU环境部署清华大ChatGLM-6B模型&#xff0c;利用量化模型&#xff0c;每个人都能跑动大模型。ChatGLM-6B是一款出色的中英双语对话模型&#xff0c;拥有超过62亿个参数&#xff0c;可高效地处理日常对话场景。与GLM-1…

数据备份系列:Rsync 备份详解(一)

一、Rsync 简介 1.1 Rsync 是一个远程增量文件备份软件工具 1.2 Rsync 的特性 支持拷贝特殊文件&#xff0c;如连接文件、设备等。可以有排除指定文件或目录同步的功能&#xff0c;相当于打包命令 tar 的排除功能。可以做到保持原文件或目录的权限、时间、软硬链接、属主、组…

MySQL中的锁有哪些,作用是什么?

概述&#xff1a; 锁最要是用来实现MySQL的隔离性。我们都知道事务有四大特性分别是&#xff1a;原子性、一致性、隔离性、持久性&#xff0c;即所说的ACID。 一、什么是ACID 1、原子性&#xff1a;事务中包含有很多操作&#xff0c;这些操作要么全部执行&#xff0c;要么全…

ShareSDK 抖音平台注册

注册开发者账号 进入抖音开放平台 &#xff1a;抖音开放平台 登录/注册账户 进入[控制台] —> [移动应用] —> [认证企业资质] 完成认证后提交 创建应用 进入[控制台] —> [移动应用] —> [创建移动应用] 填写相关信息同意隐私协议后提交 这里必须要填写好自己的…