C++哈希表的实现 -- 哈希的概念,直接定址法,哈希冲突,负载因子,除法散列法,处理哈希冲突的方式(开放地址法,链地址法)

目录

1. 哈希的概念

2. 最基本的哈希 -- 直接定址法

3. 哈希冲突

4. 负载因子

5. 哈希函数 -- 映射关系

5.1 除法散列法(除留余数法)

5.2 乘法散列法

5.3 全域散列法

6. 处理哈希冲突

6.1 开放地址法

6.1.1 线性探测

6.1.2 二次探测

6.2 开放定址法代码实现

6.2.1 开放定址法的哈希表结构

6.2.2 扩容问题

6.2.3 key不能取模的问题

6.2.4 Insert()函数的实现

6.2.5 Find()函数的实现 

6.2.6 Erase()函数的实现 

6.3 链地址法

6.3.1 扩容问题

6.4链地址法代码实现

6.4.1链地址法的哈希表结构

6.4.2 Insert()函数的实现

6.4.3 Find()函数的实现

6.4.4 Erase()函数的实现

7.参考代码 


1. 哈希的概念

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

2. 最基本的哈希 -- 直接定址法

        当关键字的范围⽐较集中时,直接定址法就是⾮常简单⾼效的⽅法,⽐如⼀组关键字都在[0,99]之间,那么我们开⼀个100个数的数组,每个关键字的值直接就是存储位置的下标。再⽐如⼀组关键字值都在[a,z]的⼩写字⺟,那么我们开⼀个26个数的数组,每个关键字的acsii码存储在数组中"ascii码 - 'a' " 的位置。也就是说直接定址法本质就是⽤关键字计算出⼀个绝对位置或者相对位置。这个方法在计数排序部分用过。下面这个OJ题也能通过直接定址法的方式进行解决。387. 字符串中的第一个唯一字符

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. 哈希冲突

        直接定址法的缺点也⾮常明显,当关键字的范围⽐较分散时,就很浪费内存甚⾄内存不够⽤。假设我们有数据范围是[0, 9999]的N个值(N 远小于 9999),这时开辟 10000 个空间来进行映射就很浪费空间。我们要映射到⼀个M个空间的数组中(⼀般情况下M >= N,M < 10000),那么就要借助哈希函数 (hash function)h,关键字key被放到数组的h(key)位置,这⾥要注意的是通过哈希函数 h(key) 计算出的值必须在[0, M)之间。这就存在的⼀个问题就是,两个不同的key可能会映射到同⼀个位置去,这种问题我们叫做哈希冲突或者哈希碰撞。理想情况是找出⼀个好的哈希函数避免冲突,但是实际场景中,冲突是不可避免的,所以尽可能设计出优秀的哈希函数,减少冲突的次数,同时也要去设计出解决冲突的方案。

4. 负载因子

        假设哈希表中已经映射存储了N个值,哈希表的⼤⼩为M,那么负载因⼦ = \frac{N}{M}负载因⼦也翻译为载荷因⼦/装载因⼦等,英⽂为load factor。负载因⼦越⼤,哈希冲突的概率越⾼,空间利⽤率越⾼;负载因⼦越⼩,哈希冲突的概率越低,空间利⽤率越低。

5. 哈希函数 -- 映射关系

        ⼀个好的哈希函数应该让N个关键字被等概率的均匀的散列分布到哈希表的M个空间中,实际中却很难做到,但是我们也要尽量往这个⽅向去考量设计。下面主要介绍除法散列法(除留余数法),乘法散列法和全域散列法大致介绍一下。

5.1 除法散列法(除留余数法)

        除法散列法也叫做除留余数法,假设哈希表的⼤⼩为M,那么通过key除以M的余数作为映射位置的下标,哈希函数也就是:h(key) = key % M。

        当使⽤除法散列法时,要尽量避免M为某些值,如2的幂等。如果是2^{x} ,以二进制的形式来看,key  %  2^{x}本质相当于保留key的后x位,那么后x位相同的值,计算出的哈希值都是⼀样的,都会映射到相同的位置,就会发生哈希冲突。如:{63 , 31} 看起来没有关联的值,如果 M 是 16 也就是 2 的 4 次方,计算出的哈希值 h(key) = 63 % 16 = 15,因为63的⼆进制后 4 位是 1111,31 的⼆进制后 4 位是 1111,所以63 和 31 或映射到同一个位置,也就是下标为 15 的位置。

        当使⽤除法散列法时,建议M取不太接近2的整数次幂的⼀个质数(素数)。使用素数可以让元素取模后的值,更不容易发生冲撞。

        合数和素数:合数即有两个以上的因数,素数(质数)即只有两个因数,分别为1和其本身。如果对一个合数取模,那么对其某个因数取模,结果可能仍然一致。例如10对8取模,结果为2,对4取模,结果也为2。而我们对一个素数取模,由于素数只有1和其本身两个素数,即不可能出现上述情形。如果插入的数据数列的间隔为1,那么不管模数为几都会均匀分布。如果间隔是模数的因数,则容易发生冲突,且模数的因数越多,冲突的可能性越大。素数只有两个因数,所以可以保证发生冲突的可能性最小。

        需要说明的是,实践中实现也没有这么绝对。Java 的 HashMap 采⽤除法散列法时就是2的整数次幂做哈希表的⼤⼩M,这样的话就不⽤取模,⽽直接位运算,相对⽽⾔位运算⽐取模更⾼效⼀些。但是他不是单纯的取模,⽐如M是2^16次⽅,本质是取后16位,那么⽤key’ = key >> 16,然后把key和key' 异或的结果作为哈希值。也就是说我们映射出的值还是在[0,M)范围内,但是尽量让key所有的位都参与计算,这样映射出的哈希值更均匀⼀些即可。

5.2 乘法散列法

        乘法散列法对哈希表⼤⼩M没有要求,思路为:(1) 关键字K乘上一个常数A(0<A<1),并抽取出 K*A 的⼩数部分。(2) 再⽤ M 乘以 k*A 的⼩数部分后向下取整。

        通过上述的操作得到的哈希值都是在[0, M)的一个整数。这样就可以将全部值映射到这个区间内。上述操作对应的哈希函数为:h(key) = floor(M * ((A * key) % 1.0)),0 < A < 1

        注意:floor函数是对其参数进行向下取整,上述哈希函数为数学中的表达式,在 C++ 中模运算模小数会编译错误。

5.3 全域散列法

        上述两种方式都是固定的一种哈希函数,使用上述的方式时,如果有人故意针,就可以通过你哈希函数的映射关系,制作会产生大量哈希冲突的数据集,这样的话就会降低哈希表的效率,反应在应用中就会出现卡顿甚至死机的情况。⽐如,只要散列函数是公开且确定的,攻击者就可以让所有关键字全部落⼊同⼀个位置中,就可以实现此攻击。解决方法是给散列函数增加随机性,在哈希表还没有运行起来的时候,使攻击者无法知晓你使用的哈希函数,攻击者就无法找出确定可以导致最坏情况的数据,这种⽅法叫做全域散列。

       h(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, 则 h(8) = ((3 × 8 + 4) % 17) % 6  =  5 。

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

6. 处理哈希冲突

        实践中哈希表⼀般还是选择除法散列法作为哈希函数,当然哈希表⽆论选择什么哈希函数也避免不了冲突。这里主要有两种解决哈希冲突的⽅法:开放定址法和链地址法

6.1 开放地址法

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

6.1.1 线性探测

        从发⽣冲突的位置依次线性向后探测,直到找到⼀个没有存储数据的位置为⽌,如果⾛到哈希表尾,则回绕到哈希表头的位置。h(key) = hash0 = key % M , hash0位置冲突了,则线性探测公式为:hc(key, i) = hashi = (hash0 + i) % M, i  = {1, 2, 3, ..., M − 1},判断 hashi 是否冲突,若 hashi 也冲突,则继续向后找。因为负载因⼦⼩于1,则最多探测M-1次,⼀定能找到⼀个存储key的位置。

        线性探测的⽐较简单且容易实现,线性探测有如下问题,假设hash0位置冲突,hash0,hash1,hash2位置已经存储数据了,后续映射到hash0,hash1,hash2,hash3的值都会争夺hash3位置,这种现象叫做群集/堆积。下⾯的⼆次探测可以⼀定程度改善这个问题。

        下⾯演⽰ {19,30,5,36,13,20,21,12} 等这⼀组值映射到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。

6.1.2 二次探测

        从发⽣冲突的位置开始,依次左右按⼆次⽅跳跃式探测,直到寻找到下⼀个没有存储数据的位置为⽌,如果往右⾛到哈希表尾,则回绕到哈希表头的位置;如果往左⾛到哈希表头,则回绕到哈希表尾的位置。h(key) = hash0 = key % M , hash0位置冲突了,则⼆次探测公式为:hc(key, i) = hashi = (hash0 ± i^{2} ) % M,i = {1, 2, 3, ...,  \frac{M}{2}},i 最多到 \frac{M}{2} ,M为偶数最多遍历 M 次,M为奇数最多遍历 M - 1次,如果不限制次数的话,二次探测有时候一直探测下去,会降低效率。

        ⼆次探测当 hashi = (hash0 − i^{2} ) % 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。

6.2 开放定址法代码实现

        下列代码实现利用的是线性探测的开放定址法解决哈希冲突。

6.2.1 开放定址法的哈希表结构

        这⾥给每个存储值的位置加⼀个状态标识,否则删除⼀些值以后,会影响后⾯冲突时的值的查找。如下图,我们规定查找到空位置时或者找到对应元素时才停止,如果我们删除 30,会导致查找 20 时遇到空位置进行返回,这样本来哈希表中有 20 但是没有查找到。当我们给每个位置加⼀个状态标识 {EXIST, EMPTY, DELETE} ,删除 30 就可以不⽤删除值,⽽是把状态改为 DELETE ,那么查找 20 时是遇到 EMPTY 才停止,就可以找到20。

//用枚举表示状态
enum State
{
	EXIST,
	EMPTY,
	DELETE
};

//HashData是哈希表每一个位置存储的数据类型
template<class K, class V>
struct HashData
{
	pair<K, V> _kv;
	State _state = EMPTY;
};

//开放定址法
namespace open_address
{
    //哈希表的结构
    //使用vector来作为存储数据的载体
    //存储的数据为HashData<K, V>
    //模板参数Hash用于接收一个函数,该函数解决key不能取模的问题
	template<class K, class V, class Hash = HashFunc<K>>
	class HashTables
	{
	public:
        //...
	private:
		vector<HashData<K, V>> _tables;
		size_t _n;
	};
}

6.2.2 扩容问题

       上面介绍到开放定址法的负载因子不能大于1,但是负载因⼦越⼤,哈希冲突的概率越⾼,空间利⽤率越⾼;负载因⼦越⼩,哈希冲突的概率越低,空间利⽤率越低。

        所以是否扩容是由负载因子决定的,负载因子为多少扩容是一个取平衡的问题。这里由以往经验我们将 负载因子控制在>=0.7 时扩容。

        如果还是按照以往vector2倍的方式扩容的话,就不能保证扩容后是一个质数了。这里解决的方式是参考 sig 版本的哈希表的扩容方式 -- 暴力枚举。

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

        上述直接给了⼀个近似 2 倍的质数表,每次扩容去质数表中获取扩容后的⼤⼩。lower_bound 函数的功能是取大于等于 n 的最小的一个质数表中的数。 

6.2.3 key不能取模的问题

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

        key 为一般的字符类型,将其直接强转为无符号整型即可,key 为 string 类型,遍历字符串的每个字符,先乘以一个常数,后加上该字符,最后得到一个无符号整型的哈希值。

        key 为 string 类型进行这样处理的原因是,string 类型字符相同顺序不同时也能得到不同的哈希值。

//仿函数 -- 把key转换成一个可以取模的整型
template<class K>
class HashFunc
{
	size_t operator()(const K& key)
	{
		return size_t(key);
	}
};

//仿函数类模板的特化 -- 处理key为string的情况
template<>
class HashFunc<string>
{
	// 字符串转换成整形,可以把字符ascii码相加即可
	// 但是直接相加的话,类似"abcd"和"bcad"这样的字符串计算出是相同的
	// 这⾥我们使⽤BKDR哈希的思路,⽤上次的计算结果去乘以⼀个质数,
	// 这个质数⼀般为31,131等效果会⽐较好
	size_t operator()(const string& key)
	{
		size_t hash_value = 0;
		for (auto e : key)
		{
			hash_value *= 131;
			hash_value += e;
		}

		return hash_value;
	}
};

6.2.4 Insert()函数的实现

        插入时找的位置为状态不为 EXIST 的位置,插入这里的扩容是遍历旧表,然后复用插入函数把旧表元素插入新表,最后进行交换。

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

			//扩容
			//负载因子 >= 0.7时扩容
			if (_n * 10 / _tables.size() >= 7)
			{
				//这里利用类似深拷贝现代写法的思想插入后交换解决
				HashTables<K, V, Hash> newHT;
				newHT._tables.resize(__stl_next_prime(_tables.size() + 1));
				for (size_t i = 0; i < _tables.size(); i++)
				{
					if (_tables[i]._state == EXIST)
					{
						newHT.Insert(_tables[i]._kv);
					}
				}

				_tables.swap(newHT._tables);
			}

			//除留余数法
			Hash hash;
			size_t hash0 = hash(kv.first) % _tables.size();
			size_t hashi = hash0;
			int i = 1;
			while (_tables[hashi]._state == EXIST)
			{
				//线性探测
				hashi = (hashi + i) % _tables.size();
				++i;
			}

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

			return true;
		}

6.2.5 Find()函数的实现 

        查找逻辑其实和插入逻辑相似,只是查找逻辑找的是状态为 EXIST 且 key 相等的值

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

			return nullptr;
		}

6.2.6 Erase()函数的实现 

        删除函数删除时先进行查找,将找到的元素状态置为 DELETE 即为删除。

    bool Erase(const K& key)
		{
			HashData<K, V>* tmp = Find(key);

			if (tmp)
			{
				tmp->_state = DELETE;
                --_n;
				return true;
			}
			else
			{
				return false;
			}
		}

6.3 链地址法

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

        下⾯演⽰ {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) = 8 。

6.3.1 扩容问题

        开放定址法负载因⼦必须⼩于1,链地址法的负载因⼦就没有限制了,可以⼤于1。负载因⼦越⼤,哈希冲突的概率越⾼,空间利⽤率越⾼;负载因⼦越⼩,哈希冲突的概率越低,空间利⽤率越低;STL 中 unordered_xxx 的最⼤负载因⼦基本控制在1,⼤于1就扩容,这里还是用的质数表进行扩容后容量大小的选择。

        如果极端场景下,某个桶特别⻓怎么办?可以考虑使⽤全域散列法,这样就不容易被针对了。但是假设不是被针对了,偶然情况下,某个桶很⻓,查找效率很低怎么办?这⾥在 Java8 的 HashMap 中当桶的⻓度超过⼀定阀值(8)时就把链表转换成红⿊树。⼀般情况下,不断扩容,单个桶很⻓的场景还是⽐较少,这个解决极端场景的思路了解⼀下即可。

6.4链地址法代码实现

6.4.1链地址法的哈希表结构

        哈希表还是用vector进行存储数据,存储的数据类型是单链表的 HashNode。这里需要注意的是析构函数需要自己写,依次把每个链表释放掉因为vector每个位置存储的是一个节点指针,自动调用析构函数只会释放该指针指向的内容,不会将整个链表进行释放。

    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 HashTables
	{
		typedef HashNode<K, V> Node;
	public:
		HashTables()
			:_tables(__stl_next_prime(0))
			//:_tables(11)
			,_n(0)
		{}

		~HashTables()
		{
			//依次把每个桶释放
			for (int i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				_tables[i] = nullptr;
			}
		}

        //...
    private:
		vector<Node*> _tables;
		size_t _n;
	};

6.4.2 Insert()函数的实现

        这里与开放定址法的不同是每插入一个节点到对应的哈希桶中都是进行的头插。扩容时这里不是进行拷贝操作,而是把之前链接在旧表的节点链接到新表中,最后在进行新表和旧表的交换实现的扩容操作。

    bool Insert(const pair<K, V>& kv)
		{
			Hash hash;
			size_t hashi = hash(kv.first) % _tables.size();

			if (Find(kv.first))
				return false;

			//负载因子 == 1 -> 扩容
			if (_n == _tables.size())
			{
				vector<Node*> newtables(__stl_next_prime(_tables.size() + 1), nullptr);
				for (int i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;
						size_t hashi = hash(cur->_kv.first) % newtables.size();
						//头插到新表
						cur->_next = newtables[hashi];
						newtables[hashi] = cur;
						cur = next;
					}

					_tables[i] = nullptr;
				}

				_tables.swap(newtables);
			}
            
            //头插
			Node* newnode = new Node(kv);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;

			return true;
		}

6.4.3 Find()函数的实现

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

				cur = cur->_next;
			}

			return nullptr;
		}

6.4.4 Erase()函数的实现

        因为这里的哈希桶是通过单链表实现的,所以这里需要一个prev指针,记录被删除节点的上一个节点,方便连接被删除节点的下一个节点。

    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)
				{
					//删除
					//1.删除的是头节点
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					//2.删除的是中间节点
					else
					{
						prev->_next = cur->_next;
					}

					delete cur;
					--_n;
					return true;
				}

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

			return false;
		}

7.参考代码 

//hash_tables.h

#pragma once
#include <vector>
#include <string>
using namespace std;

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

//仿函数 -- 把key转换成一个可以取模的整型
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return size_t(key);
	}
};

//仿函数类模板的特化 -- 处理key为string的情况
template<>
struct HashFunc<string>
{
	// 字符串转换成整形,可以把字符ascii码相加即可
	// 但是直接相加的话,类似"abcd"和"bcad"这样的字符串计算出是相同的
	// 这⾥我们使⽤BKDR哈希的思路,⽤上次的计算结果去乘以⼀个质数,
	// 这个质数⼀般为31,131等效果会⽐较好
	size_t operator()(const string& key)
	{
		size_t hash_value = 0;
		for (auto e : key)
		{
			hash_value *= 131;
			hash_value += e;
		}

		return hash_value;
	}
};

//开放定址法
namespace open_address
{
	//用枚举表示状态
	enum State
	{
		EXIST,
		EMPTY,
		DELETE
	};

	//HashData是哈希表每一个位置存储的数据类型
	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		State _state = EMPTY;
	};

	//哈希表的结构
	//使用vector来作为存储数据的载体
	//存储的数据为HashData<K, V>
	//模板参数Hash用于接收一个函数,该函数解决key不能取模的问题
	template<class K, class V, class Hash = HashFunc<K>>
	class HashTables
	{
	public:
		HashTables()
			:_tables(__stl_next_prime(0))
			//:_tables(11)
			,_n(0)
		{}

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

			//扩容
			//负载因子 >= 0.7时扩容
			if (_n * 10 / _tables.size() >= 7)
			{
				//这里利用类似深拷贝现代写法的思想插入后交换解决
				HashTables<K, V, Hash> newHT;
				newHT._tables.resize(__stl_next_prime(_tables.size() + 1));
				for (size_t i = 0; i < _tables.size(); i++)
				{
					if (_tables[i]._state == EXIST)
					{
						newHT.Insert(_tables[i]._kv);
					}
				}

				_tables.swap(newHT._tables);
			}

			//除留余数法
			Hash hash;
			size_t hash0 = hash(kv.first) % _tables.size();
			size_t hashi = hash0;
			int i = 1;
			while (_tables[hashi]._state == EXIST)
			{
				//线性探测
				hashi = (hashi + i) % _tables.size();
				++i;
			}

			_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;
			int i = 1;
			while (_tables[hashi]._state != EMPTY)
			{
				if (_tables[hashi]._state == EXIST
					&& _tables[hashi]._kv.first == key)
				{
					return &_tables[hashi];
				}
				//线性探测
				hashi = (hashi + i) % _tables.size();
				++i;
			}

			return nullptr;
		}

		bool Erase(const K& key)
		{
			HashData<K, V>* tmp = Find(key);

			if (tmp)
			{
				tmp->_state = DELETE;
                --_n;
				return true;
			}
			else
			{
				return false;
			}
		}

	private:
		vector<HashData<K, V>> _tables;
		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 Hash = HashFunc<K>>
	class HashTables
	{
		typedef HashNode<K, V> Node;
	public:
		HashTables()
			:_tables(__stl_next_prime(0))
			//:_tables(11)
			,_n(0)
		{}

		~HashTables()
		{
			//依次把每个桶释放
			for (int i = 0; i < _tables.size(); i++)
			{
				Node* cur = _tables[i];
				while (cur)
				{
					Node* next = cur->_next;
					delete cur;
					cur = next;
				}
				_tables[i] = nullptr;
			}
		}

		bool Insert(const pair<K, V>& kv)
		{
			Hash hash;
			size_t hashi = hash(kv.first) % _tables.size();

			if (Find(kv.first))
				return false;

			//负载因子 == 1 -> 扩容
			if (_n == _tables.size())
			{
				vector<Node*> newtables(__stl_next_prime(_tables.size() + 1), nullptr);
				for (int i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;
						size_t hashi = hash(cur->_kv.first) % newtables.size();
						//头插到新表
						cur->_next = newtables[hashi];
						newtables[hashi] = cur;
						cur = next;
					}

					_tables[i] = nullptr;
				}

				_tables.swap(newtables);
			}

			Node* newnode = new Node(kv);
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_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;
				}

				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)
				{
					//删除
					//1.删除的是头节点
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					//2.删除的是中间节点
					else
					{
						prev->_next = cur->_next;
					}

					delete cur;
					--_n;
					return true;
				}

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

			return false;
		}

	private:
		vector<Node*> _tables;
		size_t _n;
	};
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值