C++-哈希Hash

本期我们来学习哈希

目录

unordered系列关联式容器

unordered_map

unordered_set

性能比较

哈希概念

哈希冲突

哈希函数

哈希冲突解决

闭散列

模拟实现

开散列

模拟实现

全部代码


unordered系列关联式容器

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

unordered_map

C++-哈希Hash_第1张图片

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

unordered是无序的意思,其实这里是早期取名时没取好,应该像java一样,这里应该叫hashmap,hashset,以及treemap和treeset,这是历史问题,我们了解即可

 我们首先要知道,set以及map的底层是红黑树,而unorderedmap和unorderedset底层是哈希表,使用它们是非常简单的

我们先简单看看

C++-哈希Hash_第2张图片

C++-哈希Hash_第3张图片

还有各种接口,它和map几乎相同,区别有以下几点,1.unordered_xxx是单向迭代器,2.unordered_xxx遍历出来不是有序的,3.unordered_xxx的性能更高一些

下面我们简单使用一下,我们先看看set

C++-哈希Hash_第4张图片

和set没啥区别,只是输出无序了,也就是说,它可以去重,但不能排序

C++-哈希Hash_第5张图片

再简单看看map的,也是一样

unordered_set

C++-哈希Hash_第6张图片

相比于set,和unordered_map和map的区别以及用法是一样的,这里就不过多介绍

性能比较

下面我们来比较测试一下unordered和原来的map和set性能差距,下面是测试代码,就是产生一堆随机数,然后插入查找等等,我们首先测插入

const size_t N = 10000;
	unordered_set us;
	set s;
	vector v;
	v.reserve(N);
	srand(time(0));
	for (size_t i = 0; i < N; ++i)
	{
		v.push_back(rand());
		//v.push_back(rand()+i);
		//v.push_back(i);
	}

	size_t begin1 = clock();
	for (auto e : v)
	{
		s.insert(e);
	}
	size_t end1 = clock();
	cout << "set insert:" << end1 - begin1 << endl;
	
	size_t begin2 = clock();
	for (auto e : v)
	{
		us.insert(e);
	}
	size_t end2 = clock();
	cout << "unordered_set insert:" << end2 - begin2 << endl;
	
	/*size_t begin3 = clock();
	for (auto e : v)
	{
		s.find(e);
	}
	size_t end3 = clock();
	cout << "set find:" << end3 - begin3 << endl;
	
	size_t begin4 = clock();
	for (auto e : v)
	{
		us.find(e);
	}
	size_t end4 = clock();
	cout << "unordered_set find:" << end4 - begin4 << endl << endl;
	
	cout <<"插入数据个数:"<< s.size() << endl;
	cout <<"插入数据个数:" << us.size() << endl << endl;;
	
	size_t begin5 = clock();
	for (auto e : v)
	{
		s.erase(e);
	}
	size_t end5 = clock();
	cout << "set erase:" << end5 - begin5 << endl;
	
	size_t begin6 = clock();
	for (auto e : v)
	{
		us.erase(e);
	}
	size_t end6 = clock();
	cout << "unordered_set erase:" << end6 - begin6 << endl << endl;
		*/
	return 0;

C++-哈希Hash_第7张图片

这是1w个数据时,是看不出什么的

C++-哈希Hash_第8张图片

C++-哈希Hash_第9张图片

再看10w和100w,差距就开始明显了 ,下面我们再把查找删除也加上

C++-哈希Hash_第10张图片

因为随机数会产生重复(随机数最多3w个),所以这里我们插入10w,实际上只有3w,插入是unordered快,查找和删除也是更快,整体上优势

 C++-哈希Hash_第11张图片

100w也是,实际上只插入了3w数据

C++-哈希Hash_第12张图片

所以我们改用随机数+i,这样就有60w数据了,我们看性能,这时候差距小了很多,也就是说重复数据多的时候,unordered更占优势,重复数据不多时,优势就不明显了

C++-哈希Hash_第13张图片

当数据量达到1000w时,unordered甚至没优势了,不过我们发现,find是非常快的

C++-哈希Hash_第14张图片

我们再看看,当数据是有序时,插入1000w就是1000w,set更优一点,因为set底层是红黑树, 一直在旋转,高度不会太高,所以效率就会很高

所以世界是没有完美的东西的,大家根据情况选择即可,我们日常中10w量级和百万量级使用更多一点,或者有时候重复数据过多,unordered的效率都是更高的

哈希概念

顺序结构以及平衡树 中,元素关键码与其存储位置之间没有对应的关系,因此在 查找一个元素 时,必须要经过关键码的多次比较 顺序查找时间复杂度为 O(N) ,平衡树中为树的高度,即 O($log_2 N$) ,搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以 不经过任何比较,一次直接从表中得到要搜索的元素
如果构造一种存储结构,通过某种函数 (hashFunc) 使元素的存储位置与它的关键码之间能够建立 一一映射的关系,那么在查找时通过该函数可以很快找到该元素
当向该结构中:
插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置
取元素比较,若关键码相等,则搜索成功
该方式即为哈希 ( 散列 ) 方法, 哈希方法中使用的转换函数称为哈希 ( 散列 ) 函数,构造出来的结构称 为哈希表 (Hash Table)( 或者称散列表 )
例如:数据集合 {1 7 6 4 5 9}
哈希函数设置为: hash(key) = key % capacity ; capacity 为存储元素底层空间总的大小。

我们来看看以前我们在查找一个数时使用的方法,首先是暴力查找,我们都知道,过于低效,于是就有人发明了二分查找,效率就提升了很多,但是缺点是需要先排序,而且增删查改不方便,于是就有了平衡搜索树,不过也有缺点,所以就又有人发明了哈希,也叫散列表,它的本质是,存储的值和存储的位置建立出一个对应关系

我们举个例子,我们之前实现过计数排序,这就是一种哈希

C++-哈希Hash_第15张图片

比如这里,我们插入15时,用15减去15(最小值),就插入在0的位置,我们查找时也是一样,直接就可以在对应位置上找到,所以哈希的查找要更快一点

再举一个实际的例子,我们在图书馆找书就是一种哈希,比如图书馆的书是按字母顺序存放的,我们要找《西游记》的话,我们需要去x区(西的拼音开头x),到了x区,还会再次划分,我们要在x区里找y区(游的拼音开头y),这也是一种哈希

上面的计数排序也是有缺点的,比如数据很分散时就完了,我们统计字母时还好,只有26个,而我们统计数据时

就像这种,这可就不好玩了,我们上面的计数排序,以及图书馆都是直接定址法

而我们该如何把上面的数据存进去呢?这里要用到除留余数法,我们把数据%20即可,比如15%20就在15的位置,27%20就在7的位置

哈希冲突

但是这种方法会有一个缺陷,叫做哈希冲突/碰撞,不同的值可能会映射到同一个位置,值和位置是多对一的关系

我们可以用线性探测的方法来存储,比如1%20在1的位置,21%20也在1的位置,1的位置已经被1占了,所以我们就把21放在1的后面,如果后面也被占了,我们就继续往后走,直到有位置存放,不过这种方式也有缺陷,会影响别的数据

还有一种方法是哈希桶,我们把同一个位置的数据链接在一起,放在一个链表里,这种方法更主流一点,下面我们看看除了直接定址法和除留余数法还有哪些方法

哈希函数

引起哈希冲突的一个原因可能是: 哈希函数设计不够合理
哈希函数设计原则
哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有 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) 等方法。
数字分析法通常适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的
若干位分布较均匀的情况
注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突

 下面我们来看解决哈希冲突的方案

哈希冲突解决

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

闭散列

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

线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
插入:
通过哈希函数获取待插入元素在哈希表中的位置
如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突, 使用线性探测找到下一个空位置,插入新元素

C++-哈希Hash_第16张图片

 插入如上图所示,如果我们要在里面再插入一个15,会一直走,会走到数组结尾还没有位置,那它会绕回到数组的开头,也就是在0位置插入,只有整个数组满的时候才会扩容

我们再看一个问题,假设我们要查找44,我们用44%10得到4,结果在4的位置没有,那就继续往后找,直到找到为止,如果要找的是54,我们会一直找,直到找到空位置停下(注意,不是一圈,如果是一圈就和暴力查找没区别了),在上图中,我们会找到0的位置,然后停止

这里也侧面说明了一个问题,如果这个表太满,那么查找一些值(比如不存在的值)的效率会大幅度下降,所以我们不能让它太满

还有一个问题,如果我们要删除6,我们要直接删除吗?答案是不行的,删除6后应该在这里填充一个值,不然我们查找元素(如44)时,就会卡bug,删除是会影响前后值的,该如何解决呢?我们应该使用一个标记,而且不能用值标记,应该用一个状态,这里我们会有三种状态,存在,空和删除

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

模拟实现

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

template
struct HashData
{
	pair _kv;
	STATE _state = EMPTY;
};

template
class HashTable
{
private:
	vector> _table;
	size_t _n;
};

 首先我们先写出框架,它的底层结构是一个数组,大家观察可以发现,vector里是有size的,我们还写了一个n,这是为什么呢?这和后续扩容之类有关,是存储有效数据的个数

C++-哈希Hash_第17张图片

我们先来实现insert,我们看hashi,这里我们应该%capacity还是size?

答案是size

C++-哈希Hash_第18张图片

如果capacity是20,size是15,如果%capacity是可能出现比size大的,比如出现18,下面方括号检测是会报错的(vector只能访问0到size-1)

bool Insert(const pair& kv)
	{
		//线性探测
		size_t hashi = kv.first % _table.size();
		while (_table[hashi]._state == EXIST)
		{
			++hashi;
			hashi %= _table.size();//防止越界,这样可以回到数组开头
		}
		_table[hashi]._kv = kv;
		_table[hashi]._state = EXIST;
		++_n;
		return true;
	}

这是基础版本,while条件是等于存在,如果不存在,或者删除,说明那个位置是空,可以插入,++hashi是防止越界,下面来解决其他问题,比如太满的情况,是需要扩容的

C++-哈希Hash_第19张图片

这里引入一个新概念叫载荷因子,也叫负载因子,代表插入元素占空间的比例,满载的程度,负载因子越大,哈希冲突的概率就越大,越小冲突概率就越小,比如100个空间,我们只存了10个数据,我们再插入数据时,冲突的概率是很小的,而当我们插入90个数据后,再插入的话冲突的概率就非常大了,不过负载因子越大,空间利用率也越高,所以也不全是坏处,考虑折中,所以当负载因子在0.7到0.8时扩容是最好的,比如java里是0.75,也就是说空间利用率最大是百分之75

C++-哈希Hash_第20张图片

我们写一个构造函数,这样size最开始就不是0了,然后我们来写扩容,扩容完后还有一个问题,扩容后有些值的映射是会改变的,比如原来10个空间,我们插入1和111,它们在1和2的位置,而当我们扩容到20,那么111的位置就应该到11,所以扩容后我们需要重新映射

 我们应该直接开一个新的vector,然后重新插入(不是拷贝,要改变映射)

HashTable()
	{
		_table.resize(10);
	}
	bool Insert(const pair& kv)
	{
		//扩容
		//if ((double)_n / _table.size() >= 0.7)
		if (_n*10 / _table.size() >= 7)
		{
			size_t newSize = _table.size() * 2;
			//重新映射
			HashTable newHT;
			newHT._table.resize(newSize);
			for (size_t i = 0; i < _table.size(); i++)
			{
				if (_table[i]._state == EXIST)
				{
					newHT.Insert(_table[i]._kv);
				}
			}
			_table.swap(newHT._table);
		}
		//线性探测
		size_t hashi = kv.first % _table.size();
		while (_table[hashi]._state == EXIST)
		{
			++hashi;
			hashi %= _table.size();//防止越界,这样可以回到数组开头
		}
		_table[hashi]._kv = kv;
		_table[hashi]._state = EXIST;
		++_n;
		return true;
	}

最终是这样的,我们新开一个哈希表,然后把旧表里的数据重新插入进去,接着把新表给旧的即可

下面我们再来实现find

pair* Find(const K& key)
	{
		//线性探测
		size_t hashi = key % _table.size();
		while (_table[hashi]._state != EMPTY)
		{
			if (_table[hashi]._state == EXIST
				&& _table[hashi]._kv.first == key)
			{
				return &_table[hashi]._kv;
			}
			++hashi;
			hashi %= _table.size();
		}
		return nullptr;
	}

条件变了一下,遇到空的时候返回false,不是空就继续走,如果遇到要查找的值,首先我们要看看它是否存在

下面实现删除

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

删除就比较简单了,状态改一改即可

此时我们的哈希表还有问题,那就是key是可以被修改的

C++-哈希Hash_第21张图片

我们期望是key不能被修改,value可以,下面来解决这个问题

C++-哈希Hash_第22张图片

我们在这些地方加上const

 C++-哈希Hash_第23张图片

然后在find的return这里强转一下 

我们的问题还没有全部解决

C++-哈希Hash_第24张图片

这里的key是不一定能取模的,如果是string之类的该怎么办呢?

C++-哈希Hash_第25张图片

我们增加一个仿函数

然后在这些取模的地方调用,此时还是不能处理string等等的,我们先对整形进行测试

C++-哈希Hash_第26张图片

没有问题

C++-哈希Hash_第27张图片

接着我们加一个string的

C++-哈希Hash_第28张图片

接着我们修改这里,按需实例化

C++-哈希Hash_第29张图片

此时就没有问题了

C++-哈希Hash_第30张图片

我们还有更好的办法,就是特化,这是特化的使用方法,大家看一下

C++-哈希Hash_第31张图片

有了特化,我们不传stringHashFunc也没有问题

其实我们的问题还没有解决,我们上面用string的第一个字母的ASCII码取模,这也是不好的,因为第一个字母相同的就全冲突了

C++-哈希Hash_第32张图片

我们把所有的字母加起来,但是这样写还是有一些问题,我们看一看

C++-哈希Hash_第33张图片

还是有冲突的

字符串转为整形在实际中还是非常常见的,于是就有大佬对这些进行研究,就有了字符串哈希算法

这里参考大佬整理的

各种字符串Hash函数 - clq - 博客园 (cnblogs.com)

 根据最后的算法比较,我们选择BKDR哈希,AP哈希和DJB哈希这三个得分最高的,一会会说为什么选择3个

C++-哈希Hash_第34张图片

BKDR是*131

C++-哈希Hash_第35张图片

这样就不冲突了

至于这些算法的原理,我们也不探讨,这是数学方面的知识,如果感兴趣的话可以去看看大佬们当年发表的论文等等

另外这里字符串过长是会溢出的,不过不影响,是会截断的,插入时截断,查找时自然也截断,我们这里不需要算出准确值,只需要对应一下位置而已

我们上面的方法是闭散列的开放定址法,核心思路是线性探测,是有缺陷的,就是容易堵塞,比如我们插入4,5,6,7,后再插入25,44,位置就都被占完了,基于这种情况就有人又想到了二次探测,二次探测是探测二次方,举个例子,我们之前是hashi = key % n,然后hashi + i,二次探测是hashi + i^2,这样相对会分散一点,我们了解一下即可

开放定址法的缺点是冲突会互相影响,为了解决这个问题,就有了开散列,也叫拉链法或者哈希桶

开散列

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

 我们是开一个指针数组,然后插入时把它挂起来

C++-哈希Hash_第36张图片

就像这样,如果冲突比较多,那么也就是桶比较长而已

C++-哈希Hash_第37张图片

其实在库里面也是叫桶的,下面我们来实现它

模拟实现

namespace hash_bucket
{
	template
	struct HashNode
	{
		pair _kv;
		HashNode* _next;
	};

	template
	class HashTable
	{
		typedef HashNode Node;
	private:
		vector _table; //指针数组
		size_t _n; //有效数据
	};
}

一样的,我们先把框架写出来,这里有一个问题,我们在hashitable里使用了vector,那么可以用vector吗?答案是可以的,不过没必要,这里使用单链表即可,而且使用list还不容易实现迭代器

        HashTable()
		{
			_table.resize(10,nullptr);
		}
		bool Insert(const pair& kv)
		{
			size_t hashi = kv.first % _table.size();
			//头插
			Node* newnode = new Node(kv);
			newnode->_next = _table[hashi];
			_table[hashi] = newnode;
			++_n;
			return true;
		}

接着就是构造函数和insert,这里insert的头插看起来很怪,只有两句话,但就是这样的,大家可以画一下,大家想想这里用list的话,就会很麻烦了

另外,大家再想想这里要不要扩容呢?答案是需要的,因为不断插入的话,某些桶就会越来越长,就和链表没有差异了,效率就得不到保证,所以这里也需要一个负载因子,这里的负载因子可以适当放大一点,这里一般控制到1,也就意味着平均下来每个桶一个数据,不过这是理想状况,大多数桶的数据都是常数个,所以效率还是很高的

C++-哈希Hash_第38张图片

大家想一想,我们扩容和之前一样,把旧表的数据再插入到新表,这样好吗?

答案是不好的

C++-哈希Hash_第39张图片

我们重新插入时,是需要新开一个节点,比如这里的1和111,然后挂上去,而且我们后面还需要把旧表的节点一个一个释放掉 

bool Insert(const pair& kv)
		{
			//负载因子到1时扩容
			if (_n == _table.size())
			{
				size_t newSize = _table.size() * 2;
				vector newTable;
				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 = cur->_kv.first % newSize;
						cur->_next = newTable[hashi];
						newTable[hashi] = cur;
						cur = next;
					}
					_table[i] = nullptr;
				}
				_table.swap(newTable);
			}
			size_t hashi = kv.first % _table.size();
			//头插
			Node* newnode = new Node(kv);
			newnode->_next = _table[hashi];
			_table[hashi] = newnode;
			++_n;
			return true;
		}

我们直接把旧表的节点拿下来放到新表即可,这里可能不太好理解,大家最好画图理解一下

        void Print()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				printf("[%d]->", i);
				Node* cur = _table[i];
				while (cur)
				{
					cout << cur->_kv.first << "->";
					cur = cur->_next;
				}
				printf("NULL\n");
			}
		}

我们再写一个print方便打印测试,这里使用printf更方便一点

C++-哈希Hash_第40张图片

 没有问题,我们再插入几个值,看看扩容后怎么样

C++-哈希Hash_第41张图片

没有问题 

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

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

接着我们来实现find和erase,find没什么好说的,erase需要分情况讨论,假设我们找到的key在桶里是唯一的一个元素(桶中只有一个key),那么这里就是头删,否则我们需要把前一个节点的next指向下一个,然后删除即可,写完find后,我们还需要在insert前加上

C++-哈希Hash_第42张图片

就像这样

C++-哈希Hash_第43张图片

我们再测试一下,也没有问题

大家想一想,我们之前的开放定址法需要写析构函数吗?

答案是不需要的

C++-哈希Hash_第44张图片

它是一个vector,里面是一个data,对自定义类型会调用析构

而我们的哈希桶要写析构吗?

答案是需要的

C++-哈希Hash_第45张图片

对于内置类型,不做处理,对于自定义类型会调用它的析构,桶的起始位置存的是一个节点的指针,是一个内置类型,所以必须要我们来释放空间

        ~HashTable()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				Node* cur = _table[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = cur->_next;
				}
				_table[i] = nullptr;
			}
		}

这是析构函数,下面我们要让哈希桶和开散列一样,可以支持string等等类型

C++-哈希Hash_第46张图片

首先我们先把这两个函数放到全局

C++-哈希Hash_第47张图片

然后就和之前一样,给所有的取模都加上hf即可

 ​​​​​C++-哈希Hash_第48张图片

修改一下print函数,kv都打印出来,我们测试一下,没有问题

这里保留了几个问题,接下来就是封装了,我们下一期再讲

全部代码

#pragma once
#include
#include
using namespace std;
//Hash.h
template
struct DefaultHashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

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

namespace open_address {
	enum STATE //状态
	{
		EXIST,
		EMPTY,
		DELETE
	};

	template
	struct HashData
	{
		pair _kv;
		STATE _state = EMPTY;
	};

	//struct stringHashFunc
	//{
	//	size_t operator()(const string& str)
	//	{
	//		return str[0];
	//	}
	//};

	template>
	class HashTable
	{
	public:
		HashTable()
		{
			_table.resize(10);
		}
		bool Insert(const pair& kv)
		{
			if(Find(kv.first))
			{
				return false;
			}
			//扩容
			//if ((double)_n / _table.size() >= 0.7)
			if (_n * 10 / _table.size() >= 7)
			{
				size_t newSize = _table.size() * 2;
				//重新映射
				HashTable newHT;
				newHT._table.resize(newSize);
				for (size_t i = 0; i < _table.size(); i++)
				{
					if (_table[i]._state == EXIST)
					{
						newHT.Insert(_table[i]._kv);
					}
				}
				_table.swap(newHT._table);
			}
			//线性探测
			HashFunc hf;
			size_t hashi = hf(kv.first) % _table.size();
			while (_table[hashi]._state == EXIST)
			{
				++hashi;
				hashi %= _table.size();//防止越界,这样可以回到数组开头
			}
			_table[hashi]._kv = kv;
			_table[hashi]._state = EXIST;
			++_n;
			return true;
		}
		HashData* Find(const K& key)
		{
			//线性探测
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();
			while (_table[hashi]._state != EMPTY)
			{
				if (_table[hashi]._state == EXIST
					&& _table[hashi]._kv.first == key)
				{
					return (HashData*) & _table[hashi];
				}
				++hashi;
				hashi %= _table.size();
			}
			return nullptr;
		}
		bool Erase(const K& key)
		{
			HashData* ret = Find(key);
			if (ret)
			{
				ret->_state = DELETE;
				--_n;
				return true;
			}
			return false;
		}
	private:
		vector> _table;
		size_t _n; //存储有效数据的个数
	};
}

namespace hash_bucket
{
	template
	struct HashNode
	{
		pair _kv;
		HashNode* _next;
		HashNode(const pair& kv)
			:_kv(kv)
			,_next(nullptr)
		{}
	};

	template>
	class HashTable
	{
		typedef HashNode Node;
	public:
		HashTable()
		{
			_table.resize(10,nullptr);
		}

		~HashTable()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				Node* cur = _table[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = cur->_next;
				}
				_table[i] = nullptr;
			}
		}

		bool Insert(const pair& kv)
		{
			if (Find(kv.first))
			{
				return false;
			}
			HashFunc hf;
			//负载因子到1时扩容
			if (_n == _table.size())
			{
				size_t newSize = _table.size() * 2;
				vector newTable;
				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->_kv.first) % newSize;
						cur->_next = newTable[hashi];
						newTable[hashi] = cur;
						cur = next;
					}
					_table[i] = nullptr;
				}
				_table.swap(newTable);
			}
			size_t hashi = hf(kv.first) % _table.size();
			//头插
			Node* newnode = new Node(kv);
			newnode->_next = _table[hashi];
			_table[hashi] = newnode;
			++_n;
			return true;
		}

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

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

		void Print()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				printf("[%d]->", i);
				Node* cur = _table[i];
				while (cur)
				{
					cout << cur->_kv.first << ":" <_kv.second <<"->";
					cur = cur->_next;
				}
				printf("NULL\n");
			}
			cout << endl;
		}
	private:
		vector _table; //指针数组
		size_t _n = 0; //有效数据
	};
}

以上即为本期全部内容,希望大家可以有所收获

如有错误,还请指正

你可能感兴趣的:(哈希算法,算法,c++,数据结构)