哈希表模拟实现
目录
1.哈希概念
2.直接定址法
2.1 概念
2.2 哈希冲突
2.3 负载因⼦
3.哈希函数
3.1除法散列法/除留余数法
3.2 乘法散列法
3.3 全域散列法
4.处理哈希冲突
4.1开放定址
4.2 链地址法
1.哈希概念
哈希(hash)⼜称散列,是⼀种组织数据的⽅式。本质就是通过哈希函数把关键字Key跟存储位置建⽴⼀个映射关系,查找时通过这个哈希函数计算出Key存储的位置,进⾏快速查找。
2.直接定址法
2.1 概念
2.2 哈希冲突
两个不同的key可能会映射到同⼀个位置去,这种问题叫做哈希冲突,或者哈希碰撞。理想情况是找出⼀个好的哈希函数避免冲突,但是实际场景中,冲突是不可避免的,所以尽可能设计出优秀的哈希函数,减少冲突的次数,同时也要去设计出解决冲突的⽅案。
2.3 负载因⼦
假设哈希表中已经映射存储了N个值,哈希表的⼤⼩为M,那么 ,负载因⼦有些地⽅也翻译为载荷因⼦/装载因⼦等,他的英⽂为load factor。负载因⼦越⼤,哈希冲突的概率越⾼,空间利⽤率越⾼;负载因⼦越⼩,哈希冲突的概率越低,空间利⽤率越低
3.哈希函数
3.1除法散列法/除留余数法
除法散列法也叫做除留余数法,顾名思义,假设哈希表的⼤⼩为M,那么通过key除以M的余数作为映射位置的下标,也就是哈希函数为:h(key) = key % M。当使⽤除法散列法时,建议M取不太接近2的整数次,幂的⼀个质数(素数)。
3.2 乘法散列法
乘法散列法对哈希表⼤⼩M没有要求,他的思路第⼀步:⽤关键字 K 乘上常数 A (0<A<1),并抽取出 k*A 的⼩数部分。第⼆步:后再⽤M乘以k*A 的⼩数部分,再向下取整。哈希函数:h(key) = floor(M × ((A × key)%1.0))
3.3 全域散列法
哈希函数:hab (key) = ((a × key + b)%P )%M
4.处理哈希冲突
4.1开放定址法
4.1.1开放定址法哈希基本结构

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; // 表中存储数据个数
};
这里还有一些问题,对于哈希表来说,存储的是利用哈希函数映射到数组,当key是string/Date等类型时,key不能取模不能利用哈希函数,此时需要给HashTable增加⼀个仿函数,这个仿函数⽀持把key转换成⼀个可以取模的整形,如果key可以转换为整形并且不容易冲突,那么这个仿函数就⽤默认参数即可,如果这个Key不能转换为整形,我们就需要⾃⼰实现⼀个仿函数传给这个参数,实现这个仿函数的要求就是尽量key的每值都参与到计算中,让不同的key转换出的整形值不同。string做哈希表的key⾮常常⻅,所以可以考虑把string特化⼀下
#include<iostream>
#include<vector>
using namespace std;
enum State
{EXIST,EMPTY,DELETE
};
template<class K, class V>
struct HashData
{pair<K, V> _kv;State _state = EMPTY;
};
template <class K>
struct HashFunc
{size_t operator()(const K & key){return (size_t)key;}
};// 特化
template < >
struct HashFunc <string>
{// 字符串转换成整形,可以把字符ascii码相加即可// 但是直接相加的话,类似"abcd"和"bcad"这样的字符串计算出是相同的// 这⾥可以⽤上次的计算结果去乘以⼀个质数,这个质数⼀般去31, 131size_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
{
private:vector<HashData<K, V>> _tables;size_t _n = 0; // 表中存储数据个数
};
4.1.2 开放定址法构造
这里的构造直接对_tables开个数组就行,这里就不用写析构,程序结束,Vector内的数据会自动销毁
HashTable():_tables(13), _n(0)
{}
4.1.3线性探测
从发⽣冲突的位置开始,依次线性向后探测,直到寻找到下⼀个没有存储数据的位置为⽌,如果⾛到哈希表尾,则回绕到哈希表头的位置。h(key) = hash0 = key % M,hash0位置冲突了,则线性探测公式为:hc(key,i) = hashi = (hash0 + i) % M, i = {1, 2, 3, ..., M − 1}因为负载因⼦⼩于1,则最多探测M-1次,⼀定能找到⼀个存储key的位置。
template < class K, class V,class Hash = HashFunc<K>>
class HashTable
{HashData<K, V>* Find(const K& key)//查找{Hash hs;size_t hash0 = hs(key) % _tables.size();size_t hash1 = hash0;size_t i = 1;while (_tables[hash0]._state != EMPTY){if (_tables[hash1]._state == EXIST && _tables[hash1]._kv.first == key){return &_tables[hashi];}// 线性探测hash1 = (hash0 + i) % _tables.size();//线性探测++i;}return nullptr;}
private:vector<HashData<K, V>> _tables;size_t _n = 0; // 表中存储数据个数
};
4.1.4⼆次探测
从发⽣冲突的位置开始,依次左右按⼆次⽅跳跃式探测,直到寻找到下⼀个没有存储数据的位置为⽌,如果往右⾛到哈希表尾,则回绕到哈希表头的位置;如果往左⾛到哈希表头,则回绕到哈希表尾的位置。⼆次探测公式hc(key,i) = hashi = (hash0 ± i^2 ) % M, i = {1, 2, 3, ..., M/2}⼆次探测当 hashi = (hash0 − i^2 )%M 时,当hashi<0时,需要hashi += M
二次探测代码实现
HashData<K, V>* Find(const K& key)//查找{Hash hs;size_t hash0 = hs(key) % _tables.size();size_t hash1 = hash0;size_t i = 1;int falg=1;while (_tables[hash0]._state != EMPTY){if (_tables[hashi]._state == EXIST && _tables[hashi]._kv.first == key){return &_tables[hashi];}// 线性探测hash1 = (hash0 + i*i*falg)% _tables.size();//二次探测if (hash1 < 0){hash1 += _tables.size();}if (falg == 1){++i;falg = -1;}else {falg = 1}}}
4.1.5双重散列
第⼀个哈希函数计算出的值发⽣冲突,使⽤第⼆个哈希函数计算出⼀个跟key相关的偏移量值,不断往后探测,直到寻找到下⼀个没有存储数据的位置为⽌。hc(key,i) = hashi = (hash0 + i ∗ h2 (key)) % M, i = {1, 2, 3, ..., M}要求 h2 (key) < M且 h2 (key)和M互为质数,有两种简单的取值⽅法:1、当M为2整数幂时, h2 (key) 从[0,M-1]任选⼀个奇数;2、当M为质数时,h2 (key) = key % (M − 1) + 1
这种探测方法了解即可,这里就不进行代码实现
4.1.6 开放定址法insert实现
首先插入,可以利用哈希函数线性探测实现,其次是扩容,扩容的条件就是负载因子太大,这里实现哈希表的负载因子控制在0.7,那扩容怎么扩,扩多大,在上文,对于除法散列法的哈希函数来说,哈希表的大小要控制在不太接近2的整数次,幂的⼀个质数(素数),这里的话就用库里面的质数表每次去质数表获取扩容后的⼤⼩,这里只要能够从质数表中取数据即可
质数表
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;
}
扩容时由于还要进行线性探测进行插入,此时可以复用insert
bool Insert(const pair<K, V>& kv){if (Find(kv.first))//查找{return false;}if(_n*10/_tables.size()>7){//扩容HashTable<K, V, Hash> newht;newht._tables.resize(__stl_next_prime(_tables.size() + 1));//在质数表中取质数,这里记得+1,不然不会取到扩容后的质数for (size_t i = 0; i < _tables.size(); i++){if(_tables[i]._state==EXIST)//线性探测,复用Insert即可newht.Insert(_tables[i].kv);}_tables.swap(newht._tables);}Hash hs;size_t hash0 = hs(kv.first) % _tables.size();//哈希函数size_t hash1 = hash0;size_t i = 1;//线性探测while (_tables[hash0]._state == EXIST){hash1 = (hash0 + i) % _tables.size();++i;}//插入数据_tables[hash1].kv = kv;_tables[hash1]._state = EXIST;_n++;return true;}
4.1.7 开放定址法find,erase实现
find的实现在上文中线性探测已经实现过,这里就不再叙述,对于erase,由于每个结点有状态,此时利用查找,对查到的数据状态改变即可
bool Erase(const K& key)
{HashData<K, V>* ret = Find(key);if (ret){ret->_state = DELETE;//改变状态return true;}else{return false;}
}
4.2 链地址法
4.2.1 链地址法介绍
对于开放定址法,发现处理哈希冲突,有非常多的探测,而且这些探测有些非常复杂,此时就可以利用链地址法。
链地址法中所有的数据不再直接存储在哈希表中,哈希表中存储⼀个指针,没有数据映射这个位置时,这个指针为空,有多个数据映射到这个位置时,把这些冲突的数据链接成⼀个链表,挂在哈希表这个位置下⾯,链地址法也叫做拉链法或者哈希桶。

这里的哈希函数,还是用的是除法散列法
4.2.2 链地址法哈希结构实现
这里的哈希结构就可以不需要节点的状态,直接利用一个指针来连接数据即可,由于链地址法也需要使哈希表的大小为质数,还有对于key值是string/Date等类型时,key不能取模不能利用哈希函数,此时需要给HashTable增加⼀个仿函数,这个仿函数⽀持把key转换成⼀个可以取模的整形,这里和开放定址法一样,不用改变
结构代码实现
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>
struct HashData
{pair<K, V> _kv;State _state = EMPTY;
};
template <class K>
struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};// 特化
template < >
struct HashFunc <string>
{// 字符串转换成整形,可以把字符ascii码相加即可// 但是直接相加的话,类似"abcd"和"bcad"这样的字符串计算出是相同的// 这⾥可以⽤上次的计算结果去乘以⼀个质数,这个质数⼀般去31, 131size_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
{typedef HashNode<K, V> Node;inline unsigned long __stl_next_prime(unsigned long n){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;}
public:
private:vector<Node*> _tables; // 指针数组size_t _n = 0; // 表中存储数据个数
};
4.2.3 链地址法构造及析构函数
这里由于vector内部存的是Node*,因此要对此进行析构,而默认构造和开放定址法一样,而这里的拷贝构造由于有指针指向的数据,因此要写一个深拷贝
HashTable():_tables(__stl_next_prime(0)), _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;}
}
HashTable(const HashTable<K,V>& hs)
{_tables.resize(ht._tables.size());_n = ht._n;for (size_t i = 0; i < ht._tables.size(); ++i){Node* cur = ht._tables[i];Node* prev = nullptr;while (cur){Node* newNode = new Node(cur->_kv); // 拷贝kv对if (prev == nullptr){_tables[i] = newNode;}else{prev->_next = newNode;}prev = newNode;cur = cur->_next;}}
}
4.2.4 链地址法insert实现
对于链地址法的插入数据,就和链表一样,这里利用指针头插就行,对于扩容和开发定址法有所区别,开放定址法负载因⼦必须⼩于1,链地址法的负载因⼦就没有限制了,可以⼤于1。负载因⼦越⼤,哈希冲突的概率越⾼,空间利⽤率越⾼;负载因⼦越⼩,哈希冲突的概率越低,空间利⽤率越低;这里链地址法的最⼤负载因⼦基本控制在1,⼤于1就扩容。
bool insert(const pair<K,V>& kv){Hash hs;if (_n ==_tables.size() )//扩容{vector<Node*> newTatble(__stl_next_prime(_tables.size()+1));//直接把旧表的数据头插到新表for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->_next;size_t hashi = cur->hs(_kv.first) % newTatble.size();cur->_next = newTatble[hashi];newTatble[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newTatble);//交换}size_t hash0 = hs(kv.first) % _tables.size();Node* newnode = new Node(kv);//头插newnode->_next = _tables[hash0];_tables[hash0] = newnode;++_n;return true;}
4.2.5 链地址法find,erase实现
这里的find首先利用哈希函数映射,利用链表向下寻找就行
Node* Find(const K& key){Hash hs;size_t hash = hs(key) % _tables.size();Node* cur = _tables[hash];while (cur){if (cur->_kv.first == key){return cur;}cur = cur->_next;}reutrn nullptr;}
erase同样利用哈希函数映射,判断,当寻找的节点是头结点时,直接头删,若是中间节点和尾结点时,利用链表的删除就行
bool Erase(const K& key)
{Hash hs;size_t hash = hs(key) % _tables.size();Node* cur = _tables[hash];Node* prev = nullptr;while (cur){if (cur->_kv.first == key){if (prev == nullptr){_tables[hash] = cur->_next;}else{//中间节点prev->_next = cur->_next;}delete cur;--_n;}else{prev = cur;cur=cur->_next}}
}