【C++:哈希表】从哈希冲突到负载因子:熟悉哈希表的核心机制

🔥艾莉丝努力练剑: 个人主页

专栏传送门:《C语言》《数据结构与算法》C/C++干货分享&学习过程记录Linux操作系统编程详解笔试/面试常见算法:从基础到进阶测试开发要点全知道

⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平


🎬艾莉丝的简介:


🎬艾莉丝的C++专栏简介:


目录

C++的两个参考文档

前情提示

[1 ~> 初始哈希](#1 ~> 初始哈希)

[2 ~> 直接定址法](#2 ~> 直接定址法)

[2.1 概念](#2.1 概念)

[2.2 示例:字符串中的第一个唯一字符](#2.2 示例:字符串中的第一个唯一字符)

[3 ~> 哈希的一些概念](#3 ~> 哈希的一些概念)

[3.1 哈希冲突](#3.1 哈希冲突)

[3.2 负载因子](#3.2 负载因子)

[3.3 将关键字转为整数](#3.3 将关键字转为整数)

[4 ~> 哈希函数](#4 ~> 哈希函数)

[4.1 除法散列法 / 除留余数法](#4.1 除法散列法 / 除留余数法)

[4.2 乘法散列法(了解即可)](#4.2 乘法散列法(了解即可))

[4.3 全域散列法(了解即可)](#4.3 全域散列法(了解即可))

[4.4 其他方法(了解即可)](#4.4 其他方法(了解即可))

[5 ~> 处理哈希冲突](#5 ~> 处理哈希冲突)

[5.1 开放定址法](#5.1 开放定址法)

[5.1.1 线性探测(含堆积问题)](#5.1.1 线性探测(含堆积问题))

[5.1.2 二次探测](#5.1.2 二次探测)

[5.1.3 双重探测](#5.1.3 双重探测)

[5.2 详解开放定址法代码](#5.2 详解开放定址法代码)

[5.2.1 哈希表结构](#5.2.1 哈希表结构)

[5.2.2 扩容问题](#5.2.2 扩容问题)

[5.2.3 key不能取模的问题](#5.2.3 key不能取模的问题)

[5.3 链地址法](#5.3 链地址法)

[5.3.1 对比开放定址法和链地址法](#5.3.1 对比开放定址法和链地址法)

[5.3.2 哈希桶概念及其示例](#5.3.2 哈希桶概念及其示例)

[5.3.3 扩容](#5.3.3 扩容)

[5.3.4 极端场景](#5.3.4 极端场景)

[5.4 哈希桶代码实现](#5.4 哈希桶代码实现)

[5.4.0 在私有定义成员变量](#5.4.0 在私有定义成员变量)

[5.4.1 插入](#5.4.1 插入)

[5.4.2 查找](#5.4.2 查找)

[5.4.3 删除](#5.4.3 删除)

完整代码示例与实践演示

HashTable.h:

Test.cpp:

运行结果

open_address::TestHT1():

open_address::TestHT2():

Hash_bucket::TestHT1():

Hash_bucket::TestHT2():

博主手记

结尾


C++的两个参考文档

老朋友(非官方文档):cplusplus

准官方文档(同步更新):C++ 官方参考文档
set和multiset的参考文档:set** 、multiset**

map和multimap的参考文档:map** multimap******
unordered_set和unordered_multiset的参考文档:unordered_set** 、**unordered_multiset****



前情提示


1 ~> 初始哈希

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


2 ~> 直接定址法

2.1 概念

当关键字的范围比较集中时**,直接定址法是非常简单高效的方法**:比如一组关键字都在[0,99]之间,那么我们开一个100个数的数组,每个关键字的值直接就是存储位置的下标。再比如一组关键字值都在[a,z]的小写字母,那么我们开一个26个数的数组,每个关键字acsii码-aascii码就是存储位置的下标。也就是说直接定址法本质就是用关键字计算出一个绝对位置或者相对位置。这个方法我们不仅在计数排序部分用过,在string部分的OJ题目那里也用过了。

2.2 示例:字符串中的第一个唯一字符

力扣链接:387. 字符串中的第一个唯一字符

题目描述:

cpp 复制代码
class Solution {
public:
	int firstUniqChar(string s) {
		// 每个字⺟的ascii码-'a'的ascii码作为下标映射到count数组,数组中存储出现的次数 
		int count[26] = { 0 };

		// 统计次数 
		for (auto ch : s)
		{
			count[ch - 'a']++;
		}
		for (size_t i = 0; i < s.size(); ++i)
		{
			if (count[s[i] - 'a'] == 1)
				return i;
		}
		return -1;
	}
};

3 ~> 哈希的一些概念

3.1 哈希冲突

直接定址法的缺点也非常明显,当关键字的范围比较分散时,就很浪费内存甚至内存不够用。假设我们只有数据范围是[0,9999]的N个值,我们要映射到一个M个空间的数组中(一般情况下M >= N),那么就要借助哈希函数(hash function)hf,关键字key被放到数组的h(key)位置,这里要注意的是h(key)计算出的值必须在[O , M)之间。

这里存在的一个问题就是,两个不同的key可能会映射到同一个位置去,这种问题我们叫做哈希冲突,或者哈希碰撞。理想情况是找出一个好的哈希函数避免冲突,但是实际场景中,冲突是不可避免的,所以我们尽可能设计出优秀的哈希函数,减少冲突的次数,同时也要去设计出解决冲突的方案。

3.2 负载因子

假设哈希表中已经映射存储了N个值,哈希表的大小为M,那么负载因子 = N / M,负载因子有些地方也翻译为载荷因子/装载因子等,他的英文为loadfactor。负载因子越大,哈希冲突的概率越高,空间利用率越高;负载因子越小,哈希冲突的概率越低,空间利用率越低。

3.3 将关键字转为整数

我们将关键字映射到数组中位置,一般是整数好做映射计算,如果不是整数,我们要想办法转换成整数,这个细节我们后面代码实现中再进行细节展示。下面哈希函数部分我们讨论时,如果关键字不是整数,那么我们讨论的Key是关键字转换成的整数。


4 ~> 哈希函数

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

1、除法散列法也叫做除留余数法,顾名思义,假设哈希表的大小为M,那么通过key除以M的余数作为映射位置的下标,也就是哈希函数为:h(key) = key % M

2、当使用除法散列法时,要尽量避免M为某些值,如2的幂,10的幂等。如果是2^X,key % 2^X本质相当于保留key的后X位(后X位相同的值),计算出的哈希值都是一样的------就冲突了。比如:[63,31}看起来没有关联的值,如果M是16,也就是2^4,那么计算出的哈希值都是15,因为63的二进制后8位是00111111,31的二进制后8位是00011111。如果是10^x,就更明显了,保留的都是10进值的后X位,如:[112,12312},如果M是100(10^2),计算出的哈希值都是12。

3、当使用除法散列法时,建议M取不太接近2的整数次幂的一个质数(素数),什么是质数?可以参考下面的概念------

下面的内容仅做了解,大家可以选择跳过哦!前面第三点建议M取一个不太接近2的整数次幂的质数,Java中,HashMap采用除法散列法时就是2的整数次幂做哈希表的M,这样一来我们可以直接位运算,相对取模会更加高效些。只是Java不是单纯地取模,比如说M是2^16,本质上是取后16位,那么用key' = key >> 16,然后把key和key'异或的结果作为哈希值,也就是说:我们可以映射的值在[0 , M]范围内,尽可能让key所有的位都参与计算,这样映射出的哈希值更加均匀一些,因此,艾莉丝前面建议"M取不大接近2的整数次幂的一个质数",大多数数据结构书籍中写的基本是这个理论,我们实践中还是建议灵活运用。

4.2 乘法散列法(了解即可)

乘法散列法对哈希表大小M没有要求,这里介绍一下大思路,第一步:用关键字K乘上常数A(0 < A < 1),并抽取出k*A的小数部分第二步:后再用M乘以k * A的小数部分,再向下取整

h(key) = floor(M * ((A * key) % 1.0)),其中floor表示对表达式进行下取整,A(0 , 1),%1.0是为了取小数,这里最重要的是A的值应该如何设定,Knuth------这又是一位大佬------他认为A = (5 - 1) / 2 = 0.6180339887...(黄金分割点)比较好。

乘法散列法对哈希表大小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。

4.3 全域散列法(了解即可)

如果存在这样**一个恶意的对手,他针对我们提供的散列函数,特意构造出一个发生严重冲突的数据集,比如,让所有关键字全部落入同一个位置中------这种情况是可以存在的,只要散列函数是公开且确定的,就可以实现此攻击。****解决方法自然是见招拆招,给散列函数增加随机性,攻击者就无法找出确定可以导致最坏情况的数据。**这种方法叫做全域散列

hab(key) = ((a * key + 6) % 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。

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

4.4 其他方法(了解即可)

上面的几种方法是在《算法导论》这本书籍中讲解的方法。

《殷人昆数据结构:用面向对象方法与C++语言描述 (第二版)》和《[数据结构 (C语言版).严蔚
敏,吴伟民》等教材型书籍上面还给出了平方取中法、折叠法、随机数法、数学分析法等,这些方
法相对更适用于一些局限的特定场景,大家如果有兴趣可以去看看这些书籍。


5 ~> 处理哈希冲突

实践中哈希表一般还是选择除法散列法作为哈希函数,当然哈希表无论选择什么哈希函数也避免不了冲突,因为冲突是避免不了的,我们只能减少冲突,那么插入数据时,如何解决冲突呢?主要有两种方法,开放定址法和链地址法

5.1 开放定址法

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

5.1.1 线性探测(含堆积问题)

我们先简单谈谈"堆积 / 群积问题"------

如下图所示------

5.1.2 二次探测

5.1.3 双重探测

5.2 详解开放定址法代码

开放定址法在实践中是不如下面会介绍的链地址法的,因为开放定址法解决冲突不管使用哪种方法,占用的都是哈希表中的空间,始终存在互相影响的问题------正因如此,开放定址法我们简单选择线性探测实现即可。

5.2.1 哈希表结构

cpp 复制代码
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
{
private:
	vector<HashData<K, V>> _tables;
	size_t _n = 0; // 表中存储数据个数 
};

要注意的是这里需要给每个存储值的位置加一个状态标识,否则删除一些值以后,会影响后面冲突的值的查找。如下图所示,我们删除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。

5.2.2 扩容问题

这里我们哈希表负载因子控制在0.7,当负载因子到0.7以后我们就需要扩容了,我们还是按照2倍的方式扩容,但是同时我们要保持哈希表大小是一个质数,第一个是质数,2倍后就不是质数了。如何解决?一种方案就是上面在【除法散列法】中我们介绍过的JavaHashMap的使用2的整数次幂,但是计算时不能直接取模的改进方法;另外一种方案是SGI版本的哈希表使用的方法,给了一个近似2倍的质数表,每次去质数表获取扩容后的大小。

5.2.3 key不能取模的问题

当key是string / Date等类型时,key不能取模,我们需要给HashTable增加一个仿函数,这个仿函数支持把key转换成一个可以取模的整型,如果key可以转换为整型并且不容易冲突,那么这个仿函数就用默认参数即可,如果这个Key不能转换为整型,我们就需要自己实现一个仿函数传给这个参数,实现这个仿函数的要求就是尽量key的每值都参与到计算中,让不同的key转换出的整型值不同。string做哈希表的key非常常见,所以我们可以考虑把string特化一下------

cpp 复制代码
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
// 特化 
template<>
struct HashFunc<string>
{
	// 字符串转换成整形,可以把字符ascii码相加即可 
	// 但是直接相加的话,类似"abcd"和"bcad"这样的字符串计算出是相同的 
	// 这里我们使⽤BKDR哈希的思路,⽤上次的计算结果去乘以一个质数,这个质数一般去31, 131等效果会比较好
	size_t operator()(const string& key)
	{
		size_t hash = 0;
		for (auto e : key)
		{
			hash *= 131;
			hash += e;
		}
		return hash;
	}
};

template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:
private:
	vector<HashData<K, V>> _tables;
	size_t _n = 0; // 表中存储数据个数 
};

5.3 链地址法

5.3.1 对比开放定址法和链地址法

对比一下------

5.3.2 哈希桶概念及其示例

开放定址法中所有的元素都放到哈希表里,链地址法中所有的数据不再直接存储在哈希表中,哈希表中存储一个指针,没有数据映射这个位置时,这个指针为空,有多个数据映射到这个位置时,把这些冲突的数据链接成一个链表,挂在哈希表这个位置下面,像是一个个挂在晾衣杆的水桶,链地址法也叫做拉链法或者哈希桶。

以{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

5.3.3 扩容

开放定址法负载因子必须小于1,链地址法的负载因子就没有限制了,可以大于1。负载因子越大,哈希冲突的概率越高,空间利用率越高;负载因子越小,哈希冲突的概率越低,空间利用率越低。stl中unordered_xxx的最大负载因子基本控制在1(负载因子平均是1,但是这是理想的情况,当然没有那么平均,有的哈希桶不挂,有的挂2~3个),大于1就扩容,艾莉丝之后在代码实现中也使用这个方式进行扩容。

扩容有分散的意思在------平均每个桶的长度变短。

5.3.4 极端场景

如果在极端场景下,某个桶特别长怎么办?我们其实可以考虑使用全域散列法,这样就不容易被针对了。假设不是被针对了,此时虽然用了全域散列法,但是在偶然情况下,某个桶很长,查找效率很低怎么办呢?这里在Java8的HashMap中,当桶的长度超过一定阀值(8)时就把链表转换成红黑树,我们知道红黑树是近似平衡,时间复杂度很稳定,O(logN),而哈希在插入的时候会"抽风",而且随着插入次数增加,"抽风"程度会愈演愈烈------

不过,一般情况下,不断扩容,单个桶很长的场景还是比较少的,这个解决极端场景的思路,uu们这里可以了解一下。

换红黑树也是一种解决方案。

5.4 哈希桶代码实现

5.4.0 在私有定义成员变量

5.4.1 插入

5.4.2 查找

5.4.3 删除


完整代码示例与实践演示

HashTable.h:

cpp 复制代码
#pragma once
#include<vector>

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
};

inline unsigned long __stl_next_prime(unsigned long n)
{
	const unsigned long* first = __stl_prime_list;
	const unsigned long* last = __stl_prime_list + __stl_num_primes;
	// >= n
	const unsigned long* pos = lower_bound(first, last, n);
	return pos == last ? *(last - 1) : *pos;
}

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

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

		return hash;
	}
};

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

	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		State _state = EMPTY;
	};

	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable
	{
	public:
		HashTable()
			:_tables(__stl_next_prime(1))
		{
		}

		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
				return false;

			// 满了 / 快满了就要扩容,负载因子 >= 0.7就要扩容
			if ((double)_n / (double)_tables.size() >= 0.7) // 至少强转一个
			{
				//vector<HashData<K, V>> newTables(__stl_next_prime(_tables.size() * 2));
			//	//_tables.resize(); // 扩容可以直接resize吗?哈希表的扩容不是那么简单的,要重新分配
			//	std::vector<HashData> newtables(_tables.size());
			//	for (size_t i = 0; i < _tables, size(); i++)
			//	{
			//		if (_tables[i]._state == EXIST)
			//		{
			//			// 重新映射到新表
			//			// ...
			//		}
			//	}
			// 
			//	_tables.swap(newtables);

				HashTable<K, V, Hash> newht;
				newht._tables.resize(__stl_next_prime(_tables.size() + 1));
				for (size_t i = 0; i < _tables.size(); i++)
				{
					// 遍历旧表,旧表数据插入到newht
					if (_tables[i]._state == EXIST)
					{
						newht.Insert(_tables[i]._kv);
					}
				}
				_tables.swap(newht._tables);
			}

			Hash hs;
			// 插入的逻辑
			size_t hash0 = hs(kv.first) % _tables.size(); // 只能模size,不能模capacity
			// []访问必须要在size访问之内,模capacity放不进去(尽可能让capacity和size一致)
			// 线性探测
			size_t i = 1; // 第一次就不加了
			size_t hashi = hash0;
			while (_tables[hashi]._state == EXIST)
			{
				hashi = (hash0 + i) % _tables.size(); // 取模,回绕回去
				++i; // 不断加i
			}

			_tables[hashi]._kv = kv;
			_tables[hashi]._state = EXIST;
			++_n;

			return true;
		}

		HashData<K, V>* Find(const K& key)
		{
			Hash hs;
			size_t hash0 = hs(key) % _tables.size();
			//线性探测
			size_t i = 1;
			size_t hashi = hash0;
			while (_tables[hashi]._state != EMPTY)
			{
				if (_tables[hashi]._state != DELETE &&
					_tables[hashi]._kv.first == key)
				{
					return &_tables[hashi];
				}
				hashi = (hash0 + i) % _tables.size();
				++i;
			}

			return nullptr;
		}

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

	private:
		std::vector<HashData<K, V>> _tables; // 指针数组

		size_t _n = 0; // 存储的有效数据个数
	};
}

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(__stl_next_prime(1),nullptr)
			,_n(0)
		{ }

		// 析构,要单独实现
		~HashTable()
		{
			for (size_t i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				_tables[i] = nullptr;
			}

			_n = 0;
		}

		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
				return false;

			Hash hs; // 除余都要套上hs
			// 负载因子 == 1就开始扩容
			if (_n == _tables.size())
			{
				//HashTable<K, V> newht;
				//newht._tables.resize(_tables.size() * 2);
				//for (size_t i = 0; i < _tables.size(); i++)
				//{
				//	// 遍历旧表,旧表数据插入到newht
				//	Node* cur = _tables[i];
				//	while (cur)
				//	{
				//		newht.Insert(cur->_kv);
				//		cur = cur->_next;
				//	}
				//}
				//_tables.swap(newht._tables);

				std::vector<Node*> newtables(__stl_next_prime(_tables.size() + 1), nullptr);
				for (size_t i = 0; i < _tables.size(); i++)
				{
					// 遍历旧表,旧表节点重新映射,挪动到新表
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;

						// 头插
						size_t hashi = hs(cur->_kv.first) % newtables.size();
						cur->_next = newtables[hashi];
						newtables[hashi] = cur;

						cur = next;
					}
					_tables[i] = nullptr;
				}

				_tables.swap(newtables);
			}

			size_t hashi = hs(kv.first) % _tables.size();
			// 头插
			Node* newnode = new Node(kv);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;

			++_n; // 插入,_n是有效数据个数,要++
			return true;
		}

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

				cur = cur->_next;
			}

			return nullptr;
		}

		bool Erase(const K& key)
		{
			Hash hs;
			size_t hashi = hs(key) % _tables.size();
			Node* prev = nullptr;
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					// 删除
					if (prev == nullptr)
					{
						// 哈希桶中的第一个节点
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}

					--_n; // _n是有效数据个数,每次删除之后都要减减
					delete cur;
					return true;
				}

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

			return false;
		}

	private:
		std::vector<Node*> _tables; // 指针数组

		size_t _n; // 存储的有效数据个数
		//std::vector<std::list<K, V>> _tables;
		// 不是实现不了,而是这种实现太绕了,而且比较抽象,现阶段对我们来说还是太难了
	};
}

Test.cpp:

cpp 复制代码
#define  _CRT_SECURE_NO_WARNINGS  1

#include<iostream>
#include<unordered_map>
using namespace std;

#include"HashTable.h"

namespace open_address
{ 
void TestHT1()
{
	int a[] = { 19,30,5,36,13,20,21,12,58 };
	HashTable<int, int> ht;
	for (auto e : a)
	{
		ht.Insert({ e,e });
	}

	ht.Insert({ 2,2 });
	ht.Insert({ 22,22 });

	cout << ht.Find(5) << endl;
	cout << ht.Find(58) << endl;
	
	ht.Erase(5);

	cout << ht.Find(5) << endl;
	cout << ht.Find(58) << endl;

	//for (size_t i = 0; i < 100; i++)
	//{
	//	ht.Insert({ rand(),i });
	//}
}

	struct HashFuncString
	{
		// BKDR
		size_t operator()(const string& key)
		{
			size_t hash = 0;
			for (auto ch : key)
			{
				hash += ch;
				hash *= 131;
			}
			return hash;
		}
	};

	void TestHT2()
	{
		//HashTable<string, string, HashFuncString> dict;
		HashTable<string, string> dict;
		dict.Insert({ "string","字符串" }); // string无法取模
		dict.Insert({ "string","字符串1" });
		dict.Insert({ "left","左边" });
		dict.Insert({ "right","右边" });

		cout << dict.Find("string") << endl;
		cout << dict.Find("left") << endl;
		cout << dict.Find("left ") << endl;

		HashFuncString hfs;
		cout << hfs("abcd") << endl;
		cout << hfs("acbd") << endl;
		cout << hfs("aadd") << endl;

		unordered_map<string, string> dictmap;
		dictmap.insert({ "string","字符串" });

		// 编译报错,需要自己实现Hash的仿函数把key转成整形
		//unordered_map<pair<string, int>, string> um;
		//um.insert({ {"string", 1}, "字符串" });
	}
}

namespace Hash_bucket
{
	void TestHT1()
	{
		int a[] = { 19,30,5,36,13,20,21,12,58 };
		HashTable<int, int> ht;
		for (auto e : a)
		{
			ht.Insert({ e,e });
		}

		ht.Insert({ 2,2 });
		ht.Insert({ 22,22 });
		ht.Insert({ 44,44 });

		// 这两个过了就说明代码没问题了:先删58再删36
		ht.Erase(58);
		ht.Erase(36);
	}

	void TestHT2()
	{
		HashTable<string, string> dict;
		dict.Insert({ "string","字符串" }); // string无法取模
		dict.Insert({ "string","字符串1" });
		dict.Insert({ "left","左边" });
		dict.Insert({ "right","右边" });

		cout << dict.Find("string") << endl;
		cout << dict.Find("left") << endl;
		cout << dict.Find("left ") << endl;
	}
}

int main()
{
	// open_address::TestHT1();
	//open_address::TestHT2();

	Hash_bucket::TestHT1();
	//Hash_bucket::TestHT2();

	return 0;
}

运行结果

open_address::TestHT1():

open_address::TestHT2():

Hash_bucket::TestHT1():

Hash_bucket::TestHT2():


博主手记

这是博主的学习笔记,大家可以看看------


结尾

uu们,本文的内容到这里就全部结束了,艾莉丝再次感谢您的阅读!

往期回顾:

结语:既然都看到这里啦!请大佬不要忘记给博主来个"一键四连"哦!

🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡

૮₍ ˶ ˊ ᴥ ˋ˶₎ა

相关推荐
蓝天智能1 小时前
立体字型效果的Qt QSS实现
开发语言·qt
虾..1 小时前
C++ 特殊类的设计
开发语言·c++
拾忆,想起2 小时前
Dubbo超时问题排查与调优指南:从根因到解决方案
服务器·开发语言·网络·微服务·架构·php·dubbo
7ioik3 小时前
什么是线程池?线程池的作用?线程池的四种创建方法?
java·开发语言·spring
晨非辰3 小时前
数据结构排序系列指南:从O(n²)到O(n),计数排序如何实现线性时间复杂度
运维·数据结构·c++·人工智能·后端·深度学习·排序算法
寻星探路3 小时前
JavaSE重点总结后篇
java·开发语言·算法
Charles_go4 小时前
C#中级8、什么是缓存
开发语言·缓存·c#
残影飞雪4 小时前
Jetson版本下Pytorch和torchvision
c++
松涛和鸣5 小时前
14、C 语言进阶:函数指针、typedef、二级指针、const 指针
c语言·开发语言·算法·排序算法·学习方法