C++-哈希Hash

news2024/12/22 22:46:05

本期我们来学习哈希

目录

unordered系列关联式容器

unordered_map

unordered_set

性能比较

哈希概念

哈希冲突

哈希函数

哈希冲突解决

闭散列

模拟实现

开散列

模拟实现

全部代码


unordered系列关联式容器

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

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. 它的迭代器至少是前向迭代器

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

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

我们先简单看看

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

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

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

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

unordered_set

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

性能比较

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

const size_t N = 10000;
	unordered_set<int> us;
	set<int> s;
	vector<int> 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;

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

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

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

 

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

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

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

我们再看看,当数据是有序时,插入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 为存储元素底层空间总的大小。

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

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

比如这里,我们插入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 存放到冲突位置中的 下一个 空位置中去。 那如何寻找下一个空位置呢?

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

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

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

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

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

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

模拟实现

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>> _table;
	size_t _n;
};

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

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

答案是size

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

bool Insert(const pair<K, V>& 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是防止越界,下面来解决其他问题,比如太满的情况,是需要扩容的

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

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

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

HashTable()
	{
		_table.resize(10);
	}
	bool Insert(const pair<K, V>& kv)
	{
		//扩容
		//if ((double)_n / _table.size() >= 0.7)
		if (_n*10 / _table.size() >= 7)
		{
			size_t newSize = _table.size() * 2;
			//重新映射
			HashTable<K, V> 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<K, V>* 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<K, V>* ret = Find(key);
		if (ret)
		{
			ret->_state = DELETE;
			--_n;
			return true;
		}
		return false;
	}

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

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

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

我们在这些地方加上const

 

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

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

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

我们增加一个仿函数

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

没有问题

接着我们加一个string的

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

此时就没有问题了

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

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

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

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

还是有冲突的

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

这里参考大佬整理的

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

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

BKDR是*131

这样就不冲突了

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

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

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

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

开散列

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

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

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

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

模拟实现

namespace hash_bucket
{
	template<class K, class V>
	struct HashNode
	{
		pair<K, V> _kv;
		HashNode<K, V>* _next;
	};

	template<class K,class V>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	private:
		vector<Node*> _table; //指针数组
		size_t _n; //有效数据
	};
}

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

        HashTable()
		{
			_table.resize(10,nullptr);
		}
		bool Insert(const pair<K, V>& 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,也就意味着平均下来每个桶一个数据,不过这是理想状况,大多数桶的数据都是常数个,所以效率还是很高的

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

答案是不好的

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

bool Insert(const pair<K, V>& kv)
		{
			//负载因子到1时扩容
			if (_n == _table.size())
			{
				size_t newSize = _table.size() * 2;
				vector<Node*> 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更方便一点

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

没有问题 

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前加上

就像这样

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

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

答案是不需要的

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

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

答案是需要的

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

        ~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等等类型

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

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

 ​​​​​

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

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

全部代码

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

template<>
struct DefaultHashFunc<string>
{
	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<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		STATE _state = EMPTY;
	};

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

	template<class K, class V, class HashFunc = DefaultHashFunc<K>>
	class HashTable
	{
	public:
		HashTable()
		{
			_table.resize(10);
		}
		bool Insert(const pair<K, V>& 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<K, V, HashFunc> 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<const K, V>* 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<const K, V>*) & _table[hashi];
				}
				++hashi;
				hashi %= _table.size();
			}
			return nullptr;
		}
		bool Erase(const K& key)
		{
			HashData<const K, V>* ret = Find(key);
			if (ret)
			{
				ret->_state = DELETE;
				--_n;
				return true;
			}
			return false;
		}
	private:
		vector<HashData<K, V>> _table;
		size_t _n; //存储有效数据的个数
	};
}

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 HashFunc = DefaultHashFunc<K>>
	class HashTable
	{
		typedef HashNode<K, V> 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<K, V>& kv)
		{
			if (Find(kv.first))
			{
				return false;
			}
			HashFunc hf;
			//负载因子到1时扩容
			if (_n == _table.size())
			{
				size_t newSize = _table.size() * 2;
				vector<Node*> 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 << ":" <<cur->_kv.second <<"->";
					cur = cur->_next;
				}
				printf("NULL\n");
			}
			cout << endl;
		}
	private:
		vector<Node*> _table; //指针数组
		size_t _n = 0; //有效数据
	};
}

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

如有错误,还请指正

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

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

相关文章

知识图谱小白入门(1):neo4j的安装与CQL的使用

文章目录 序一、安装neo4j1.1 下载neo4j1.2 安装JDK1.3 BUG&#xff1a;dbms failed to start 二、CQL语法2.1 CQL语法2.2 习题 习题答案 序 知识图谱&#xff0c;是一种实体间的信息与关系知识的网状结构&#xff0c;借用图论中点与边的概念进行组建&#xff0c;易于结构化和…

生信教程:ABBA-BABA分析之滑动窗口

简介 ABBA BABA 统计&#xff08;也称为 D 统计&#xff09;为偏离严格的分叉进化历史提供了简单而有力的检验。因此&#xff0c;它们经常用于使用基因组规模的 SNP 数据测试基因渗入。 虽然最初开发用于基因渗入的全基因组测试&#xff0c;但它们也可以应用于较小的窗口&#…

Qt地铁智慧换乘系统浅学(四 )实现添加线路,添加站点,添加边 并且存储到本地txt文件

玩的就是添加 添加前的构思界面设计tabWidget添加线路界面添加站点界面添加边界面 代码实现添加线路思路连接槽函数槽函数 添加站点思路连接槽函数初始化combox槽函数更新容器函数 添加边思路槽函数 和代码 注意 添加前的构思 假设 现要添加一个线路 &#xff1a; 9号线 如果…

[H5动画制作系列 ]帧代码运行顺序测试

刚开始接触Animate CC&#xff08;过去叫&#xff1a;Flash&#xff09;,对于帧代码的执行顺序十分迷惑。所以,专门做一个简单代码顺序测试. 准备工作: 代码图层actions&#xff0c;第1帧和第10帧为关键帧。 背景图层bg&#xff0c;就一个字符串红色Test.界面如下: 代码测试步…

八个不可不知的SQL高级方法

结构化查询语言&#xff08;SQL&#xff09;是一种广泛使用的工具&#xff0c;用于管理和操作数据库。基本的SQL查询简单易学&#xff0c;但掌握高级SQL技术可以将您的数据分析和管理能力提升到新的高度。 高级SQL技术是指一系列功能和函数&#xff0c;使您能够对数据执行复杂…

Zero-Shot Learning by Harnessing Adversarial Samples 理论 代码解读

《Zero-Shot Learning by Harnessing Adversarial Samples》基于对抗样本的零样本学习 该论文要解决的问题&#xff1a; 减轻了传统图像增强技术中固有的语义失真问题。我们希望我们的实验研究将有助于理解单标签监督和语义属性监督在模型行为上的差异&#xff0c;并为开发更…

10.01

服务器 #include<myhead.h> //键盘输入事件 int keybord_events(fd_set readfds) {char buf[128] "";int sndfd -1; //从终端获取一个文件描述符&#xff0c;发送数据给该文件描述符对应的客户端bzero(buf, sizeof(buf));int res scanf("…

Junit的常用操作

注:本篇文章讲解的是junit5 目录 Juint是什么 Juint需要导入的依赖 Juint常用注解 Junit执行顺序 参数化 断言 测试套件 Juint是什么 Juint 是 Java 的一个单元测试框架. 也是回归测试框架. 使用 Junit 能让我们快速的完成单元测试。 注意&#xff1a;Junit 测试也是程序…

网络安全渗透测试工具之skipfish

网络安全渗透测试工具skipfish介绍 在数字化的时代,Web 应用程序安全成为了首要任务。想象一下,您是一位勇敢的安全冒险家,迎接着那些隐藏在 Web 应用程序中的未知风险。而在这个冒险之旅中,您需要一款强大的工具来帮助您发现漏洞,揭示弱点。而这个工具就是 Skipfish。 …

【Android】安卓手机系统内置应用安装失败解决方案

现有的闲置手机有个内置app可老旧了&#xff0c;没有开发者维护&#xff0c;于是问题不断&#xff0c;影响了体验&#xff0c;后来在网上查找发现有它的新版本&#xff0c;想要更新却没有自动更新&#xff08;后台服务断开了&#xff09;&#xff0c;有类似的想法可以来这里了解…

国庆创作周 组播《第十二课》

国庆创作周《第十二课》图解

实现单行/多行文本溢出

在日常开发展示页面&#xff0c;如果一段文本的数量过长&#xff0c;受制于元素宽度的因素&#xff0c;有可能不能完全显示&#xff0c;为了提高用户的使用体验&#xff0c;这个时候就需要我们把溢出的文本显示成省略号。 一. 单行文本溢出 即文本在一行内显示&#xff0c;超出…

Blued引流脚本

于多数人来说&#xff0c;引流都是一个比较困难的操作&#xff0c;因为流量不会听你的。所以任何人在网上做生意&#xff0c;或者开一个实体店&#xff0c;都会为流量而发愁&#xff0c;其实对于流量的吸引来说&#xff0c;我们越是刻意为之&#xff0c;可能所获得的效果也越不…

基于Java的在线听歌平台设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作…

从 0 到 1 ,手把手教你编写《消息队列》项目(Java实现) —— 介绍项目/ 需求分析

文章目录 一、消息队列是什么&#xff1f;二、需求分析结构解析功能解析规则解析绑定关系交换机类型消息应答 三、持久化存储四、网络通信提供的API复用TCP连接 五、消息队列概念图 一、消息队列是什么&#xff1f; 消息队列 (Message Queue, MQ)就是将阻塞队列这一数据结构提取…

国庆作业2

select实现服务器并发 代码&#xff1a; #include <myhead.h>#define ERR_MSG(msg) do{\printf("%d\n",__LINE__);\perror(msg);\ }while(0)#define PORT 8888#define IP "192.168.1.5"int main(int argc, const char *argv[]) {//创建流式套接字…

格拉姆角场GAF将时序数据转换为图像并应用于东南大学轴承故障诊断(Python代码,CNN模型)

1.运行效果&#xff1a;格拉姆角场GAF将时序数据转换为图像并应用于东南大学轴承故障诊断&#xff08;Python代码&#xff0c;CNN模型&#xff09;_哔哩哔哩_bilibili 环境库 只要tensorflow版本大于等于2.4.0即可运行 2.GAF的内容 GAF是一种用于时间序列数据可视化和特征提…

崇州街子古镇中秋国庆热闹非凡

今天&#xff08;国庆节日&#xff09;下午约4点钟&#xff0c;笔者实在耐不住寂寞&#xff0c;走出寄居养老的成都市崇州街子古镇青城神韵小区&#xff0c;去到国家AAAA级旅游景区那古色古香的街子古镇街道&#xff0c;旨在要亲身感受一下今年这里过双节&#xff0c;气氛究竟会…

28294-2012 钢渣复合料 课堂随笔

声明 本文是学习GB-T 28294-2012 钢渣复合料. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 本标准规定了混凝土用钢渣复合料的术语和定义、原材料组成及要求、强度等级、技术要求、试验方 法、检验规则、包装、标识、运输与贮存。 本标准…

WinHex数据恢复方法(误删后没覆盖)

winhex永远滴神&#xff01;winhex永远滴神&#xff01;winhex永远滴神&#xff01; md&#xff0c;安卓手机插u盘&#xff0c;改个文件夹名竟然把整个文件夹搞没了&#xff01;于是我赶紧查怎么恢复&#xff0c;然后依次找到并试用了diskgenus&#xff08;410RMB&#xff09;、…