散列表(哈希表) 原理 【C++】

目录

前言

散列/哈希概念

哈希冲突

散列/哈希函数

直接定址法

数字分析法

平方取中法

折叠法

除留余数法(最常用)

随机数法

哈希冲突解决 

闭散列--开放定址法

一、线性探测

二、二次探测

开散列—— 链地址法(拉链法、哈希桶)

哈希表的闭散列实现

哈希表的结构

哈希表的插入

哈希表的查找

哈希表的删除 

哈希表的开散列实现(哈希桶)

哈希表的结构

哈希表的插入

哈希表的查找

哈希表的删除

哈希表的大小设置

完整代码


前言

对于数据查找我们学过顺序表查找,二分查找,借助红黑树AVL树等数据结构进行查找,其实红黑树的效率已经很高了,但是我们是否有一种手段可以不经过比较,一次或者常数次就能找到对应元素呢?

我们去中药房抓药,只要告诉药师药材名称,对方就直接拉开对应抽屉拿药,一步到位,在学校上课,对应的课到对应的教室,也不会遍历楼层的所有教室,这其实就是运用一种映射的思想。

也就是说,我们只需要通过某个函数f , 使得

                                                      f(key) = value

那样我们可以不通过比较,直接获得所需元素的存储位置,这就是一种新的存储技术--散列技术

散列/哈希概念

散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得 每个关键字key对应一个存储位置f(key)

这里我们把这种对应关系f称为散列函数,又称为哈希(Hash)函数。按照这个思想,采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表。

  ●插入元素

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

        例如:数据集合{1,7,6,4,5,9};

开辟一个元素个数为10的数组,key为元素的值;哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小。因为这里总空间大小为10, capacity=10,通过取模得到该元素在数组上所对应的位置。

        ●搜索元素

        对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置 取元素比较,若关键码相等,则搜索成功。用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快,但是按照上述哈希方式,向集合中插入元素44,会出现什么问题?

哈希冲突

 我们发现在哈希表插入过程中会出现 key1 != key2 ,但是hash(key1) = hash(key2)的情况,这种现象我们称为冲突(collision),并把key1 , key2称为这个散列函数的同义词(synonym)

散列/哈希函数

我们首先要明确一点,什么是好的散列/哈希函数?

1.计算简单:计算时间应低于其他查找技术与关键字比较的时间

2.散列地址分布均匀::对于哈希冲突,最好的办法就是尽量让散列地址均匀的分布在存储空间中,这样可以保证存储空间的有效利用,并减少为处理冲突而耗费的时间。

接下来介绍几种常用的散列函数构造方法。

直接定址法

取关键字的某个线性函数值为散列地址,即

                                 f(key) = a * key + b(a , b为常数)

比如对于小写字母我们用长度为26的顺序表,f(key) = ch - 'a'就可以进行映射

这样的散列函数优点:简单,均匀且不会产生冲突

局限:事先知道关键字的分布情况,适合查找表较小且连续的情况

数字分析法

如果我们的关键字是位数比较多的数字,比如我们的11位手机号“130xxxx1234”,其中前三位是接入号,一般对应不同运营商的子品牌,如130是联通如意通、136是移动神州行、153是电信等等;中间的4位是HLR识别号,表示用户号的归属地;后四位才是真正的用户号。

现在我们要存储一批用户信息,如果用手机号作为关键字,那么极有可能前7位都是相同的,我们选择后面4位称为散列地址就是不错的选择,如果这样的抽取工作还是容易出现冲突问题,那么我们还可以对抽取出来的数字再进行反转(如1234反转成4321),右环位移(如1234改成4123),左环位移,甚至前两位与后两位相加(如1234改成12 + 34 = 46)。

总的目的就是为了提供一个散列函数,能够合理地将关键字分配到散列表的各位置。

适用情况:关键字位数比较多

平方取中法

这个方法比较简单,比如1234,它的平方就是1522756,再抽取中间的三位就是227,作为散列地址。

适用情况:不知道关键字的分布,而位数又不是很多的情况

折叠法

将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。

比如:关键字9876543210,散列表长度为3位,我们将它分为4组,987|654|321|0。然后将他们叠加求和987 + 654 + 321 + 0 = 1962 , 再求后三位得到散列地址为962.

折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况

除留余数法(最常用)

对于散列表长为m的散列函数公式为:

                                    f(key) = key mod p (p <= m)

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

随机数法

        选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中

random为随机数函数。通常应用于关键字长度不等时采用此法。

哈希冲突解决 

常见解决哈希冲突的两种方法有闭散列开散列,闭散列是在原来的未被占用的空间上继续插入,开散列则是在该结构上基础上增加链式结构。两者其目的都是在出现冲突后能继续插入元素。

闭散列--开放定址法

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

寻找“下一个位置”的方式多种多样,常见的方式有以下两种:

一、线性探测

当发生哈希冲突时,从发生冲突的位置开始,依次向后探测,直到找到下一个空位置为止。

Hi=(H0+i)%m ( i = 1 , 2 , 3 , . . . ) (i=1,2,3,...)(i=1,2,3,...)

H0:通过哈希函数对元素的关键码进行计算得到的位置。
Hi:冲突元素通过线性探测后得到的存放位置。
m:表的大小。

还是用上述数据集合{1,7,6,4,5,9}举例,当插入元素44时,经过哈希函数计算出哈希地址后,发现改地址已经被之前插入元素4占用,此时就通过线性探测,从4映射的位置依次向后探测,直到找到空位置(8)时,进行插入。

通过上图可以看到,随着哈希表中数据的增多,产生哈希冲突的可能性也随着增加,最后在40进行插入的时候更是连续出现了四次哈希冲突。

我们将数据插入到有限的空间,那么空间中的元素越多,插入元素时产生冲突的概率也就越大,冲突多次后插入哈希表的元素,在查找时的效率必然也会降低。介于此,哈希表当中引入了负载因子(载荷因子)

负载因子 = 表中有效数据个数 / 空间的大小

  • 负载因子越大,产出冲突的概率越高,增删查改的效率越低。
  • 负载因子越小,产出冲突的概率越低,增删查改的效率越高。

例如,我们将哈希表的大小改为20,可以看到在插入序列时,产生的哈希冲突会有所减少:
 

但负载因子越小,也就意味着空间的利用率越低,此时大量的空间实际上都被浪费了。对于闭散列(开放定址法)来说,负载因子是特别重要的因素,一般控制在0.7~0.8以下,超过0.8会导致在查表时CPU缓存不命中(cache missing)按照指数曲线上升。
因此,一些采用开放定址法的hash库,如JAVA的系统库限制了负载因子为0.75,当超过该值时,会对哈希表进行增容。

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


二、二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题,找下一个空位置的方法为:

Hi=(H0+i 2 )%m ( i = 1 , 2 , 3 , . . . ) (i=1,2,3,...)(i=1,2,3,...)

H0:通过哈希函数对元素的关键码进行计算得到的位置。
Hi:冲突元素通过二次探测后得到的存放位置。
m:表的大小。

采用二次探测为产生哈希冲突的数据寻找下一个位置,相比线性探测而言,采用二次探测的哈希表中元素的分布会相对稀疏一些,不容易导致数据堆积。

但是,闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。

开散列—— 链地址法(拉链法、哈希桶)

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

 从上图看,其实开散列和闭散列结构其实差不多,因为可以将他们看做是除留余数法的两个分支。一个是纯数组,一个是在数组中存的单链表节点。最开始都是需要哈希函数计算,求出数组对应的位置然后进行插入。对单链表有所忘记的就从下图回顾。

闭散列解决哈希冲突,采用的是一种报复的方式,“我的位置被占用了我就去占用其他位置”。而开散列解决哈希冲突,采用的是一种乐观的方式,“虽然我的位置被占用了,但是没关系,我可以‘挂’在这个位置下面”。

与闭散列不同的是,这种将相同哈希地址的元素通过单链表链接起来,然后将链表的头结点存储在哈希表中的方式,不会影响与自己哈希地址不同的元素的增删查改的效率,因此开散列的负载因子相比闭散列而言,可以稍微大一点。

  • 闭散列的开放定址法,负载因子不能超过1,一般建议控制在[0.0, 0.7]之间。
  • 开散列的哈希桶,负载因子可以超过1,一般建议控制在[0.0, 1.0]之间。

在实际中,开散列的哈希桶结构比闭散列更实用,主要原因有两点:

  1. 哈希桶的负载因子可以更大,空间利用率高。
  2. 哈希桶在极端情况下还有可用的解决方案。

 哈希桶的极端情况就是,所有元素全部产生冲突,最终都放到了同一个哈希桶中,此时该哈希表增删查改的效率就退化成了O ( N ) O(N)O(N):

 这时,我们可以考虑将这个桶中的元素,由单链表结构改为红黑树结构,并将红黑树的根结点存储在哈希表中。

在这种情况下,就算有十亿个元素全部冲突到一个哈希桶中,我们也只需要在这个哈希桶中查找30次左右,这就是所谓的“桶里种树”。 

为了避免出现这种极端情况,当桶当中的元素个数超过一定长度,有些地方就会选择将该桶中的单链表结构换成红黑树结构,比如在JAVA中比较新一点的版本中,当桶当中的数据个数超过8时,就会将该桶当中的单链表结构换成红黑树结构,而当该桶当中的数据个数减少到8或8以下时,又会将该桶当中的红黑树结构换回单链表结构。

但有些地方也会选择不做此处理,因为随着哈希表中数据的增多,该哈希表的负载因子也会逐渐增大,最终会触发哈希表的增容条件,此时该哈希表当中的数据会全部重新插入到另一个空间更大的哈希表,此时同一个桶当中冲突的数据个数也会减少,因此不做处理问题也不大。

哈希表的闭散列实现

哈希表的结构

在闭散列的哈希表中,哈希表每个位置除了存储所给数据之外,还应该存储该位置当前的状态,哈希表中每个位置的可能状态如下:

  1. EMPTY(无数据的空位置)。
  2. EXIST(已存储数据)。
  3. DELETE(原本有数据,但现在被删除了)。

我们可以用枚举类定义这三个状态。

	enum class state
	{
		EMPTY, EXIST, DELETE
	};

为什么需要标识哈希表中每个位置的状态?

若是不设置哈希表中每个位置的状态,那么在哈希表中查找数据的时候可能是这样的。以除留余数法的线性探测为例,我们若是要判断下面这个哈希表是否存在元素40,步骤如下:

  1. 通过除留余数法求得元素40在该哈希表中的哈希地址是0。
  2. 从0下标开始向后进行查找,若找到了40则说明存在。

但是我们在寻找元素40时,不可能从0下标开始将整个哈希表全部遍历一次,这样就失去了哈希的意义。我们只需要从0下标开始往后查找,直到找到元素40判定为存在,或是找到一个空位置判定为不存在即可。

因为线性探测在为冲突元素寻找下一个位置时是依次往后寻找的,既然我们已经找到了一个空位置,那就说明这个空位置的后面不会再有从下标0位置开始冲突的元素了。比如我们要判断该哈希表中是否存在元素90,步骤如下:

  1. 通过除留余数法求得元素90在该哈希表中的哈希地址是0。
  2. 从0下标开始向后进行查找,直到找到下标为5的空位置,停止查找,判定元素90不存在。

但这种方式是不可行的,原因如下:

  1. 如何标识一个空位置?用数字0吗?那如果我们要存储的元素就是0怎么办?因此我们必须要单独给每个位置设置一个状态字段。
  2. 如果只给哈希表中的每个位置设置存在和不存在两种状态,那么当遇到下面这种情况时就会出现错误。

我们先将上述哈希表当中的元素1000找到,并将其删除,此时我们要判断当前哈希表当中是否存在元素40,当我们从0下标开始往后找到2下标(空位置)时,我们就应该停下来,此时并没有找到元素40,但是元素40却在哈希表中存在

 因此我们必须为哈希表中的每一个位置设置一个状态,并且每个位置的状态应该有三种可能,当哈希表中的一个元素被删除后,我们不应该简单的将该位置的状态设置为EMPTY,而是应该将该位置的状态设置为DELETE。
这样一来,当我们在哈希表中查找元素的过程中,若当前位置的元素与待查找的元素不匹配,但是当前位置的状态是EXIST或是DELETE,那么我们都应该继续往后进行查找,而当我们插入元素的时候,可以将元素插入到状态为EMPTY或是DELETE的位置。

因此,闭散列的哈希表中的每个位置存储的结构,应该包括所给数据和该位置的当前状态。

	template<class T>
	struct HashData
	{
		state _state;
		T _data;
	};

而为了在插入元素时好计算当前哈希表的负载因子,我们还应该时刻存储整个哈希表中的有效元素个数,当负载因子过大时就应该进行哈希表的增容。

//哈希表
template<class K, class V>
class HashTable
{
public:
	//...
private:
	vector<HashData<K, V>> _table; //哈希表
	size_t _n = 0; //哈希表中的有效元素个数
};

哈希表的插入

向哈希表中插入数据的步骤如下:

  1. 查看哈希表中是否存在该键值的键值对,若已存在则插入失败。
  2. 判断是否需要调整哈希表的大小,若哈希表的大小为0,或负载因子过大都需要对哈希表的大小进行调整。
  3. 将键值对插入哈希表。
  4. 哈希表中的有效元素个数加一。

其中,哈希表的调整方式如下:

  • 若哈希表的大小为0,则将哈希表的初始大小设置为10。
  • 若哈希表的负载因子大于0.7,则先创建一个新的哈希表,该哈希表的大小为原哈希表的两倍,之后遍历原哈希表,将原哈希表中的数据插入到新哈希表,最后将原哈希表与新哈希表交换即可。

注意: 在将原哈希表的数据插入到新哈希表的过程中,不能只是简单的将原哈希表中的数据对应的挪到新哈希表中,而是需要根据新哈希表的大小重新计算每个数据在新哈希表中的位置,然后再进行插入。

将键值对插入哈希表的具体步骤如下:

  1. 通过哈希函数计算出对应的哈希地址。
  2. 若产生哈希冲突,则从哈希地址处开始,采用线性探测向后寻找一个状态为EMPTY或DELETE的位置。
  3. 将键值对插入到该位置,并将该位置的状态设置为EXIST。

注意: 产生哈希冲突向后进行探测时,一定会找到一个合适位置进行插入,因为哈希表的负载因子是控制在0.7以下的,也就是说哈希表永远都不会被装满。

bool insert(const T& d)
		{
			if (_tables.size() == 0 || _num * 10 / _tables.size() >= 7)
			{
				HashTable<K, T, KeyOfT> newht;
				size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
				newht._tables.resize(newsize);
				for (auto data : _tables)
				{
					if (data._state == state::EXIST)
						newht.insert(data._data);
				}
				_tables.swap(newht._tables);

			}

			// 计算x在表中的位置
			size_t index = koft(d) % _tables.size();
			//线性探测
			while (_tables[index]._state == state::EXIST)
			{
				if (_tables[index]._data == d)
					return false;
				++index;
				if (index == _tables.size())
					index = 0;

			}

			_tables[index]._data = d;
			_tables[index]._state = state::EXIST;
			_num++;
			return true;
		}

哈希表的查找

在哈希表中查找数据的步骤如下:

  1. 先判断哈希表的大小是否为0,若为0则查找失败。
  2. 通过哈希函数计算出对应的哈希地址。
  3. 从哈希地址处开始,采用线性探测向后向后进行数据的查找,直到找到待查找的元素判定为查找成功,或找到一个状态为EMPTY的位置判定为查找失败。

注意: 在查找过程中,必须找到位置状态为EXIST,并且key值匹配的元素,才算查找成功。若仅仅是key值匹配,但该位置当前状态为DELETE,则还需继续进行查找,因为该位置的元素已经被删除了。

		hashdata* Find(const K& key)
		{
	        if (_tables.size() == 0) //哈希表大小为0,查找失败
	        {
		    return nullptr;
	        }
			KeyOfT koft;
			size_t index = key % _tables.size();

			while (_tables[index]._state != state::EXIST)
			{

				if (koft(_tables[index]._data) == key && 
_tables[index]._state == state::EXIST)
				{
					return &_tables[index];
				}
				++index;
				if (index == _tables.size())
					index = 0;
			}
			return nullptr;
		}

哈希表的删除 

删除哈希表中的元素非常简单,我们只需要进行伪删除即可,也就是将待删除元素所在位置的状态设置为DELETE。

在哈希表中删除数据的步骤如下:

  1. 查看哈希表中是否存在该键值的键值对,若不存在则删除失败。
  2. 若存在,则将该键值对所在位置的状态改为DELETE即可。
  3. 哈希表中的有效元素个数减一。

 注意: 虽然删除元素时没有将该位置的数据清0,只是将该元素所在状态设为了DELETE,但是并不会造成空间的浪费,因为我们在插入数据时是可以将数据插入到状态为DELETE的位置的,此时插入的数据就会把该数据覆盖。

		bool erase(const K& key)
		{
			hashdata* ret = Find(key);
			if (ret)
			{
				ret->_state = state::DELETE;
				_num--;
				return true;
			}
			else
			{
				return false;
			}
		}

哈希表的开散列实现(哈希桶)

哈希表的结构

在开散列的哈希表中,哈希表的每个位置存储的实际上是某个单链表的头结点,即每个哈希桶中存储的数据实际上是一个结点类型,该结点类型除了存储所给数据之外,还需要存储一个结点指针用于指向下一个结点。

	template<class T>
	struct HashNode
	{
		HashNode(const T& data ) : _data(data), _next(nullptr)
		{}
		T _data;
		HashNode<T>* _next;
	};

与闭散列的哈希表不同的是,在实现开散列的哈希表时,我们不用为哈希表中的每个位置设置一个状态字段,因为在开散列的哈希表中,我们将哈希地址相同的元素都放到了同一个哈希桶中,并不需要经过探测寻找所谓的“下一个位置”。

哈希表的开散列实现方式,在插入数据时也需要根据负载因子判断是否需要增容,所以我们也应该时刻存储整个哈希表中的有效元素个数,当负载因子过大时就应该进行哈希表的增容。

//哈希表
template<class K ,class T>
class HashTable
{
public:
	//...
private:
		vector<Node*> _tables;
		size_t _num = 0;
};

哈希表的插入

向哈希表中插入数据的步骤如下:

  1. 查看哈希表中是否存在该键值的键值对,若已存在则插入失败。
  2. 判断是否需要调整哈希表的大小,若哈希表的大小为0,或负载因子过大都需要对哈希表的大小进行调整。
  3. 将键值对插入哈希表。
  4. 哈希表中的有效元素个数加一。

其中,哈希表的调整方式如下:

  • 若哈希表的大小为0,则将哈希表的初始大小设置为10。
  • 若哈希表的负载因子已经等于1了,则先创建一个新的哈希表,该哈希表的大小为原哈希表的两倍,之后遍历原哈希表,将原哈希表中的数据插入到新哈希表,最后将原哈希表与新哈希表交换即可。

重点: 在将原哈希表的数据插入到新哈希表的过程中,不要通过复用插入函数将原哈希表中的数据插入到新哈希表,因为在这个过程中我们需要创建相同数据的结点插入到新哈希表,在插入完毕后还需要将原哈希表中的结点进行释放,多此一举。

实际上,我们只需要遍历原哈希表的每个哈希桶,通过哈希函数将每个哈希桶中的结点重新找到对应位置插入到新哈希表即可,不用进行结点的创建与释放。

将键值对插入哈希表的具体步骤如下:

  1. 通过哈希函数计算出对应的哈希地址。
  2. 若产生哈希冲突,则直接将该结点头插到对应单链表即可。

pair<iterator , bool> insert(const T& data)
		{
			KeyOfT koft;
			iterator exist = find(koft(data));
			if (exist != end())
				return make_pair(exist, false);
			if (_tables.size() == _num)
			{
                 //获取新size  后面再讲
				size_t newsize = nextprime(_tables.size());
				vector<Node*> newtable;
				newtable.resize(newsize);
				for (auto& node : _tables)
				{
					Node* cur = node;
					while (cur)
					{
						Node* next = cur->_next;
						size_t index = Hashfunc(koft(cur->_data)) % newsize;
						cur->_next = newtable[index];
						newtable[index] = cur;
						cur = next;
					}
					node = nullptr;
				}
				_tables.swap(newtable);
			}



			size_t index = Hashfunc(koft(data)) % _tables.size();
			Node* newnode = new Node(data);
			newnode->_next = _tables[index];
			_tables[index] = newnode;
			_num++;
			return make_pair(iterator(newnode, this), true);
		}

哈希表的查找

在哈希表中查找数据的步骤如下:

  1. 先判断哈希表的大小是否为0,若为0则查找失败。
  2. 通过哈希函数计算出对应的哈希地址。
  3. 通过哈希地址找到对应的哈希桶中的单链表,遍历单链表进行查找即可。
iterator find(const K& key)
		{
			if (_tables.size() == 0) //哈希表大小为0,查找失败
			{
				return end();
			}
			KeyOfT koft;
			size_t index = Hashfunc(key) % _tables.size();
			Node* cur = _tables[index];
			while (cur)
			{
				if (koft(cur->_data) == key)
					return iterator(cur , this);
				else
				{
					cur = cur->_next;
				}
			}

			return end();
		}

哈希表的删除

在哈希表中删除数据的步骤如下:

  1. 通过哈希函数计算出对应的哈希桶编号。
  2. 遍历对应的哈希桶,寻找待删除结点。
  3. 若找到了待删除结点,则将该结点从单链表中移除并释放。
  4. 删除结点后,将哈希表中的有效元素个数减一。
		bool erase(const K& key)
		{
			KeyOfT koft;
			size_t index = Hashfunc(key) % _tables.size();
			Node* cur = _tables[index];
			Node* prev = nullptr;
			while (cur)
			{
				if (koft(cur->_data) == key)
				{
					if (prev == nullptr)
					{
						_tables[index] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}
					delete cur;
					return true;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}


			return false;
		}

哈希表的大小设置

我们阅读STL源码发现hashtable里面有这样一段代码

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, 3221225473ul, 4294967291ul
};
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;
  const unsigned long* pos = upper_bound(first, last, n);
  return pos == last ? *(last - 1) : *pos;
}

这是hashtable增容时用来获取新size的函数,我们发现stl把hashtable的大小设置为了素数

原因是:使用除留余数法时,哈希表的大小最好是素数,这样能够减少哈希冲突产生的次数。

其实思考一下会发现这样有其合理之处

对于任意 n ,

假设n为合数 ,那么其因子有 1 , a1 ,a2 ,a3 ... n

存在n | k * ai    ,当一组数据插入时,满足等于k*n的数字间会冲突外还会和满足等于 k*ai的数字发生哈希冲突

(这里的符号 | 为整除关系 , 即k*ai % n = 0)

假设n为质数   其因子只有1   , n   满足等于k*n的数字只与同样为n的倍数的数字冲突

完整代码

	template<class T>
	struct HashNode
	{
		HashNode(const T& data ) : _data(data), _next(nullptr)
		{}
		T _data;
		HashNode<T>* _next;
	};


	template<class K, class T, class KeyOfT, class Hash>
	class HashTable;

	template<class K, class T , class KeyOfT , class Hash>
	struct __HashTableIterator
	{
		typedef HashNode<T> Node;
		typedef __HashTableIterator<K, T, KeyOfT , Hash> Self;
		typedef HashTable<K, T, KeyOfT , Hash> HashTable;
		Node* _node;
		HashTable* _pht;
		__HashTableIterator(Node* node , HashTable* pht):_node(node) , _pht(pht)
		{}

		T& operator*()
		{
			return _node->_data;
		}

		T* operator->()
		{
			return &_node->_data;
		}

		Self& operator++()
		{
			KeyOfT koft;
			if (_node->_next)
			{
				_node = _node->_next;
			}
			else
			{
				KeyOfT koft;
				size_t i = _pht->Hashfunc(koft(_node->_data)) % _pht->_tables.size();
				++i;

				for (; i < _pht->_tables.size(); ++i)
				{
					Node* cur = _pht->_tables[i];
					if (cur)
					{
						_node = cur;
						return *this;
					}
				}
				_node = nullptr;
			}
			return *this;
		}

		Self& operator++(int)
		{
			Self ret(_node, _pht);
			++(*this);
			return ret;
		}

		bool operator!=(const Self& it)
		{
			return _node != it._node;
		}

		bool operator==(const Self& it)
		{
			return _node == it._node;
		}
	};



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

	template<>
	struct _Hash<string>
	{
		size_t operator()(const string& s)
		{
			size_t ret = 0;
			for (auto x : s)
			{
				ret = ret * 131 + x;
			}
			return ret;
		}
	};

	static const int num_primes = 28;
	static const unsigned long prime_list[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, 3221225473ul, 4294967291ul
	};

	inline unsigned long nextprime(unsigned long n)
	{
		const unsigned long* first = prime_list;
		const unsigned long* last = prime_list + num_primes;
        const unsigned long* pos = upper_bound(first, last, n);
		return pos == last ? *(last - 1) : *pos;
	}

	template<class K ,class T , class KeyOfT , class Hash = _Hash<K>>
	class HashTable
	{
		friend struct __HashTableIterator<K, T, KeyOfT , Hash>;
		typedef HashNode<T> Node;
	public:
		typedef __HashTableIterator<K, T, KeyOfT , Hash> iterator;
	public:
		HashTable() = default;
		//copy constructor
		HashTable(const HashTable& ht)
		{
			_tables.resize(ht.size());

			for (size_t i = 0; i < ht._tables.size(); i++)
			{
				if (ht._tables[i])
				{
					Node* cur = ht._tables[i];
					while (cur)
					{
						Node* newnode = new Node(cur->_data);
						newnode->_next = _tables[i];
						_tables[i] = newnode;
						cur = cur->_next;
					}
				}
			}
			_num = ht._num;
		}

		HashTable& operator=(const HashTable ht)
		{
			_tables.swap(ht._tables);
			swap(_num, ht._num);

			return *this;
		}


		iterator begin()
		{
			for (size_t i = 0; i < _tables.size(); i++)
				if (_tables[i])
					return iterator(_tables[i] , this);
			return end();
		}
		iterator end()
		{
			return iterator(nullptr , this);
		}



		pair<iterator , bool> insert(const T& data)
		{
			KeyOfT koft;
			iterator exist = find(koft(data));
			if (exist != end())
				return make_pair(exist, false);
			if (_tables.size() == _num)
			{
				size_t newsize = nextprime(_tables.size());
				vector<Node*> newtable;
				newtable.resize(newsize);
				for (auto& node : _tables)
				{
					Node* cur = node;
					while (cur)
					{
						Node* next = cur->_next;
						size_t index = Hashfunc(koft(cur->_data)) % newsize;
						cur->_next = newtable[index];
						newtable[index] = cur;
						cur = next;
					}
					node = nullptr;
				}
				_tables.swap(newtable);
			}



			size_t index = Hashfunc(koft(data)) % _tables.size();
			Node* newnode = new Node(data);
			newnode->_next = _tables[index];
			_tables[index] = newnode;
			_num++;
			return make_pair(iterator(newnode, this), true);
		}
		iterator find(const K& key)
		{
			if (_tables.size() == 0) //哈希表大小为0,查找失败
			{
				return end();
			}
			KeyOfT koft;
			size_t index = Hashfunc(key) % _tables.size();
			Node* cur = _tables[index];
			while (cur)
			{
				if (koft(cur->_data) == key)
					return iterator(cur , this);
				else
				{
					cur = cur->_next;
				}
			}

			return end();
		}
		bool erase(const K& key)
		{
			KeyOfT koft;
			size_t index = Hashfunc(key) % _tables.size();
			Node* cur = _tables[index];
			Node* prev = nullptr;
			while (cur)
			{
				if (koft(cur->_data) == key)
				{
					if (prev == nullptr)
					{
						_tables[index] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}
					delete cur;
					return true;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}


			return false;
		}

		void clear()
		{
			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;
			}
		}

		~HashTable()
		{
			clear();
		}

	private:
		vector<Node*> _tables;
		size_t _num = 0;
		Hash Hashfunc;
	};
	template<class K>
	struct KOfT
	{
		const K& operator()(const K& k)
		{
			return k;
		}
	};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

EQUINOX1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值