unordered-------Hash

news2024/11/18 9:22:18

✅<1>主页:我的代码爱吃辣
📃<2>知识讲解:数据结构——哈希表
☂️<3>开发环境:Visual Studio 2022
💬<4>前言:哈希是一种映射的思想,哈希表即使利用这种思想,在查找上进行很少的比较次数就能够将元素找到,非常的高效,在一定程度上,效率比红黑树还要强,因此在C++11中,STL又提供了4个unordered系列的关联式容器,他们的底层就是哈希。

目录

一.unordered系列关联式容器

1. unordered_map

1.1 unordered_map的构造

1.2unordered_map的容量

 1.3unordered_map的迭代器

1.4 unordered_map的元素访问

1.5unordered_map的查询 

1.6unordered_map的修改操作

 1.2unordered_set

二.哈希

1.哈希概念

2. 哈希冲突

3.哈希冲突解决

三.实现闭散列除留余数法+线性探测

1.整体结构

2.插入

3.查询

4.删除

四.开散列

1.开散列实现

2.插入

3.查询

4.删除

5.析构函数

五.完整代码即测试


 

一.unordered系列关联式容器

在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 log{_{2}}^{N},即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好
的查询是,进行很少的比较次数就能够将元素找到,因此在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同,本文中只对unordered_map和unordered_set进行介绍,unordered_multimap和unordered_multiset学生可查看文档介绍。

1. unordered_map

reference-------unordered_map

  1. unordered_map是存储<key, value>键值对的关联式容器,其允许通过keys快速的索引到与其对应的value。
  2. 在unordered_map中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此键关联。键和映射值的类型可能不同。
  3. 在内部,unordered_map没有对<kye, value>按照任何特定的顺序排序, 为了能在常数范围内找到key所对应的value,unordered_map将相同哈希值的键值对放在相同的桶中。
  4. unordered_map容器通过key访问单个元素要比map快,但它通常在遍历元素子集的范围迭代方面效率较低。
  5. unordered_maps实现了直接访问操作符(operator[]),它允许使用key作为参数直接访问value。
  6. 它的迭代器至少是前向迭代器。

1.1 unordered_map的构造

函数声明功能介绍
unordered_map构造不同格式的unordered_map对象

1.2unordered_map的容量

 

函数声明功能介绍
bool empty() const检测unordered_map是否为空
size_t size() const获取unordered_map的有效元素个数

 1.3unordered_map的迭代器

函数声明功能介绍
begin返回unordered_map第一个元素的迭代器
end返回unordered_map最后一个元素下一个位置的迭代器
cbegin返回unordered_map第一个元素的const迭代器
cend返回unordered_map最后一个元素下一个位置的const迭代器

1.4 unordered_map的元素访问

函数声明功能介绍
operator[]返回与key对应的value,没有一个默认值

注意:该函数中实际调用哈希桶的插入操作,用参数key与V()构造一个默认值往底层哈希桶
中插入,如果key不在哈希桶中,插入成功,返回V(),插入失败,说明key已经在哈希桶中,
将key对应的value返回。 

1.5unordered_map的查询 

函数声明功能介绍
iterator find(const K& key)返回key在哈希桶中的位置
size_t count(const K& key)返回哈希桶中关键码为key的键值对的个数

1.6unordered_map的修改操作

函数声明功能介绍
insert向容器中插入键值对
erase删除容器中的键值对
void clear()清空容器中有效元素个数
void swap(unordered_map&)交换两个容器中的元素

 1.2unordered_set

reference-------unordered_set

二.哈希

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

1.哈希概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素
时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即
O(log{_{2}}^{N}),搜索的效率取决于搜索过程中元素的比较次数。

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素
如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立
一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

当向该结构中:

  • 插入元素

根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。

  • 搜索元素

对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置
取元素比较,若关键码相等,则搜索成功。

 该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称
为哈希表(Hash Table)(或者称散列表)。

例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。

 用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快
问题:按照上述哈希方式,向集合中插入元素44,会出现什么问题?发生位置冲突。

2. 哈希冲突

对于两个数据元素的关键字k1和k2,有k1 != k2,但有:Hash(k1) ==
Hash(k2),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突
或哈希碰撞。

把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。

哈希碰撞的产生一部分原因是,哈希函数设计的不够合理。

哈希函数设计原则:

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
  • 哈希函数计算出来的地址能均匀分布在整个空间中
  • 哈希函数应该比较简单

 常见哈希函数:

1.直接定址法--(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
优点:简单、均匀。
缺点:需要事先知道关键字的分布情况。
使用场景:适合查找比较小且连续的情况。

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


3. 平方取中法--(了解)
假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;
再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址
平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况。


4. 折叠法--(了解)
折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这
几部分叠加求和,并按散列表表长,取后几位作为散列地址。
折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况。


5. 随机数法--(了解)
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中
random为随机数函数。通常应用于关键字长度不等时采用此法。

6. 数学分析法--(了解)
设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定
相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只
有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散
列地址。例如:

 假设要存储某家公司员工登记表,如果用手机号作为关键字,那么极有可能前7位都是 相同
的,那么我们可以选择后面的四位作为散列地址,如果这样的抽取工作还容易出现 冲突,还
可以对抽取出来的数字进行反转(如1234改成4321)、右环位移(如1234改成4123)、左环移
位、前两数与后两数叠加(如1234改成12+34=46)等方法。

数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的
若干位分布较均匀的情况。

注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突。

3.哈希冲突解决

解决哈希冲突两种常见的方法是:闭散列和开散列。

闭散列:

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有
空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。
那如何寻找下一个空位置
呢?

1. 线性探测

比如下面中的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4,
因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

  •  插入

通过哈希函数获取待插入元素在哈希表中的位置如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素。

  • 删除

采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素
会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影
响。因此线性探测采用标记的伪删除法来删除一个元素。

// 哈希表每个空间给个标记
// EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
enum State{EMPTY, EXIST, DELETE};

三.实现闭散列除留余数法+线性探测

1.整体结构

	
    //状态  
    enum State{	EXIST,EMPTY,DELETE	};

    //数据结点
	template<class K, class V>
	struct HashDate 
	{
		HashDate()
		{
		}
		HashDate(pair<K,V> kv)
			:_kv(kv)
		{}

		pair<K, V> _kv;
		State state = EMPTY;
	};
    
    //开放定址法哈希表
    template<class K, class V>
	class HashTable
	{
		typedef HashDate<K, V> Date;
	public:
        //插入
		bool insert(pair<K, V> kv)
		{
			
		}
        
        //删除
		bool erase(const K& key)
		{
			
		}
        //查询
		pair<int,bool> find(const K& key)
		{
			
		}

	private:
		vector<Date> _sh;    //数据存储
		size_t _n = 0;       //存储数据的个数
	};
}

2.插入

插入的步骤:

  1. 计算插入位置
  2. 线性探测
        bool insert(pair<K, V> kv)
		{

			//开放定值法,线性探测解决冲突
            //1.计算插入位置
			int hashi = kv.first % _sh.size();
			int i = 1; int hashn = hashi;
            //r如果插入的位置已经有数据了,进行线性探测
			while (_sh[hashn].state == EXIST)
			{
				hashn = hashi + i;
				hashn %= _sh.size();
				i++;
			}
            //探测空位置,插入数据
			_sh[hashn] = Date(kv);
            //修改状态
			_sh[hashn].state = EXIST;
			_n++;
			return true;
		}

注意:在计算插入位置时,我们楚留余数法,使用的是表的size,而不是capacity,是因为我们真正合法插入的位置,是size控制的。

扩容:

哈希表什么情况下进行扩容?如何扩容?

这里引入一个新的概念:负载因子

 我们约定当负载因子达到0.7时就扩容,扩容的步骤:

    bool insert(pair<K, V> kv)
		{
			//如果插入的值已经存在
			if (find(kv.first).second)
			{
				return false;
			}

			//扩容
			//负载因子为0.7
			if (_sh.size() == 0 || _n * 10 / _sh.size() == 7)
			{
				//1.确定新的容量
				int newsize = _sh.size() == 0 ? 10 : _sh.size() * 2;
				//每一次扩容都要重新插入数据
				//2.创建新的哈希表
				HashTable<K, V> newHash;
				//3.将原表数据插入进去,复用已经实现的逻辑
				newHash._sh.resize(newsize);
				for (auto e : _sh)
				{
					newHash.insert(e._kv);
				}
				//最后将新表与旧表交换即可
				_sh.swap(newHash._sh);
			}

			//开放定值法,线性探测解决冲突
			int hashi = kv.first % _sh.size();
			int i = 1; int hashn = hashi;
			while (_sh[hashn].state == EXIST)
			{
				hashn = hashi + i;
				hashn %= _sh.size();
				i++;
			}
			_sh[hashn] = Date(kv);
			_sh[hashn].state = EXIST;
			_n++;
			return true;
		}

3.查询

查询步骤:

  1. 判空
  2. 探测查找
pair<K,bool> find(const K& key)
		{
			//如果表中是空的,返回坐标,和插入false
			if (_sh.empty())
			{
				return pair<K, bool>(-1, false);
			}
			//计算位置
			size_t hashi = key % _sh.size();
			int i = 1; int hashn = hashi;
			//线性探测直到遇到空
			while (_sh[hashn].state!=EMPTY)
			{
				if (_sh[hashn].state == EXIST && _sh[hashn]._kv.first == key)
				{
					//如果探测到,存在且键值相等,就返回该键值和ture
					return pair<K, bool>(hashn,true);
				}
				hashn = hashi + i;
				hashn %= _sh.size();
				i++;
				if (hashn == hashi)
				{
					//已经探测一圈回到了原点,即没找到
					return pair<K, bool>(-1, false);
				}
			}
			//遇到空没找到
			return pair<K, bool>(-1, false);
		}

4.删除

	    //删除
        bool erase(const K& key)
		{
			//先查询,存在就删除,不存在直接返回false
			pair<int, bool> retfind = find(key);
			if (!retfind.second)
			{
				return false;
			}
            //删除后修改状态
			_sh[retfind.first].state = DELETE;
			return true;
		}

线性探测优点:实现非常简单。
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同
关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降
低。
如何缓解呢?

四.开散列

开散列概念:

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地
址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链
接起来,各链表的头结点存储在哈希表中。

 从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。

1.开散列实现

 开散列的结点和结构:

    template<class K, class V>
	struct HashDate
	{
		HashDate(pair<K, V> kv)
			:_kv(kv),
			_next(nullptr)
		{}

		pair<K, V> _kv;   //键值对
		HashDate* _next;  //下一个结点的指针
	};

	template<class K,class V>
	class Hashbucket
	{
		typedef HashDate<K, V> Date;
		typedef pair<K, V> KV;
	public:

		bool insert(KV kv)
		{
			//...
		}

		Date* find(const K& key)
		{
			//....
		}

		bool erase(const K& key)
		{
			//...
		}
		~Hashbucket()
		{
			//....
		}

	private:
		vector<Date*> _table;  //数据存储
		size_t _n = 0;         //数据存储个数
	};
}

2.插入

因为_table的表中存储都是每个结点的指针,每一个桶实际上就是一个链表,所以对哈希的插入实际就是对链表的头插。

bool insert(KV kv)
{

	//插入
    //计算桶的位置
	size_t hashi = kv.first % _table.size();
    //创建结点
	Date* newNode = new Date(kv);
     //新节点的next指向头节点
	newNode->_next = _table[hashi];
     //新插入的结点变成新的头
	_table[hashi] = newNode;
	_n++;
}

扩容:

约定当数据个数达到桶的个数时,进行扩容:

扩容步骤:

bool insert(KV kv)
		{
			//如果待插入的数据已经存在
			if ( find(kv.first))
			{
				return false;
			}

			//扩容
			if (_n == _table.size())
			{
				//计算新的桶数
				size_t newsize = _table.size() == 0 ? 10 : _table.size() * 2;
				//创建一个新的表
				vector<Date*> newtable;
				newtable.resize(newsize);
				//将就表中的结点拿过来,注意此处直接拿旧的结点插入新表,
				//而不是拿到数据后创建新的结点,减少不必要的消耗
				for (auto& cur : _table)
				{
					while (cur)
					{
						size_t hashi = cur->_kv.first % newtable.size();
						Date* next = cur->_next;
						cur->_next = newtable[hashi];
						newtable[hashi] = cur;
						cur = next;
					}
				}
				//新旧表交换
				_table.swap(newtable);
			}

			//插入
			size_t hashi = kv.first % _table.size();
			Date* newNode = new Date(kv);
			newNode->_next = _table[hashi];
			_table[hashi] = newNode;
			_n++;
		}

3.查询

查询步骤:

  1. 找到桶
  2. 遍历桶中数据

找到了返回结点指针,没找到返回nullptr

		Date* find(const K& key)
		{
			//如果表是空的,即就是没有一个数据
			if (_table.empty())
			{
				return nullptr;
			}
			//1.计算桶的位置
			size_t hashi = key % _table.size();
			Date* cur = _table[hashi];
			//2.在桶里面遍历查询
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					return cur;
				}
				cur = cur->_next;
			}
			return nullptr;
		}

4.删除

删除归根结底还是链表的删除:

删除的步骤:

  1. 判断在不在表中
  2. 计算出桶的位置
  3. 找打待删除结点,并记录其前一个结点
  4. 改变指向,释放结点

 

    bool erase(const K& key)
		{
			//1.查找在不在表中
			if (!find(key))
			{
				return false;
			}
			//2.计算桶的位置
			size_t hashi = key % _table.size();
			Date* cur = _table[hashi];
			//记录前驱结点
			Date* prov = nullptr;
			//3.找到待删除的结点
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (prov == nullptr)
					{
						_table[hashi] = cur->_next;	
					}
					else
					{
						prov->_next = cur->_next;
					}
					//4.删除改变指向,释放结点
					delete cur;
					return true;
				}
				else
				{
					prov = cur;
					cur = cur->_next;
				}
			}
		}

5.析构函数

遍历表中每一个桶,并对每一个桶进行释放,对桶的销毁,就是对链表的销毁。

		~Hashbucket()
		{
			for (auto& cur : _table)
			{
				while (cur)
				{
					Date* next = cur->_next;
					delete cur;
					cur = next;
				}
				cur = nullptr;
			}
		}

五.完整代码即测试

#pragma once
#include<vector>
#include<iostream>
using namespace std;

namespace HashOpenAdress
{
	enum State
	{
		EXIST,
		EMPTY,
		DELETE
	};

	template<class K, class V>
	struct HashDate
	{
		HashDate()
		{
		}
		HashDate(pair<K,V> kv)
			:_kv(kv)
		{}

		pair<K, V> _kv;
		State state = EMPTY;
	};

	template<class K, class V>
	class HashTable
	{
		typedef HashDate<K, V> Date;
	public:
		bool insert(pair<K, V> kv)
		{
			//如果插入的值已经存在
			if (find(kv.first).second)
			{
				return false;
			}

			//扩容
			//负载因子为0.7
			if (_sh.size() == 0 || _n * 10 / _sh.size() == 7)
			{
				//确定新的容量
				int newsize = _sh.size() == 0 ? 10 : _sh.size() * 2;
				//每一次扩容都要重新插入数据
				HashTable<K, V> newHash;
				newHash._sh.resize(newsize);
				for (auto e : _sh)
				{
					newHash.insert(e._kv);
				}
				_sh.swap(newHash._sh);
			}

			//开放定值法,线性探测解决冲突
			int hashi = kv.first % _sh.size();
			int i = 1; int hashn = hashi;
			while (_sh[hashn].state == EXIST)
			{
				hashn = hashi + i;
				hashn %= _sh.size();
				i++;
			}
			_sh[hashn] = Date(kv);
			_sh[hashn].state = EXIST;
			_n++;
			return true;
		}

		bool erase(const K& key)
		{
			pair<int, bool> retfind = find(key);
			if (!retfind.second)
			{
				return false;
			}
			_sh[retfind.first].state = DELETE;
			return true;
		}

		pair<int,bool> find(const K& key)
		{
			if (_sh.empty())
			{
				return pair<int, bool>(-1, false);
			}
			size_t hashi = key % _sh.size();
			int i = 1; int hashn = hashi;
			while (_sh[hashn].state!=EMPTY)
			{
				if (_sh[hashn].state == EXIST && _sh[hashn]._kv.first == key)
				{
					return pair<int,bool>(hashn,true);
				}
				hashn = hashi + i;
				hashn %= _sh.size();
				i++;
				if (hashn == hashi)
				{
					return pair<int, bool>(-1, false);
				}
			}
			return pair<int, bool>(-1, false);
		}

	private:
		vector<Date> _sh; //数据存储
		size_t _n = 0;       //存储数据的个数
	};
}


namespace HashBucket
{
	template<class K, class V>
	struct HashDate
	{
		HashDate(pair<K, V> kv)
			:_kv(kv),
			_next(nullptr)
		{}

		pair<K, V> _kv;
		HashDate* _next;
	};

	template<class K,class V>
	class Hashbucket
	{
		typedef HashDate<K, V> Date;
		typedef pair<K, V> KV;
	public:

		bool insert(KV kv)
		{
			//如果待插入的数据已经存在
			if ( find(kv.first))
			{
				return false;
			}

			//扩容
			if (_n == _table.size())
			{
				//计算新的桶数
				size_t newsize = _table.size() == 0 ? 10 : _table.size() * 2;
				//创建一个新的表
				vector<Date*> newtable;
				newtable.resize(newsize);
				//将就表中的结点拿过来,注意此处直接拿旧的结点插入新表,
				//而不是拿到数据后创建新的结点,减少不必要的消耗
				for (auto& cur : _table)
				{
					while (cur)
					{
						size_t hashi = cur->_kv.first % newtable.size();
						Date* next = cur->_next;
						cur->_next = newtable[hashi];
						newtable[hashi] = cur;
						cur = next;
					}
				}
				//新旧表交换
				_table.swap(newtable);
			}

			//插入
			size_t hashi = kv.first % _table.size();
			Date* newNode = new Date(kv);
			newNode->_next = _table[hashi];
			_table[hashi] = newNode;
			_n++;
		}

		Date* find(const K& key)
		{
			//如果表是空的,即就是没有一个数据
			if (_table.empty())
			{
				return nullptr;
			}
			//1.计算桶的位置
			size_t hashi = key % _table.size();
			Date* cur = _table[hashi];
			//2.在桶里面遍历查询
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					return cur;
				}
				cur = cur->_next;
			}
			return nullptr;
		}

		bool erase(const K& key)
		{
			//1.查找在不在表中
			if (!find(key))
			{
				return false;
			}
			//2.计算桶的位置
			size_t hashi = key % _table.size();
			Date* cur = _table[hashi];
			//记录前驱结点
			Date* prov = nullptr;
			//3.找到待删除的结点
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (prov == nullptr)
					{
						_table[hashi] = cur->_next;	
					}
					else
					{
						prov->_next = cur->_next;
					}
					//4.删除改变指向,释放结点
					delete cur;
					return true;
				}
				else
				{
					prov = cur;
					cur = cur->_next;
				}
			}
		}
		~Hashbucket()
		{
			for (auto& cur : _table)
			{
				while (cur)
				{
					Date* next = cur->_next;
					delete cur;
					cur = next;
				}
				cur = nullptr;
			}
		}

	private:
		vector<Date*> _table;
		size_t _n = 0;
	};
}

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

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

相关文章

基于野马算法优化的BP神经网络(预测应用) - 附代码

基于野马算法优化的BP神经网络&#xff08;预测应用&#xff09; - 附代码 文章目录 基于野马算法优化的BP神经网络&#xff08;预测应用&#xff09; - 附代码1.数据介绍2.野马优化BP神经网络2.1 BP神经网络参数设置2.2 野马算法应用 4.测试结果&#xff1a;5.Matlab代码 摘要…

【深入浅出设计模式--状态模式】

深入浅出设计模式--状态模式 一、背景二、问题三、解决方案四、 适用场景总结五、后记 一、背景 状态模式是一种行为设计模式&#xff0c;让你能在一个对象的内部状态变化时改变其行为&#xff0c;使其看上去就像改变了自身所属的类一样。其与有限状态机的概念紧密相关&#x…

RT-Thread在STM32硬件I2C的踩坑记录

RT-Thread在STM32硬件I2C的踩坑记录 0.前言一、软硬件I2C区别二、RT Thread中的I2C驱动三、尝试适配硬件I2C四、i2c-bit-ops操作函数替换五、Attention Please!六、总结 参考文章&#xff1a; 1.将硬件I2C巧妙地将“嫁接”到RTT原生的模拟I2C驱动框架 2.基于STM32F4平台的硬件I…

flutter 上传图片并裁剪

1.首先在pubspec.yaml文件中新增依赖pub.dev image_picker: ^0.8.75 image_cropper: ^4.0.1 2.在Android的AndroidManifest.xml文件里面添加权限 <activityandroid:name"com.yalantis.ucrop.UCropActivity"android:screenOrientation"portrait"andro…

Golang网络编程

互联网协议介绍引入 1. 物理层&#xff08;Physical Layer&#xff09;&#xff1a; - 功能&#xff1a;物理层负责定义物理介质传输数据的方式和规范&#xff0c;它传输的是原始数据比特流。 - 协议&#xff1a;Ethernet、Wi-Fi、USB、光纤等。 - 例子&#xff1a;将…

[javaWeb]Socket网络编程

网络编程&#xff1a;写一个应用程序,让这个程序可以使用网络通信。这里就需要调用传输层提供的 api。 Socket套接字 传输层提供协议&#xff0c;主要是两个: UDP和TCP 提供了两套不同的 api&#xff0c;这api也叫做socket api。 UDP和 TCP 特点对比&#xff1a; UDP: 无连…

数据结构day05(单链表)

今日任务&#xff1a; 思维导图&#xff1a; 实现 代码&#xff1a;&#xff08;多文件&#xff09; head.h #ifndef __HEAD_H__ #define __HEAD_H__#include <stdio.h> #include <string.h> #include <stdlib.h> typedef int datatype;typedef struct Lin…

Nuxt3 与 Vue3 的 Server api 全栈开发之路

Nuxt Server Api 阅读时长&#xff1a;15分钟 本文内容&#xff1a; 国内关于Nuxt3的资料太少了&#xff0c;而Nuxt3又发布了没有多久&#xff0c;导致资料太少。本文浓缩讲解了&#xff0c;对于一个前端开发&#xff0c;上手使用 Nuxt3&#xff0c;并一个人承担前后端开发的所…

Hive3第六章:更换引擎

系列文章目录 Hive3第一章&#xff1a;环境安装 Hive3第二章&#xff1a;简单交互 Hive3第三章&#xff1a;DML数据操作 Hive3第三章&#xff1a;DML数据操作(二) Hive3第四章&#xff1a;分区表和分桶表 Hive3第五章&#xff1a;函数 Hive3第六章&#xff1a;更换引擎 文章目…

网络安全—0基础学习笔记(黑客)

一、前言 1.这是一条坚持的道路,三分钟的热情可以放弃往下看了. 2.多练多想,不要离开了教程什么都不会了.最好看完教程自己独立完成技术方面的开发. 3.有时多 google,baidu,我们往往都遇不到好心的大神,谁会无聊天天给你做解答. 4.遇到实在搞不懂的,可以先放放,以后再来解决. …

2023年智慧政务一网通办云平台顶层设计与建设方案PPT

导读&#xff1a;原文《2023年智慧政务一网通办云平台顶层设计与建设方案PPT》&#xff08;获取来源见文尾&#xff09;&#xff0c;本文精选其中精华及架构部分&#xff0c;逻辑清晰、内容完整&#xff0c;为快速形成售前方案提供参考。 部分内容&#xff1a; 喜欢文章&#…

Spring——RESTful Web服务

文章目录 RESTful Web 服务介绍内容概览下载 Lombok 优化代码利器RESTful Web 服务开发运行项目并测试效果 RESTful Web 服务介绍 本节我们将开发一个简单的 RESTful Web 服务。 RESTful Web 服务与传统的 MVC 开发一个关键区别是返回给客户端的内容的创建方式&#xff1a;传…

Go操作各大消息队列教程(RabbitMQ、Kafka)

Go操作各大消息队列教程 1 RabbitMQ 1.1 概念 ①基本名词 当前市面上mq的产品很多&#xff0c;比如RabbitMQ、Kafka、ActiveMQ、ZeroMQ和阿里巴巴捐献给Apache的RocketMQ。甚至连redis这种NoSQL都支持MQ的功能。 Broker&#xff1a;表示消息队列服务实体Virtual Host&#x…

感觉车载测试的这一波敏捷风快过去了

敏捷&#xff0c;算不得汽车行业的原生产物&#xff0c;几年前&#xff0c;耳边很少听到这个字眼&#xff0c;基本算是在近几年传统汽车行业开始衰落的大背景下&#xff0c;而后伴随着软件从互联网等行业传进来的。 这两年&#xff0c;大家开始把敏捷谈得风生水起&#xff0c;…

用idea查看sqlite数据库idea sqlite

1、安装Database Navigator插件 2、导入数据库并查看 3、删除数据库连接 在此做个笔记

VUE环境下 CSS3+JS 实现发牌 翻牌

创建牌容器&#xff08;关键点&#xff1a;overflow&#xff1a;hidden&#xff09;&#xff1a; <div class"popup-box"></div> .popup-box {position: absolute;width: 100vw;height: 100vh;top: 0px;left: 0;overflow: hidden; } 创建每一张牌《固…

python+TensorFlow实现人脸识别智能小程序的项目(包含TensorFlow版本与Pytorch版本)(一)

pythonTensorFlow实现人脸识别智能小程序的项目&#xff08;包含TensorFlow版本与Pytorch版本&#xff09;&#xff08;一&#xff09; 一&#xff1a;TensorFlow基础知识内容部分&#xff08;简明扼要&#xff0c;快速适应&#xff09;1、下载Cifar10数据集&#xff0c;并进行…

react17:生命周期函数

挂载时更新时 setState触发更新、父组件重新渲染时触发更新forceUpdate触发更新卸载时 react&#xff08;v17.0.2&#xff09;的生命周期图谱如下。 相较于16版本&#xff0c;17版本生命周期函数有如下变化&#xff1a; componentWillMount() componentWillUpdate() compone…

mac电脑屏幕录制Berrycast Mac屏幕录制软件

Berrycast是一款为Mac设计的优秀屏幕录制软件&#xff0c;它让屏幕录制变得简单而高效。以下是Berrycast的一些主要特点&#xff1a; 简单的用户界面&#xff1a;Berrycast拥有直观和简洁的用户界面&#xff0c;使得用户可以轻松上手。高质量的视频输出&#xff1a;Berrycast能…

电商系统架构设计系列(十):怎么能避免写出慢SQL?

上篇文章中&#xff0c;我给你留了一个思考题&#xff1a;怎么能避免写出慢SQL&#xff1f; 我们知道&#xff0c;一个慢 SQL 就可以直接让 MySQL 瘫痪。以我个人经验总结来看&#xff0c;一般情况下系统出问题&#xff0c;大多数都是因为SQL语句的问题。掌握和用好了SQL&…