【C++】unordered_map和unordered_set

news2024/12/23 11:46:06

哈希表

  • 1. unordered_map
    • 1.1 概念
    • 1.2 常见接口
  • 2. unordered_set
    • 2.1 概念
    • 2.1 常见接口
  • 3. 底层实现
    • 3.1 哈希
    • 3.2 哈希函数
    • 3.3 闭散列和开散列
      • 3.3.1 闭散列
      • 3.3.2 开散列
    • 3.4 模拟实现
      • 3.4.1 改造哈希桶
      • 3.4.2 模拟实现unordered_set
      • 3.4.3 模拟实现unordered_map


在C++11中,STL新增了4个unordered_xxx系列的关联式容器,它们在用法上和红黑树的关联式容器类似。但在查询效率上,unordered_xxx系列略胜一筹。红黑树中的节点非常多时,查询效率也不理想(需要进行多次比较),最坏情况下要比较高度次,unordered_xx系列容器进行很少的比较次数就能够将元素找到。

1. unordered_map

1.1 概念

在这里插入图片描述

  1. unordered_map存储的是pair<key,value>的键值对。
  2. unordered_map的key是唯一的,通过key可以找到对应的value。
  3. unordered_map存储的元素是无序的。
  4. unordered_map的迭代器是单向迭代器(前向迭代器)。
  5. unordered_map支持[]访问,通过key返回value。

1.2 常见接口

  1. 构造

在这里插入图片描述

  1. 容量

在这里插入图片描述

  1. 迭代器

在这里插入图片描述
只有正向迭代器,没有反向迭代器。

  1. 元素查找

(1)find

在这里插入图片描述

(2)count
在这里插入图片描述

(3)equal_range
在这里插入图片描述

  1. 插入和删除

在这里插入图片描述
在这里插入图片描述

  1. 哈希桶

在这里插入图片描述

  1. []访问

在这里插入图片描述


2. unordered_set

2.1 概念

在这里插入图片描述

2.1 常见接口

unordered_set的常见接口和unordered_map的接口一样,这里就不赘述。


3. 底层实现

unordered系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。

3.1 哈希

  1. 哈希是存储的值与存储位置建立一个一一映射的关系。哈希表就是用哈希函数建立的结构,通过这层关系就可以找到想要的元素,不需要红黑树需要进行多次比较。
  2. 不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。这些不同关键字映射出相同的哈希函数,叫做同义词。
  3. 如何解决哈希冲突?可以根据不同的场景,使用合适的哈希函数。

3.2 哈希函数

这里讲的两种方法是最常见的。

  1. 直接定址法

取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B。这种方法比较简单,映射的哈希函数值比较均匀。但是只适用于范围集中的关键字,否则映射的哈希值的差越大,浪费的空间越多。

  1. 除留余数法

设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。

3.3 闭散列和开散列

闭散列和开散列是解决哈希冲突的方法,采用的哈希函数是除留余数法。

3.3.1 闭散列

闭散列(也叫开放定址法),在插入关键字时,如果关键字映射的哈希地址内容为空,直接填入关键字,如果关键字映射的哈希地址内容非空,将关键字填入下一个空位置。这下一个空位置,可以由两种方法得到:线性探测和二次探测。

  1. 线性探测

(1)在插入元素发现,映射的位置发生冲突,此时向后探测,直到发现空位置即可。
(2)在删除元素时,不能直接删除,因为删除后会影响元素的查找和插入,应该用标记法记录元素此时的状态。
(3)如果不停地插入元素,最终找不到空位置,怎么办?不会找不到空位置的 ,在哈希表中元素个数和哈希表的长度达到某种关系后,就会扩容。这种关系就是负载因子α = 元素个数/哈希表的长度,α越大,产生冲突的概率越大,α越小,产生冲突的概率越小,但浪费的空间越多。所以选择合适的α是有必要的,一般α的范围是0.7~0.8。也就是说,当α到达这个范围就得扩容。
(4)如果插入元素不是整数,怎么办?想办法把它转换成整数。
在这里插入图片描述

//在取模操作时,如果key不是整形,怎么办?想办法让它转换成整形,确保查找和删除时能准确定位,建立新的映射关系。
//为此,可以写个仿函数
template<class K>
struct ConvertToInt
{
	size_t operator()(const K& key)
	{
		return size_t(key);
	}
};
//如果key是个string,如何将string转换成整形
//返回第一个字符,那就太容易冲突;将所有字符加起来,但string的字符顺序不同,转换成整形相同,也容易冲突
//可以用到一些大佬的算法:BKDR算法。就可以减少冲突。
template<>
struct ConvertToInt< string >
{
	size_t operator()(const string&str)
	{
		size_t hash = 0;
		for (auto e : str)
		{
			hash *= 131;
			hash += e;
		}
		return hash;
	}
};

namespace open_address
{
	//元素状态
	//为什么要设置元素的状态?当元素被删除,与该元素有相同余数的元素就得往前挪一位,效率低
	//所以设置状态标记,将删除元素的位置设置为DELETE,有元素的位置设置为EXIST,没有元素的位置设置为EMPTY
	enum state
	{
		EXIST,
		EMPTY,
		DELETE
	};

	//哈希表存储元素的类型
	//假设存储的是pair
	template<class K,class V>
	struct HashData
	{
		pair<K, V> _data;
		state _state = EMPTY;
	};

	template<class K,class V,class HashFunc = ConvertToInt<K>>
	class HashTable
	{
	public:
		HashTable()
		{
			//初始大小设置为10
			_table.resize(10);
		}

		//查找
		HashData<const K, V>* Find(const K&key)
		{
			HashFunc hf;

			//除留余数法,获得在哈希表中的下标。
			//为什么不%_capacity?如果%capacity,下标可能大于size,但[]访问限制下标<size。
			int hashi = hf(key) % _table.size();
			//为什么查找到状态为EMPTY的元素就结束?因为哈希表是不会满的,否则效率就降低。
			while (_table[hashi]._state != EMPTY)
			{
				if(_table[hashi]._state == EXIST && _table[hashi]._data.first == key)
				{
					return (HashData<const K, V>*) & _table[hashi];
				}
				++hashi;
				//防止越界
				hashi %= _table.size();
			}
			return nullptr;
		}

		//插入
		bool Insert(const pair<K, V>& data)
		{
			if (Find(data.first))
			{
				return false;
			}

			//考虑扩容
			//负载因子 = 填入表中的元素个数(_n)/散列表的长度(_table.size())
			//负载因子越大,发生冲突的概率越大;负载因子越小,发生冲突的概率越小,空间利用率越低。
			//哈希表不能满了再扩容,控制负载因子到一定值就扩容,比如0.7
			if ((double)_n / _table.size() >= 0.7)
			{
				//扩容后有些值的映射关系可能发生变化。原本冲突的现在不一定冲突;原本不冲突的现在可能冲突
				HashTable<K, V> newHT;
				newHT._table.resize(_table.size() * 2);
				for (auto& e : _table)
				{
					if (e._state == EXIST)
					{
						//newHT空间足够,就不会进到扩容里面,不会造成死循环
						newHT.Insert(e._data);
					}
				}
				_table.swap(newHT._table);
			}

			HashFunc hf;
			int hashi = hf(data.first) % _table.size();
			//不用担心找不到状态为空的位置,因为前面已经扩容
			while (_table[hashi]._state == EXIST)
			{
				++hashi;
				hashi %= _table.size();
			}
			_table[hashi]._data = data;
			_table[hashi]._state = EXIST;
			++_n;
			return true;
		}

		//删除
		bool Erase(const K& key)
		{
			HashData<const K,V>*ret = Find(key);
			if (ret)
			{
				//不能直接删除,应该将状态置成DELETE
				ret->_state = DELETE;
				--_n;
				return true;
			}
			return false;
		}

	private:
		vector<HashData<K, V>> _table;
		size_t _n = 0;//哈希表中有效元素(不包括删除的节点)的个数
	};
}
  1. 二次探测

二次探测是在存储位置冲突时,在hashi+i ^2的位置或者在hashi-i ^2的位置,i取决于在查找和插入时冲突的次数。二次探测只是存储位置的规则发生变化,其他与线性探测一样。
在这里插入图片描述

3.3.2 开散列

闭散列的方法空间利用率太低,浪费空间,且冲突会相互影响,你抢占我的位置,我抢占他的位置。所以就有第二种方法开散列(也叫拉链法/哈希桶)。
在这里插入图片描述
开散列的实现

//解决哈希冲突的方法:拉链法/哈希桶

//在取模操作时,如果key不是整形,怎么办?想办法让它转换成整形,确保查找和删除时能准确定位,建立新的映射关系。
//为此,可以写个仿函数
template<class K>
struct ConvertToInt
{
	size_t operator()(const K& key)
	{
		return size_t(key);
	}
};
//如果key是个string,如何将string转换成整形
//返回第一个字符,那就太容易冲突;将所有字符加起来,但string的字符顺序不同,转换成整形相同,也容易冲突
//可以用到一些大佬的算法:BKDR算法。
template<>
struct ConvertToInt< string >
{
	size_t operator()(const string&str)
	{
		size_t hash = 0;
		for (auto e : str)
		{
			hash *= 131;
			hash += e;
		}
		return hash;
	}
};

namespace hash_bucket
{
	//节点
	//不同于开放定址法,哈希桶放的是节点
	template<class K, class V>
	struct HashNode
	{
		pair<K, V> _data;
		HashNode<K, V>* _next;

		HashNode(const pair<K, V>& data)
			:_data(data)
			, _next(nullptr)
		{}
	};

	//哈希表
	template<class K, class V, class HashFunc = ConvertToInt<K>>
	class HashTable
	{
	public:
		typedef HashNode<K, V> Node;
		//构造
		HashTable()
		{
			//先开10个哈希桶
			_table.resize(10,nullptr);
		}
		
		//析构
		//析构函数需要自己定义,因为指针是内置类型,编译器不会调用其析构
		~HashTable()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				Node* cur = _table[i];
				Node* next = nullptr;
				while (cur)
				{
					next = cur->_next;
					delete cur;
					cur = next;
				}
				_table[i] = nullptr;
			}
		}

		//查找
		Node* Find(const K& key)
		{
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();
			Node* cur = _table[hashi];
			while (cur)
			{
				if (cur->_data.first == key)
				{
					return cur;
				}
				cur = cur->_next;
			}
			return nullptr;
		}

		//插入
		bool Insert(const pair<K,V>& data)
		{
			if (Find(data.first))
			{
				return false;
			}

			HashFunc hf;

			//扩容
			//为什么还需要扩容?将元素挂在哈希桶不就可以了,不用考虑容量问题。
			//不扩容,不断插入节点,某些桶越来越长,和链表一样,查找效率就下降了。
			//负载因子可以适当放大,一般负载因子控制在1,平均下来每个桶一个元素,这样查找效率就很高。
			if (_n == _table.size())
			{
				vector<Node*> newtable;
				size_t newsize = _table.size() * 2;
				newtable.resize(newsize, nullptr);
				//遍历旧表,将旧表中的节点牵过来,挂在新表对应的位置
				for (size_t i = 0; i < _table.size(); i++)
				{
					Node* cur = _table[i];
					while (cur)
					{
						Node* next = cur->_next;
						//在新表中,插入位置可能改变
						size_t hashi = hf(cur->_data.first) % newsize;
						//头插
						cur->_next = newtable[hashi];
						newtable[hashi] = cur;
						cur = next;
					}
					_table[i] = nullptr;
				}
				_table.swap(newtable);
			}

			size_t hashi = hf(data.first) % _table.size();
			//头插
			Node* newnode = new Node(data);
			newnode->_next = _table[hashi];
			_table[hashi] = newnode;
			++_n;
			return true;
		}

		//删除
		bool Erase(const K& key)
		{
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();
			Node* cur = _table[hashi];
			Node* prev = nullptr;
			while (cur)
			{
				if (cur->_data.first == key)
				{
					if (prev)
					{
						prev->_next = cur->_next;
					}
					else
					{
						_table[hashi] = cur->_next;
					}
					delete cur;
					return true;
				}
				prev = cur;
				cur = cur->_next;
			}
			return false;
		}

		//打印
		void Print()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				printf("[%d]->", i);
				Node* cur = _table[i];
				while (cur)
				{
					cout << cur->_data.first << ":" << cur->_data.second << "->";
					cur = cur->_next;
				}
				cout << "NULL" << endl;
			}
			cout << endl;
		}
	private:
		vector<Node*> _table;
		size_t _n;
	};
}

3.4 模拟实现

3.4.1 改造哈希桶

template<class K>
struct ConvertToInt
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
template<>
struct ConvertToInt< string >
{
	size_t operator()(const string&str)
	{
		size_t hash = 0;
		for (auto e : str)
		{
			hash *= 131;
			hash += e;
		}
		return hash;
	}
};
//改造哈希桶,将它封装成unordered_set和unordered_map
namespace hash_bucket
{
	//节点
	//不同于开放定址法,哈希桶放的是节点
	template<class T>
	struct HashNode
	{
		T _data;
		HashNode<T>* _next;

		HashNode(const T& data)
			:_data(data)
			, _next(nullptr)
		{}
	};

	//前置声明
	//哈希表定义在后,在迭代器使用哈希表指针,得前置声明
	//K是关键码的类型;T是存储元素的类型,如果是unordered_set,T就是K,如果是unordereed_map,T就是键值对;
	//KeyOfT是仿函数,可以提取T中的K;HashFunc是仿函数,可以将K转换成整形
	template<class K, class T, class KeyOfT, class HashFunc>
	class HashTable;

	//迭代器
	//为了实现简单,在哈希桶的迭代器类中需要用到HashTable本身
	template<class K, class T, class Ref, class Ptr, class KeyOfT, class HashFunc>
	struct HashTableIterator
	{
		typedef HashNode<T> Node;
		typedef HashTableIterator<K, T, Ref, Ptr, KeyOfT, HashFunc> Self;
		typedef HashTableIterator<K, T, T&, T*, KeyOfT, HashFunc> iterator;
		
		//当是const_iterator的构造,支持iterator转换成const_iterator
		//当是普通迭代器,这个函数是拷贝构造
		HashTableIterator(const iterator& it)
			:_node(it._node)
			, _pht(it._pht)
		{}


		Node* _node;
		//在迭代器中没有修改_pht的需要,所以可以是const类型
		const HashTable<K, T, KeyOfT, HashFunc>* _pht;//指向哈希表的指针

		HashTableIterator(Node* node,HashTable<K,T,KeyOfT,HashFunc>* pht)
			:_node(node)
			,_pht(pht)
		{}
		//重载一个构造,用来接受const修饰的HashTable
		HashTableIterator(Node* node, const HashTable<K, T, KeyOfT, HashFunc>* pht)
			:_node(node)
			, _pht(pht)//_pht也必须是const类型,否则权限就放大
		{}

		Ref operator*()
		{
			return _node->_data;
		}

		Ptr operator->()
		{
			return &(_node->_data);
		}

		Self& operator++()
		{
			//当前桶还没完
			if (_node->_next)
			{
				_node = _node->_next;
			}
			//当前桶已走到头,通过哈希表指针走到下一个桶
			else
			{
				KeyOfT kot;
				HashFunc hf;
				size_t hashi = hf(kot(_node->_data)) % _pht->_table.size();
				//从下一个位置开始查找一个不为空的桶
				++hashi;
				while (hashi < _pht->_table.size())
				{
					if (_pht->_table[hashi])
					{
						_node = _pht->_table[hashi];
						return *this;
					}
					++hashi;
				}
				_node = nullptr;
			}
			return *this;
		}

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

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

	//哈希表
	template<class K, class T, class KeyOfT, class HashFunc = ConvertToInt<K>>
	class HashTable
	{
	public:
		typedef HashNode<T> Node;
		typedef HashTableIterator<K, T, T&, T*, KeyOfT, HashFunc> iterator;
		typedef HashTableIterator<K, T, const T&, const T*, KeyOfT, HashFunc> const_iterator;


		// 友元声明
		template<class K, class T, class Ref, class Ptr, class KeyOfT, class HashFunc>
		friend struct HashTableIterator;

		iterator begin()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				if (_table[i])
				{
					return iterator(_table[i], this);
				}
			}
			return iterator(nullptr, this);
		}

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

		const_iterator begin()const
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				if (_table[i])
				{
					//const修饰的this指向的内容,传this过去
					//构造必须要必须用const修饰的HashTable接收
					return const_iterator(_table[i], this);
				}
			}
			return const_iterator(nullptr, this);
		}

		const_iterator end()const
		{
			return const_iterator(nullptr, this);
		}

		//构造
		HashTable()
		{
			//先开10个哈希桶
			_table.resize(10,nullptr);
		}
		
		//析构
		//析构函数需要自己定义,因为指针是内置类型,编译器不会调用其析构
		~HashTable()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				Node* cur = _table[i];
				Node* next = nullptr;
				while (cur)
				{
					next = cur->_next;
					delete cur;
					cur = next;
				}
				_table[i] = nullptr;
			}
		}

		//查找
		iterator Find(const K& key)
		{
			KeyOfT kot;
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();
			Node* cur = _table[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					return iterator(cur, this);
				}
				cur = cur->_next;
			}
			return iterator(nullptr, this);
		}

		//插入
		pair<iterator,bool> Insert(const T& data)
		{
			KeyOfT kot;
			iterator it = Find(kot(data));
			if (it!=end())
			{
				return make_pair(it,false);
			}

			HashFunc hf;

			//扩容
			//为什么还需要扩容?将元素挂在哈希桶不就可以了,不用考虑容量问题。
			//不扩容,不断插入节点,某些桶越来越长,和链表一样,查找效率就下降了。
			//负载因子可以适当放大,一般负载因子控制在1,平均下来每个桶一个元素,这样查找效率就很高。
			if (_n == _table.size())
			{
				vector<Node*> newtable;
				size_t newsize = _table.size() * 2;
				newtable.resize(newsize, nullptr);
				//遍历旧表,将旧表中的节点牵过来,挂在新表对应的位置
				for (size_t i = 0; i < _table.size(); i++)
				{
					Node* cur = _table[i];
					while (cur)
					{
						Node* next = cur->_next;
						//在新表中,插入位置可能改变
						size_t hashi = hf(kot(cur->_data)) % newsize;
						cur->_next = newtable[hashi];
						newtable[hashi] = cur;
						cur = next;
					}
					_table[i] = nullptr;
				}
				_table.swap(newtable);
			}

			size_t hashi = hf(kot(data)) % _table.size();
			//头插
			Node* newnode = new Node(data);
			newnode->_next = _table[hashi];
			_table[hashi] = newnode;
			++_n;
			return make_pair(iterator(newnode, this), true);
		}

		//删除
		bool Erase(const K& key)
		{
			KeyOfT kot;
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();
			Node* cur = _table[hashi];
			Node* prev = nullptr;
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					if (prev)
					{
						prev->_next = cur->_next;
					}
					else
					{
						_table[hashi] = cur->_next;
					}
					delete cur;
					return true;
				}
				prev = cur;
				cur = cur->_next;
			}
			return false;
		}

		//打印
		void Print()
		{
			KeyOfT kot;
			for (size_t i = 0; i < _table.size(); i++)
			{
				printf("[%d]->", i);
				Node* cur = _table[i];
				while (cur)
				{
					cout << cur->_data.first << ":" << cur->_data.second << "->";
					cur = cur->_next;
				}
				cout << "NULL" << endl;
			}
			cout << endl;
		}
	private:
		vector<Node*> _table;
		size_t _n;
	};
}

3.4.2 模拟实现unordered_set

#include"MyHashTable.h"
namespace zn
{
	template<class K>
	class unordered_set
	{
		struct SetKeyOfT
		{
			const K& operator()(const K& key)
			{
				return key;
			}
		};
	public:
		//unordered_set的key不能修改,所以普通迭代器的底层是const迭代器
		//+typename告诉编译器,这是内置类型,不是静态成员
		typedef typename hash_bucket::HashTable<K, K, SetKeyOfT>::const_iterator iterator;
		typedef typename hash_bucket::HashTable<K, K, SetKeyOfT>::const_iterator const_iterator;
		//调用const迭代器,得用const修饰的this调用
		iterator begin()const
		{
			return _ht.begin();
		}
		iterator end()const
		{
			return _ht.end();
		}
		//unordered_set的insert返回值中的iterator实际上是const_iterator
		pair<iterator,bool> insert(const K& key)
		{
			//HashTable的insert返回值中的iterator是普通迭代器
			//我们期望iterator转换成const_iterator,就得写一个const_iterator的构造
			pair<typename hash_bucket::HashTable<K, K, SetKeyOfT>::iterator, bool>ret = _ht.Insert(key);
			return pair<iterator, bool>(ret.first, ret.second);//将ret.first赋值给const_iterator,会调用const_iterator的构造
		}
	private:
		hash_bucket::HashTable<K, K, SetKeyOfT> _ht;
	};
}

3.4.3 模拟实现unordered_map

#include"MyHashTable.h"
namespace zn
{
	template<class K, class V>
	class unordered_map
	{
		struct MapKeyOfT
		{
			const K& operator()(const pair<const K,V>& kv)
			{
				return kv.first;
			}
		};

	public:
		typedef typename hash_bucket::HashTable<K, pair<const K,V>, MapKeyOfT>::iterator iterator;
		typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT>::const_iterator const_iterator;
		iterator begin()
		{
			return _ht.begin();
		}
		iterator end()
		{
			return _ht.end();
		}
		const_iterator begin()const
		{
			return _ht.begin();
		}
		const_iterator end()const
		{
			return _ht.end();
		}
		pair<iterator,bool> insert(pair<const K,V>& kv)
		{
			return _ht.Insert(kv);
		}
		V&operator[](const K& key)
		{
			pair<iterator, bool> ret = insert(make_pair(key, V()));
			return ret.first.second;
		}
	private:
		hash_bucket::HashTable<K, pair<const K,V>, MapKeyOfT> _ht;
	};
}

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

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

相关文章

Promise, async, await 学习

异步编程简介&#xff1a; 介绍&#xff1a;异步编程是一种编程范式&#xff0c;旨在提高程序的性能和响应能力。该模型允许程序在执行某些任务时&#xff0c;不必等待这些任务完成就可以进行下一步操作&#xff0c;从而提高了程序的效率。 作用&#xff1a;异步编程通常用于…

IPT2602协议-USB 快速充电端口控制器

产品描述&#xff1a; IPT2602是一款USB端口快速充电协议控制芯片。IPT2602智能识别多种快速充电协议&#xff0c;对手机等受电设备进行快速充电。IPT2602根据受电设备发送的电压请求能够精确的调整VBUS输出电压&#xff0c;从而实现快速充电。 IPT2602在调整5V输出电压前会自动…

10.5 认识XEDParse汇编引擎

XEDParse 是一款开源的x86指令编码库&#xff0c;该库用于将MASM语法的汇编指令级转换为对等的机器码&#xff0c;并以XED格式输出&#xff0c;目前该库支持x86、x64平台下的汇编编码&#xff0c;XEDParse的特点是高效、准确、易于使用&#xff0c;它可以良好地处理各种类型的指…

[硬件基础]-快速了解RS232串行通信

快速了解RS232串行通信 文章目录 快速了解RS232串行通信1、概述2、什么是串行数据通信&#xff1f;3、什么是RS232&#xff1f;4、RS232应用5、RS232如何工作&#xff1f;6、RS232协议基础6.1 电压与逻辑表示6.2 数据编码6.3 起始位和停止位6.4 奇偶校验位6.5 波特率6.5 RS232电…

掌握 SwiftUI 中的 ScrollView

文章目录 前言scrollTransition 修饰符ScrollTransitionPhase弹性动画总结 前言 SwiftUI 框架的第五个版本引入了许多与 ScrollView 相关的新 API&#xff0c;使其比以前更强大。本周将开始介绍 ScrollView 在 SwiftUI 中的新功能系列文章&#xff0c;首先我们将讨论滚动过渡。…

【LeetCode: 918. 环形子数组的最大和 | 动态规划】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

关联规则挖掘(上):数据分析 | 数据挖掘 | 十大算法之一

⭐️⭐️⭐️⭐️⭐️欢迎来到我的博客⭐️⭐️⭐️⭐️⭐️ &#x1f434;作者&#xff1a;秋无之地 &#x1f434;简介&#xff1a;CSDN爬虫、后端、大数据领域创作者。目前从事python爬虫、后端和大数据等相关工作&#xff0c;主要擅长领域有&#xff1a;爬虫、后端、大数据…

【全方位带你配置yolo开发环境】快速上手yolov5

本文用于记录yolo开发环境的配置&#xff0c;以及我在配置中出现的各种问题&#xff0c;以供大伙参考。&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; 本人持续分享更多关于电子通信专业内容以及嵌入式和单片机的知识…

阿里云服务器ECS详细介绍_云主机_服务器托管_弹性计算

阿里云服务器ECS英文全程Elastic Compute Service&#xff0c;云服务器ECS是一种安全可靠、弹性可伸缩的云计算服务&#xff0c;阿里云提供多种云服务器ECS实例规格&#xff0c;如经济型e实例、通用算力型u1、ECS计算型c7、通用型g7、GPU实例等&#xff0c;阿里云服务器网分享阿…

使用css制作3D盒子,目的是把盒子并列制作成3D货架

1. 首先看效果&#xff08;第一个五颜六色的是透明多个面&#xff0c;第2-3都是只有3个面是我实际需要的&#xff0c;右边的有3个并列的正方体与3个并列的长方体&#xff09;&#xff1a; 长方体与正方体&#xff0c;所有代码&#xff1a; <!DOCTYPE html> <html lan…

汽车网络安全--安全芯片应用场景解析

​在聊汽车网络安全时,最先想到的就是使用芯片内置HSM,比如说英飞凌TC2xx系列的HSM、瑞萨RH850的ICU、NXP的HSE等等;实际上除了内置HSM,还有外置HSM(通过UART、SPI等通信)、安全存储芯片等等。而这些芯片统称为安全芯片。 安全芯片的主要作用是为整个系统建立起一个可信的…

【Java】类和接口的区别

1. 类和类的继承关系&#xff08;一个类只能单继承一个父类&#xff0c;不能继承n多个不同的父类&#xff09; 继承关系&#xff0c;只能单继承&#xff0c;但可以多层继承 2. 类和接口的实现关系&#xff08;一个类可以实现n多个不同的接口&#xff09; 实现关系&#xff0c;可…

【QT开发笔记-基础篇】| 第四章 事件QEvent | 4.4 鼠标按下、移动、释放事件

本章要实现的整体效果如下&#xff1a; QEvent::MouseButtonPress ​ 鼠标按下时&#xff0c;触发该事件&#xff0c;它对应的子类是 QMouseEvent QEvent::MouseMove ​ 鼠标移动时&#xff0c;触发该事件&#xff0c;它对应的子类是 QMouseEvent QEvent::MouseButtonRel…

golang gin框架1——简单案例以及api版本控制

gin框架 gin是golang的一个后台WEB框架 简单案例 package mainimport ("github.com/gin-gonic/gin""net/http" )func main() {r : gin.Default()r.GET("/ping", func(c *gin.Context) {//以json形式输出&#xff0c;还可以xml protobufc.JSON…

网络安全黑客究竟是什么?

“网络安全”是指任何活动旨在保护您的网络和数据的可用性和完整性。它包括硬件和软件技术。有效的网络安全管理对网络的访问。它针对的是一种不同的威胁,阻止他们进入或在您的网络传播。 网络安全是如何工作的呢? 网络安全结合多层防御的优势和网络。每个网络安全层实现政策…

前端TypeScript学习day01-TS介绍与TS常用类型

(创作不易&#xff0c;感谢有你&#xff0c;你的支持&#xff0c;就是我前行的最大动力&#xff0c;如果看完对你有帮助&#xff0c;请留下您的足迹&#xff09; 目录 TypeScript 介绍 TypeScript 是什么 TypeScript 为什么要为 JS 添加类型支持&#xff1f; TypeScript 相…

【Redis】基础数据结构-quicklist

Redis List 在Redis3.2版之前&#xff0c;Redis使用压缩列表和双向链表作为List的底层实现。当元素个数比较少并且元素长度比较小时&#xff0c;Redis使用压缩列表实现&#xff0c;否则Redis使用双向链表实现。 ziplist存在问题 不能保存过多的元素&#xff0c;否则查找复杂度…

vue-devtools插件安装

拓展程序连接 链接&#xff1a;https://pan.baidu.com/s/1tEyZJUCEK_PHPGhU_cu_MQ?pwdr2cj 提取码&#xff1a;r2cj 一、打开谷歌浏览器&#xff0c;点击扩展程序-管理扩展程序 二、打开开发者模式&#xff0c;将vue-devtools.crx 拖入页面&#xff0c;点击添加扩展程序 成…

三、【色彩模式与颜色填充】

文章目录 Photoshop常用的几种颜色模式包括&#xff1a;1. RGB模式2. CMYK模式3. 灰度模式4. LAB模式5. 多通道模式 Photoshop颜色填充1.色彩基础2.拾色器认识3.颜色填充最后附上流程图&#xff1a; Photoshop常用的几种颜色模式包括&#xff1a; 1. RGB模式 详细可参考&…

mysql-sql执行流程

sql执行流程 MYSQL 中的执行流程 MYSQL 中的执行流程 sql 执行流程如下图