C++ —— 哈希详解 - 开散列与闭散列

news2024/12/29 1:41:42

目录

1. 哈希的概念

1.1 直接定址法

1.2 哈希冲突 

1.3 负载因子

1.4 哈希函数

 1.4.1 除法散列法/除留余数法 

 1.4.2 乘法散列法

 1.4.3 全域散列法

1.5 处理哈希冲突

1.5.1 开放定址法(闭散列)

1. 线性探测(挨着查找)

2. 二次探测(跳跃着查找)

3. 双重散列

2. 闭散列实现哈希表

2.1 开发地址法的基础构架

2.2 扩容

2.3 插入

2.4 查找

2.5 删除

2.6 闭散列代码

3. key不能取模的问题

4. 链地址法(开散列/哈希桶)

4.1 链地址法的基础框架

4.2 插入

4.3 扩容

4.4 查找

4.5 删除

4.6 开散列代码


1. 哈希的概念

哈希(hash)⼜称散列,是⼀种组织数据的⽅式。从译名来看,有散乱排列的意思。本质就是通过哈希函数把关键字Key跟存储位置建⽴⼀个映射关系,查找时通过这个哈希函数计算出Key存储的位置,进⾏快速查找


1.1 直接定址法

当关键字的范围⽐较集中时,直接定址法就是⾮常简单⾼效的⽅法,⽐如⼀组关键字都在[0,99]之间,那么我们开⼀个100个数的数组,每个关键字的值直接就是存储位置的下标。再⽐如⼀组关键字值都在[a,z]的⼩写字⺟,那么我们开⼀个26个数的数组,每个关键字acsii码-a ascii码就是存储位置的下标

    
也就是说直接定址法本质就是⽤关键字计算出⼀个绝对位置或者相对位置

直接定址法的缺点也⾮常明显:当关键字的范围⽐较分散时,就很浪费内存甚⾄内存不够⽤

   

假设我们只有数据范围是[0, 9999]的N个值,我们要映射到⼀个M个空间的数组中(⼀般情况下M >= N),那么就要借助哈希函数(hash function)hf,关键字key被放到数组的h(key)位置,这⾥要注意的是h(key)计算出的值必须在[0, M)之间 


1.2 哈希冲突 

这⾥存在的⼀个问题就是,两个不同的key可能会映射到同⼀个位置去,这种问题我们叫做哈希冲突,或者哈希碰撞

    

理想情况是找出⼀个好的哈希函数避免冲突,但是实际场景中,冲突是不可避免的,所以我们尽可能设计出优秀的哈希函数,减少冲突的次数,同时也要去设计出解决冲突的⽅案 


1.3 负载因子

假设哈希表中已经映射存储了N个值,哈希表的⼤⼩为M,那么 负载因⼦ = N/M(M分之N),负载因⼦越⼤,哈希冲突的概率越⾼,空间利⽤率越⾼;负载因⼦越⼩,哈希冲突的概率越低,空间利⽤率越低

   

负载因子的大小最好是<=0.7


1.4 哈希函数

⼀个好的哈希函数应该让N个关键字被等概率的均匀的散列分布到哈希表的M个空间中,但是实际中却很难做到,但是我们要尽量往这个⽅向去考量设计

 1.4.1 除法散列法/除留余数法 

1. 除法散列法也叫做除留余数法,顾名思义,假设哈希表的空间大小为M,那么通过Key%M

   

key(数据个数)除以M(表的空间大小)得到的余数作为映射位置的下标

   

也就是哈希函数为:h(key) = key % M

    
2. 当使⽤除法散列法时,要尽量避免M为某些值,如2的冥,10的冥等

   

如果是 2X ,那么key %本质相当于保留key的后X位,那么后x位相同的值,计算出的哈希值都是⼀样的,就冲突了

    

如:{63 , 31}看起来没有关联的值,如果M是16,也就是 24 ,那么计算出的哈希值都是15,因为63的⼆进制后8位是 00111111,31的⼆进制后8位是 00011111。如果是 10X ,就更明显了,保留的都是10进值的后x位,如:{112, 12312},如果M是100,也就是 102 ,那么计算出的哈希值都是122X

    
3. 当使⽤除法散列法时,建议M取不太接近2的整数次冥的⼀个质数(素数)


 1.4.2 乘法散列法

1. 乘法散列法对哈希表大小M没有要求,他的⼤思路第⼀步:

   

                                                a. ⽤关键字 K 乘上常数 A (0<A<1),并抽取出 k*A 的⼩数部分

  

                                                b. 再⽤M乘以k*A 的⼩数部分,再向下取整

    

                                本质就是用M*(0~1)之间的小数  


2. h(key) = floor(M × ((A × key)%1.0)) ,其中floor表⽰对表达式进⾏下取整,A∈(0,1),这⾥最重要的是A的值应该如何设定,Knuth认为 A = ( 5 − 1)/2 = 0.6180339887.... (⻩⾦分割点)⽐较好

  

3. 乘法散列法对哈希表⼤⼩M是没有要求的,假设M为1024,key为1234,A = 0.6180339887, A*key= 762.6539420558,取⼩数部分为0.6539420558, M×((A×key)%1.0) = 0.6539420558*1024 =669.6366651392,那么h(1234) = 669


 1.4.3 全域散列法

1. 如果存在⼀个恶意的对⼿,他针对我们提供的散列函数,特意构造出⼀个发⽣严重冲突的数据集

   
⽐如,让所有关键字全部落⼊同⼀个位置中。这种情况是可以存在的,只要散列函数是公开且确定的,就可以实现此攻击。解决⽅法⾃然是⻅招拆招,给散列函数增加随机性,攻击者就⽆法找出确定可以导致最坏情况的数据。这种⽅法叫做全域散列

    
2.  hab (key) = ((a × key + b)%P)%M ,P需要选⼀个⾜够⼤的质数,a可以随机选[1,P-1]之间的任意整数,b可以随机选[0,P-1]之间的任意整数,这些函数构成了⼀个P*(P-1)组全域散列函数组

   
假设P=17,M=6,a = 3, b = 4, 则 h34 (8) = ((3 × 8 + 4)%17)%6  =  5 

    
3.  需要注意的是每次初始化哈希表时,随机选取全域散列函数组中的⼀个散列函数使⽤,后续增删查改都固定使⽤这个散列函数,否则每次哈希都是随机选⼀个散列函数,那么插⼊是⼀个散列函数,查找⼜是另⼀个散列函数,就会导致找不到插⼊的key了


1.5 处理哈希冲突

实践中哈希表⼀般还是选择除法散列法作为哈希函数,当然哈希表⽆论选择什么哈希函数也避免不了冲突,那么插⼊数据时,如何解决冲突呢?主要有两种两种⽅法,开放定址法和链地址法


1.5.1 开放定址法(闭散列)

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

在开放定址法中所有的元素都放到哈希表⾥,当⼀个关键字key⽤哈希函数计算出的位置冲突了,则按照某种规则找到⼀个没有存储数据的位置进⾏存储,开放定址法中负载因⼦⼀定是⼩于的。这⾥的规则有三种:线性探测、⼆次探测、双重探测

1. 线性探测(挨着查找)

1. 从发⽣冲突的位置开始,依次线性向后探测,直到寻找到下⼀个没有存储数据的位置为⽌,如果⾛到哈希表尾,则回绕到哈希表头的位置(回绕方法就是进行取模

     

//如果到达表的最后一个位置那么就模一下表的空间大小
hashi = (hash0 + i) % _tables.size();


2. h(key) = hash0 =  key % M , hash0位置冲突了,则线性探测公式为:
hc(key, i) = hashi = (hash0 + i) % M, i  = {1, 2, 3, ..., M − 1},因为负载因⼦⼩于1,则最多探测M-1次,⼀定能找到⼀个存储key的位置
    
3. 线性探测的⽐较简单且容易实现,线性探测的问题假设,hash0位置连续冲突,hash0,hash1,hash2位置已经存储数据了,后续映射到hash0,hash1,hash2,hash3的值都会争夺hash3位置,这种现象叫做群集/堆积,下⾯的⼆次探测可以⼀定程度改善这个问题

下⾯演⽰ {19,30,5,36,13,20,21,12} 等这⼀组值映射到M=11的表中(key%11)

   

h(19) = 8,h(30) = 8,h(5) = 5,h(36) = 3,h(13) = 2,h(20) = 9,h(21) =10,h(12) = 1

线性探测法占别人的位置会导致堆积

2. 二次探测(跳跃着查找)

1. 从发⽣冲突的位置开始,依次左右按⼆次⽅跳跃式探测,直到寻找到下⼀个没有存储数据的位置为⽌,如果往右⾛到哈希表尾,则回绕到哈希表头的位置;如果往左⾛到哈希表头,则回绕到哈希表尾的位置

   

2. h(key) = hash0 =  key % M , hash0位置冲突了,则⼆次探测公式为:
hc(key, i) = hashi = (hash0 ± i *i) % M,  i  = {1, 2, 3, ...,  M/2(二分之M)}

  

hashi = (hash0 + (i*i*flag)) % _tables.size();

3. ⼆次探测当 hashi = (hash0 − i )%M时,当hashi<0时,需要hashi += M

下⾯演⽰ {19,30,52,63,11,22} 等这⼀组值映射到M=11的表中

  

h(19) = 8, h(30) = 8, h(52) = 8, h(63) = 8, h(11) = 0, h(22) = 0

二次探测法虽然跳跃起来了但是却无法充分利用位置

3. 双重散列

1. 第⼀个哈希函数计算出的值发⽣冲突,使⽤第⼆个哈希函数计算出⼀个跟key相关的偏移量值,不断往后探测,直到寻找到下⼀个没有存储数据的位置为⽌

  

2. h1 (key) = hash0 =  key % M , hash0位置冲突了,则双重探测公式为:
hc(key, i) = hashi = (hash0 +  i ∗ h2 (key)) % M, i  =  {1, 2, 3, ..., M}

  

也跳跃着查找,但是使用i*下一个哈希函数算出来的值   

3. 要求 h2 (key) < M 且 h2 (key) 和M互为质数,有两种简单的取值⽅法:

   

                                                a. 当M为2整数冥时,h2 (key) 从[0,M-1]任选⼀个奇数

   

                                                b. 当M为质数时, h2 (key)  =  key % (M − 1)  +  1

  

4. 保证 h2 (key) 与M互质是因为根据固定的偏移量所寻址的所有位置将形成⼀个群,若最⼤公约数说⽆法充分利⽤整个散列表

    

举例来说,若初始探查位置为1,偏移量为3,整个散列表⼤⼩为12,那么所能寻址的位置为{1, 4, 7, 10},寻址个数为p = gcd(M, h1 (key)) > 1 ,那么所能寻址的位置的个数为 M/P < M ,使得对于⼀个关键字来12/gcd(12, 3) = 4

下⾯演⽰ {19,30,52} 等这⼀组值映射到M=11的表中,设 h2 (key)  = key%10 + 1

上面的三种方法都无法完全解决哈希冲突的问题,只有跳出内卷循环才能解决问题,也就是链地址法


2. 闭散列实现哈希表

  

2.1 开发地址法的基础构架

开放定址法在实践中,不如下⾯的链地址法因为开放定址法解决冲突不管使⽤哪种⽅法,占⽤的都是哈希表中的空间,始终存在互相影响的问题

//定义一个枚举来记录数组的三个状态
enum State
{
	EXIST,//存在
	EMPTY,//空
	DELETE//删除
};

template<class K, class V>
struct HashData
{
	pair<K, V> _kv;
	State _state = EMPTY;//状态为空
};

template<class K, class V>
class HashTable
{
public:

private:
	vector<HashData<K, V>> _tables;//表的空间大小
	size_t _n;  // 记录数据个数
};

哈希是通过哈希函数使得元素的存储位置与它的关键码之间能够建立一一映射的关系,需要使用pair<K,V>类型进行存储。采用vector作为底层逻辑,存储元素类型为哈希节点类型HashData<K, V>

这里不采用size作为哈希表中有效元素个数,考虑到容器中结构的差异性,是由于_ size一般用于序列式容器中表示有效元素个数,在关联式容器中命名约定一般规定_n作为记录有效元素个数

要注意的是这⾥需要给每个存储值的位置加⼀个状态标识,否则删除⼀些值以后,会影响后⾯冲突的值的查找

    

如下图,我们删除30,会导致查找20失败,当我们给每个位置加⼀个状态标识
{EXIST,EMPTY,DELETE} ,删除30就可以不⽤删除值,⽽是把状态改为 DELETE ,那么查找20时是遇到 EMPTY 才能,就可以找到20

   

h(19) = 8,h(30) = 8,h(5) = 5,h(36) = 3,h(13) = 2,h(20) = 9,h(21) =10,h(12) = 1


2.2 扩容

这⾥我们哈希表负载因⼦控制在0.7,当负载因⼦到0.7以后我们就需要扩容了,我们还是按照2倍扩容,但是同时我们要保持哈希表⼤⼩是⼀个质数,第⼀个是质数,2倍后就不是质数了。那么如何解决了,⼀种⽅案就是上⾯1.4.1除法散列中我们讲的Java HashMap的使⽤2的整数冥,但是计算时不能直接取模的改进⽅法。另外⼀种⽅案是sgi版本的哈希表使⽤的⽅法,给了⼀个近似2倍的质数表,每次去质数表获取扩容后的⼤⼩

  

负载因子 >= 0.7扩容 n/m 数据个数/表的空间大小

当哈希表进行扩容时,表的长度发生了变换。这也意味着通过哈希函数(开发定址法)得到的位置需要重新安排插入,所以需要再开辟空间和插入数据,重新进行映射到新表中 ,遍历旧表,将旧表的数据映射到新表,然后再使用新对象去调用插入,把旧表的数据插入到新表,交换新旧表的空间

素数表:

//素数表
inline unsigned long __stl_next_prime(unsigned long n)
{
	// Note: assumes long is at least 32 bits.
	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
	};
	const unsigned long* first = __stl_prime_list;
	const unsigned long* last = __stl_prime_list + __stl_num_primes;
	const unsigned long* pos = lower_bound(first, last, n);
	return pos == last ? *(last - 1) : *pos;
}

扩容代码:

//扩容
// 负载因子 >= 0.7扩容 n/m 数据个数/表的空间大小
//为了方便计算分子n*10
if (_n * 10 / _tables.size() >= 7)
{
	//创建一个新的哈希表 newht 哈希表里本来就有vector
	HashTable<K, V> newht;
	//*2是无法一直保持素数的
	//newht._tables.resize(_tables.size() * 2);

	//使用素数表来获取比素数表的值大一点的值
	newht._tables.resize(__stl_next_prime(_tables.size() + 1));

	for (auto& data : _tables)
	{
		// 遍历旧表,旧表的数据映射到新表
		if (data._state == EXIST)
		{
			//使用新对象去调用插入,把旧表的数据插入到新表
			newht.Insert(data._kv);
		}
	}
	//交换新旧表的空间
	_tables.swap(newht._tables);
}

2.3 插入

在插入过程,元素通过除留余数法找到对应位置进行插入,期间可能会出现哈希冲突的问题,我们需要以该位置向后寻找状态标记为空的位置进行插入

bool Insert(const pair<K, V>& kv)
{
	//如果值已经存在
	if (Find(kv.first))
		return false;
	Hash hash;//仿函数,用于转换成为无符号整形
	//插入值之后从起始位置hash0去用插入的值对表的大小取模算出值对应的位置
	size_t hash0 = hash(kv.first) % _tables.size();//hash0是第一次算出来的位置
	size_t hashi = hash0;
	size_t i = 1;
	int flag = 1;
	while (_tables[hashi]._state == EXIST)//如果hashi的状态为存在
	{
		//进行线性探测
		//如果到达表的最后一个位置那么就模一下表的空间大小
		hashi = (hash0 + i) % _tables.size();
		++i;

		//二次探测
		/*hashi = (hash0 + (i*i*flag)) % _tables.size();
		if (hashi < _tables.size())
			hashi += _tables.size();

		if (flag == 1)
		{
			flag = -1;
		}
		else
		{
			++i;
			flag = 1;
		}*/
	}

	//当遇到空的位置就插入
	_tables[hashi].kv = kv;
	_tables[hashi]._state = EXIST;//将插入的位置标记为存在
	++_n;

	return true;
}

2.4 查找

HashData<K, V>* Find(const K& key)
{
	Hash hash;
	size_t hash0 = hash(key) % _tables.size();
	size_t hashi = hash0;
	size_t i = 1;
	while (_tables[hashi]._state != EMPTY)
	{
		if (_tables[hashi]._state == EXIST//如果状态是存在并且是那个值
			&& _tables[hashi]._kv.first == key)
		{
			return &_tables[hashi];
		}

		// 线性探测
		hashi = (hash0 + i) % _tables.size();
		++i;
	}

	return nullptr;
}

2.5 删除

删除只用改变位置状态就可以了

bool Erase(const K& key)
{
	HashData<K, V>* ret = Find(key);
	if (ret)
	{
		ret->_state = DELETE;
		return true;
	}
	else
	{
		return false;
	}
}

2.6 闭散列代码

//定义一个枚举来记录数组的三个状态
enum State
{
	EXIST,//存在
	EMPTY,//空
	DELETE//删除
};

template<class K, class V>
struct HashData
{
	pair<K, V> _kv;
	State _state = EMPTY;//状态为空
};


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

/*
1. 将string类型转换成无符号整形(BKDR_Hash)
2. 字符串转换成整形,可以把字符ascii码相加即可
3. 但是直接相加的话,类似"abcd"和"bcad"这样的字符串计算出是相同的
4. 这⾥我们使⽤BKDR哈希的思路,⽤上次的计算结果去
乘以⼀个质数,这个质数⼀般去31, 131等效果会⽐较好
*/

template<>
struct HashFunc<string>
{
	size_t operator()(const string& s)
	{
		// BKDR
		size_t hash = 0;
		for (auto ch : s)
		{
			hash += ch;
			hash *= 131;
		}

		return hash;
	}
};

inline unsigned long __stl_next_prime(unsigned long n)
{
	// Note: assumes long is at least 32 bits.
	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
	};
	const unsigned long* first = __stl_prime_list;
	const unsigned long* last = __stl_prime_list + __stl_num_primes;
	const unsigned long* pos = lower_bound(first, last, n);
	return pos == last ? *(last - 1) : *pos;
}

namespace open_address//开发定址法
{								//加上一个仿函数Hash,用于转换成为无符号整形
	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable
	{
	public:
		HashTable()
			:_tables(__stl_next_prime(0))//给一个0去获取>=0的素数
			, _n(0)//数据个数
		{}

		bool Insert(const pair<K, V>& kv)
		{
			//如果值已经存在
			if (Find(kv.first))
				return false;

			//扩容
			// 负载因子 >= 0.7扩容 n/m 数据个数/表的空间大小
			//为了方便计算分子n*10
			if (_n * 10 / _tables.size() >= 7)
			{
				//创建一个新的哈希表 newht 哈希表里本来就有vector
				HashTable<K, V> newht;
				//*2是无法一直保持素数的
				//newht._tables.resize(_tables.size() * 2);

				//使用素数表来获取比素数表的值大一点的值
				newht._tables.resize(__stl_next_prime(_tables.size() + 1));

				for (auto& data : _tables)
				{
					// 遍历旧表,旧表的数据映射到新表
					if (data._state == EXIST)
					{
						//使用新对象去调用插入,把旧表的数据插入到新表
						newht.Insert(data._kv);
					}
				}
				//交换新旧表的空间
				_tables.swap(newht._tables);
			}


			Hash hash;//仿函数,用于转换成为无符号整形
			//插入值之后从起始位置hash0去用插入的值对表的大小取模算出值对应的位置
			size_t hash0 = hash(kv.first) % _tables.size();//hash0是第一次算出来的位置
			size_t hashi = hash0;
			size_t i = 1;
			int flag = 1;
			while (_tables[hashi]._state == EXIST)//如果hashi的状态为存在
			{
				//进行线性探测
				//如果到达表的最后一个位置那么就模一下表的空间大小
				hashi = (hash0 + i) % _tables.size();
				++i;

				//二次探测
				/*hashi = (hash0 + (i*i*flag)) % _tables.size();
				if (hashi < _tables.size())
					hashi += _tables.size();

				if (flag == 1)
				{
					flag = -1;
				}
				else
				{
					++i;
					flag = 1;
				}*/
			}

			//当遇到空的位置就插入
			_tables[hashi].kv = kv;
			_tables[hashi]._state = EXIST;//将插入的位置标记为存在
			++_n;

			return true;
		}

		HashData<K, V>* Find(const K& key)
		{
			Hash hash;
			size_t hash0 = hash(key) % _tables.size();
			size_t hashi = hash0;
			size_t i = 1;
			while (_tables[hashi]._state != EMPTY)
			{
				if (_tables[hashi]._state == EXIST//如果状态是存在并且是那个值
					&& _tables[hashi]._kv.first == key)
				{
					return &_tables[hashi];
				}

				// 线性探测
				hashi = (hash0 + i) % _tables.size();
				++i;
			}

			return nullptr;
		}

		bool Erase(const K& key)
		{
			
			size_t hashi = key % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					if (prev == nullptr)
					{
						// 头结点
						_tables[hashi] = cur->_next;
					}
					else
					{
						// 中间节点
						prev->_next = cur->_next;
					}

					delete cur;
					--_n;

					return true;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}

			return false;
		}



	private:
		vector<HashData<K, V>> _tables;//表的空间大小
		size_t _n;  // 记录数据个数
	};
}


3. key不能取模的问题

当key是string/Date等类型时,key不能取模,那么我们需要给HashTable增加⼀个仿函数,这个仿函数⽀持把key转换成⼀个可以取模的整形

    

如果key可以转换为整形并且不容易冲突,那么这个仿函数就⽤默认参数即可,如果这个Key不能转换为整形,我们就需要⾃⼰实现⼀个仿函数传给这个参数,实现这个仿函数的要求就是尽量key的每值都参与到计算中,让不同的key转换出的整形值不同

   

string做哈希表的key⾮常常⻅,所以我们可以考虑把string特化⼀下

//将普通类型转换成无符号整形
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};


/*
1. 将string类型转换成无符号整形(BKDR_Hash)
2. 字符串转换成整形,可以把字符ascii码相加即可
3. 但是直接相加的话,类似"abcd"和"bcad"这样的字符串计算出是相同的
4. 这⾥我们使⽤BKDR哈希的思路,⽤上次的计算结果去
乘以⼀个质数,这个质数⼀般去31, 131等效果会⽐较好
*/
template<>
struct HashFunc<string>
{
	size_t operator()(const string& s)
	{
		// BKDR
		size_t hash = 0;
		for (auto ch : s)
		{
			hash += ch;
			hash *= 131;
		}

		return hash;
	}
};

4. 链地址法(开散列/哈希桶)

解决冲突的思路

    
开放定址法中所有的元素都放到哈希表⾥,链地址法中所有的数据不再直接存储在哈希表中,哈希表中存储⼀个指针,没有数据映射这个位置时,这个指针为空,有多个数据映射到这个位置时,我们把这些冲突的数据链接成⼀个链表,挂在哈希表这个位置下⾯,链地址法也叫做拉链法或者哈希桶

下⾯演⽰ {19,30,5,36,13,20,21,12,24,96} 等这⼀组值映射到M=11的表中

   

h(19) = 8,h(30) = 8,h(5) = 5,h(36) = 3,h(13) = 2,h(20) = 9,h(21) =10,h(12) = 1,h(24) = 2,h(96) = 88


4.1 链地址法的基础框架

namespace hash_bucket//哈希桶
{
	template<class K, class V>
	struct HashNode//给一个节点用来挂节点
	{
		pair<K, V> _kv;
		HashNode<K, V>* _next;

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

	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		//构造
		HashTable()
			:_tables(11)
			, _n(0)
		{}
	private:
		vector<Node*> _tables; // 指针数组
		size_t _n = 0;// 表中存储数据个数
	};
}

4.2 插入

bool Insert(const pair<K, V>& kv)
{
    Hash hs;
    size_t hashi = kv.first % _tables.size();
    // 头插
    //让新节点变成哈希表里的第一个也就是说要让哈希表里存储新节点的地址
    Node* newnode = new Node(kv);//创建一个新节点new Node

    //将新节点的下一个节点指向原来的第一个节点的地址
    //第一个节点的地址在哈希表里
    newnode->_next = _tables[hashi];

    _tables[hashi] = newnode;//再把新节点给与_tables[hashi]里存储的指针
    ++_n;

    return true;
}


4.3 扩容

开放定址法负载因⼦必须⼩于1,链地址法的负载因⼦就没有限制了,可以⼤于1

  

负载因⼦越⼤,哈希冲突的概率越⾼,空间利⽤率越⾼;负载因⼦越⼩,哈希冲突的概率越低,空间利⽤率越低

// 负载因子 == 1时扩容
if (_n == _tables.size())
{
	vector<Node*> newTatble(_tables.size() * 2);
	//遍历旧表
	for (size_t i = 0; i < _tables.size(); i++)
	{
		Node* cur = _tables[i];
		while (cur)
		{
			Node* next = cur->_next;
			// 旧表数据头插到新表
			size_t hashi = cur->_kv.first % newTatble.size();
			cur->_next = newTatble[hashi];
			newTatble[hashi] = cur;

			cur = next;
		}
		//交换
		_tables[i] = nullptr;

	}

	_tables.swap(newTatble);
}


4.4 查找

HashData<K, V>* Find(const K& key)
{
	Hash hash;
	size_t hash0 = hash(key) % _tables.size();
	size_t hashi = hash0;
	size_t i = 1;
	while (_tables[hashi]._state != EMPTY)
	{
		if (_tables[hashi]._state == EXIST//如果状态是存在并且是那个值
			&& _tables[hashi]._kv.first == key)
		{
			return &_tables[hashi];
		}

		// 线性探测
		hashi = (hash0 + i) % _tables.size();
		++i;
	}

	return nullptr;
}

4.5 删除

两种情况:一种是删除第一个节点,另一种是删除其他节点prev->_next = cur->_next

   

在删除节点需要前后兼顾,保存下前驱指针指向节点

bool Erase(const K& key)
		{
			
			size_t hashi = key % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (kot(cur->_data) == key)
				{
					if (prev == nullptr)
					{
						// 头结点
						_tables[hashi] = cur->_next;
					}
					else
					{
						// 中间节点
						prev->_next = cur->_next;
					}

					delete cur;
					--_n;

					return true;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}

			return false;
		}


4.6 开散列代码

namespace hash_bucket//哈希桶
{
	template<class K, class V>
	struct HashNode//给一个节点用来挂节点
	{
		pair<K, V> _kv;
		HashNode<K, V>* _next;

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

	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		//构造
		HashTable()
			:_tables(11)
			, _n(0)
		{}

		bool Insert(const pair<K, V>& kv)
		{
			// 负载因子 == 1时扩容
			if (_n == _tables.size())
			{
				vector<Node*> newTatble(_tables.size() * 2);
				//遍历旧表
				for (size_t i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;
						// 旧表数据头插到新表
						size_t hashi = cur->_kv.first % newTatble.size();
						cur->_next = newTatble[hashi];
						newTatble[hashi] = cur;

						cur = next;
					}
					//交换
					_tables[i] = nullptr;

				}

				_tables.swap(newTatble);
			}

			size_t hashi = kv.first % _tables.size();
			// 头插
			//让新节点变成哈希表里的第一个也就是说要让哈希表里存储新节点的地址
			Node* newnode = new Node(kv);//创建一个新节点new Node

			//将新节点的下一个节点指向原来的第一个节点的地址
			//第一个节点的地址在哈希表里
			newnode->_next = _tables[hashi];

			_tables[hashi] = newnode;//再把新节点给与_tables[hashi]里存储的指针
			++_n;

			return true;
		}

		Node* Find(const K& key)
		{
			Hash hash;
			size_t hashi = hash(key) % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					return &cur->_kv.first;
				}
				else
				{
					cur = cur->_next;
				}
			}
			return nullptr;
		}

		bool Erase(const K& key)
		{
			Hash hash;
			size_t hashi = hash(key) % _tables.size();

			Node* cur = _tables[hashi];
			Node* prev = nullptr;

			while (cur)
			{
				if (cur->_kv.first == key)
				{
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}
					delete cur;

					return true;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}
			return false;
		}


	private:
		vector<Node*> _tables; // 指针数组
		size_t _n = 0;// 表中存储数据个数
	};
}

 

此间为迷迭

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

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

相关文章

微波无源器件 OMT1 一种用于倍频程接收机前端的十字转门四脊正交模耦合器(24-51GHz)

摘要&#xff1a; 我们报道了一种用于天文学射电望远镜的毫米波波长接收机的一种十字转门四脊OMT的设计&#xff0c;制造和实测结果。此四脊OMT被直接兼容到一个四脊馈电喇叭来实现可以拓展矩形波导单模带宽的双极化低噪声接收机。使用了24-51GHz的带宽&#xff0c;OMT证实了0.…

如何在 WordPress 中轻松强制所有用户退出登录

作为一名长期管理 WordPress 网站的站长&#xff0c;我深知维护网站安全性的重要性。尤其是在面对会员网站或付费内容平台时&#xff0c;确保所有用户的登录状态是最新的&#xff0c;是维持网站正常运营的关键之一。今天&#xff0c;我就分享一下如何通过简单的步骤&#xff0c…

RT-DETR融合[ECCV2024]自调制特征聚合SMFA模块及相关改进思路

RT-DETR使用教程&#xff1a; RT-DETR使用教程 RT-DETR改进汇总贴&#xff1a;RT-DETR更新汇总贴 《SMFANet: A Lightweight Self-Modulation Feature Aggregation Network for Efficient Image Super-Resolution》 一、 模块介绍 论文链接&#xff1a;https://link.springer.…

WebAPI性能监控-MiniProfiler与Swagger集成

Net8_WebAPI性能监控-MiniProfiler与Swagger集成 要在.NET Core项目中集成MiniProfiler和Swagger&#xff0c;可以按照以下步骤操作&#xff1a; 安装NuGet包&#xff1a; 安装MiniProfiler.AspNetCore.Mvc包以集成MiniProfiler。安装MiniProfiler.EntityFrameworkCore包以监…

第十五章 Spring之假如让你来写AOP——Joinpoint(连接点)篇

Spring源码阅读目录 第一部分——IOC篇 第一章 Spring之最熟悉的陌生人——IOC 第二章 Spring之假如让你来写IOC容器——加载资源篇 第三章 Spring之假如让你来写IOC容器——解析配置文件篇 第四章 Spring之假如让你来写IOC容器——XML配置文件篇 第五章 Spring之假如让你来写…

喜讯 | 科东软件荣获广东省工业软件科学技术进步奖一等奖

工业软件是制造业数字化、智能化转型升级的核心支撑&#xff0c;贯穿于工业生产的全过程&#xff0c;包括研发设计、测试&#xff0c;智能装备与操作系统嵌入式&#xff0c;系统与平台&#xff0c;算法、模型与工具等类型。通过开展工业软件科学技术奖评选活动&#xff0c;激励…

SystemVerilog学习笔记(二):数组

数组是元素的集合&#xff0c;所有元素都具有相同的类型&#xff0c;并使用其名称和一个或多个索引进行访问。 Verilog 2001 要求数组的下限和上限必须是数组声明的一部分。 System Verilog 引入了紧凑数组声明样式&#xff0c;只需给出数组大小以及数组名称声明就足够了。 下…

批量从Excel某一列中找到符合要求的值并提取其对应数据

本文介绍在Excel中&#xff0c;从某一列数据中找到与已知数据对应的字段&#xff0c;并提取这个字段对应数值的方法。 首先&#xff0c;来明确一下我们的需求。现在已知一个Excel数据&#xff0c;假设其中W列包含了上海市全部社区的名称&#xff0c;而其后的Y列则是这些社区对应…

握手协议是如何在SSL VPN中发挥作用的?

SSL握手协议&#xff1a;客户端和服务器通过握手协议建立一个会话。会话包含一组参数&#xff0c;主要有会话ID、对方的证书、加密算法列表&#xff08;包括密钥交换算法、数据加密算法和MAC算法&#xff09;、压缩算法以及主密钥。SSL会话可以被多个连接共享&#xff0c;以减少…

数字化转型:基于价值流的业务架构战略解析

在当前数字化浪潮下&#xff0c;企业纷纷转向数字化转型&#xff0c;以适应市场需求的快速变化和技术革新。数字化转型不仅仅是技术层面的变革&#xff0c;更是对企业业务模式、文化以及价值创造方式的全面重新思考和重塑。《价值流&#xff08;Value Streams&#xff09;》为企…

Mac终端字体高亮、提示插件

一、安装配置“oh my zsh” 1.1 安装brew /bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)" 按照步骤安装即可&#xff0c;安装完成查看版本 brew -v 1.2 安装zsh brew install zsh 安装完成后查看版本 zsh --version 1.3 …

什么是CRM系统?

越来越多的企业意识到&#xff1a;如何有效管理与客户的关系、提升客户满意度&#xff0c;并通过这些提升推动销售增长&#xff0c;已经成为许多公司亟待解决的问题。为此&#xff0c;客户关系管理&#xff08;Customer Relationship Management&#xff0c;简称CRM&#xff09…

Ilya Sutskever AI行业将进入一个新的“探索时代”

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

MySQL(5)【数据类型 —— 字符串类型】

阅读导航 引言一、char&#x1f3af;基本语法&#x1f3af;使用示例 二、varchar&#x1f3af;基本语法&#x1f3af;使用示例 三、char 和 varchar 比较四、日期和时间类型1. 基本概念2. 使用示例 五、enum 和 set&#x1f3af;基本语法 引言 之前我们聊过MySQL中的数值类型&…

湾区聚力 开源启智 | 2024 CCF中国开源大会暨第五届OpenI/O启智开发者大会闪耀深圳

当下&#xff0c;全球数字化浪潮席卷而来&#xff0c;开源技术已成为科技创新和产业升级的关键驱动力。11月9-10日&#xff0c;以“湾区聚力 开源启智”为主题的2024 CCF中国开源大会在深圳隆重举行。本届大会由中国计算机学会主办&#xff0c;CCF开源发展委员会、鹏城实验室、…

Linux基本指令(中)(2)

文章目录 前言一、echo二、cat三、more四、less五、head六、tail七、date八、cal九、find十、whoami十一、clear总结 前言 承上启下&#xff0c;我们再来看看另外一些常用的基础指令吧&#xff01; 一、echo 语法&#xff1a;echo [选项] [字符串] 功能&#xff1a;在终端设备上…

MYSQL中JDBC的使用

一、JDBC基础概念 JDBC 是Java 中的一组API&#xff0c;用于执行SQL 操作&#xff08;例如CRUD 操作&#xff1a;增、删、改、关系&#xff09;&#xff0c;同时可以和各种类型的数据库类型进行连接&#xff08;MySQL、Oracle、SQL Server 等&#xff09;。 JDBC是Java标准库的…

UnixBench和Geekbench进行服务器跑分

1 概述 服务器的基准测试&#xff0c;常见的测试工具有UnixBench、Geekbench、sysbench等。本文主要介绍UnixBench和Geekbench。 1.1 UnixBench UnixBench是一款开源的测试UNIX系统基本性能的工具&#xff08;https://github.com/kdlucas/byte-unixbench&#xff09;&#x…

基于Java Springboot人力资源管理系统

一、作品包含 源码数据库设计文档万字全套环境和工具资源部署教程 二、项目技术 前端技术&#xff1a;Html、Css、Js、Vue 数据库&#xff1a;MySQL 后端技术&#xff1a;Java、Spring Boot、MyBatis 三、运行环境 开发工具&#xff1a;IDEA 数据库&#xff1a;MySQL8.0…

使用OpenCV(C++)通过鼠标点击操作获取图像的像素坐标和像素值

使用OpenCV&#xff08;C&#xff09;通过鼠标点击操作获取图像的像素坐标和像素值 在这篇博客中&#xff0c;我们将介绍如何使用OpenCV库在C中实现鼠标点击操作&#xff0c;以获取图像的像素坐标和像素值。代码分为两个部分&#xff1a;一个是鼠标事件处理的回调函数&#xff…