文章目录
- 1.开放地址法
- 1.1key不能取模的问题
- 1.1.1将字符串转为整型
- 1.1.2将日期类转为整型
- 2.哈希函数
- 2.1乘法散列法(了解)
- 2.2全域散列法(了解)
- 3.处理哈希冲突
- 3.1线性探测(挨着找)
- 3.2二次探测(跳跃着找)
- 3.3双重散列(了解)
- 4.链地址法
- 4.1扩容
- 4.2基本的框架
- 4.3插入
- 4.4查找
- 4.5删除
- 5.代码
1.开放地址法
1.1key不能取模的问题
当key是string/Date等类型时,key不能取模,那么我们需要给HashTable增加一个仿函数,这个仿函数支持把key转换成一个可以取模的整形,如果key可以转换为整形并且不容易冲突,那么这个仿函数就用默认参数即可,如果这个Key不能转换为整形,我们就需要自己实现一个仿函数传给这个参数,实现这个仿函数的要求就是尽量key的每个值都参与到计算中,让不同的key转换出的整形值不同。string做哈希表的key非常常见,所以我们可以考虑把string特化一下。
1.1.1将字符串转为整型
key如果是字符串,转为整形需要仿函数
// key / M , M哈希表的空间大小
size_t hash0 = hash(kv.first) % _tables.size();// 将key转为无符号的整形,因为key可能是负数
template<class K>
struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};// 特化
template<>
struct HashFunc<string>
{size_t operator()(const string& s){size_t sum = 0;for (auto& ch : s){sum += ch;sum *= 131;// *131为了避免哈希冲突,每次的key都不一样}return sum;}
};int main()
{const char* a[] = { "abcd","def","gca" };HashTable<string, string> ha;// 类型+()是匿名对象// 哈希冲突了cout << HashFunc<string>()("abcd") << endl;cout << HashFunc<string>()("aadd") << endl;cout << HashFunc<string>()("acbd") << endl;for (auto& ch : a){ha.Insert({ ch,ch });}return 0;
}
1.1.2将日期类转为整型
struct Date
{int _year;int _month;int _day;Date(int year = 1,int month = 1,int day = 1):_year(year),_month(month),_day(day){}bool operator==(const Date& d){return _year == d._year&&_month == d._month&&_day == d._day;}
};struct DateHashFunc
{size_t operator()(const Date& d){size_t hash = 0;hash += d._year;hash *= 131;hash += d._month;hash *= 131;hash += d._day;hash *= 131;return hash;}
};int main()
{// 将日期类转化为整型HashTable<Date, int, DateHashFunc> ht;ht.Insert({ { 2024,12,10 }, 1 });ht.Insert({ { 2024,10,12 }, 1 });return 0;
}
2.哈希函数
设计哈希函数为了减少冲突,让更多的位参与运算,不管使用%不太接近2的幂次方的质数,还是用位运算计算都是可以的
2.1乘法散列法(了解)
- 乘法散列法对哈希表大小M没有要求,他的大思路第一步:用关键字 Key 乘上常数 A (0<A<1),并抽
取出 key * A 的小数部分。第二步:后再用M乘以key*A 的小数部分,再向下取整。- h(key) = floor(M × ((A × key)%1.0)) ,其中floor表示对表达式进行下取整,A∈(0,1),这里最重要的是A的值应该如何设定,Knuth认为 A = ( 5 − 1)/2 = 0.6180339887… (黄金分割点])比较好。
- 乘法散列法对哈希表大小M是没有要求的,假设M为1024,key为1234,A = 0.6180339887, A * key
= 762.6539420558,取小数部分为0.6539420558, M×((A×key)%1.0) = 0.6539420558*1024 =669.6366651392,那么h(1234) = 669。
2.2全域散列法(了解)
- 如果存在一个恶意的对手,他针对我们提供的散列函数,特意构造出一个发生严重冲突的数据集,比如,让所有关键字全部落入同一个位置中。这种情况是可以存在的,只要散列函数是公开且确定的,就可以实现此攻击。解决方法自然是见招拆招,给散列函数增加随机性,攻击者就无法找出确定可以导致最坏情况的数据。这种方法叫做全域散列。
- hab (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, 则 h34 (8) = ((3 × 8 + 4)%17)%6 = 5 。
- 需要注意的是每次初始化哈希表时,随机选取全域散列函数组中的⼀个散列函数使用,后续增删查改都固定使用这个散列函数,否则每次哈希都是随机选一个散列函数,那么插入是一个散列函数,查找又是另一个散列函数,就会导致找不到插入的key了。
3.处理哈希冲突
3.1线性探测(挨着找)
缺点:堆积
3.2二次探测(跳跃着找)
缺点:无法充分利用位置
3.1和3.2上一篇博客详细说明了
3.3双重散列(了解)
缺点:虽然可以充分利用位置,但是还是要解决冲突的问题
- h1 (key) = hash0 = key % M , hash0位置冲突了,则双重探测公式为: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 - 反例:保证 h2 (key) 与M互质是因为根据固定的偏移量所寻址的所有位置将形成一个群,若最大公约数说无法充分利用整个散列表。举例来说,若初始探查位置为1,偏移量为3,整个散列表大小为12,那么所能寻址的位置为{1, 4, 7, 10},寻址个数为p = gcd(M, h1 (key)) > 1 ,那么所能寻址的位置的个数为 M/P < M ,使得对于一个关键字来
12/gcd(12, 3) = 4 - 下面演示 {19,30,52,74} 等这一组值映射到M=11的表中,设 h2 (key) = key%10 + 1
- 本质是跳跃探测,减少冲突堆积
- 双重散列就是让数据更加地分散,不容易产生哈希冲突
4.链地址法
开放地址法的问题是你占别人位置,别人来了又占其他人的位置,链地址法就不用占别人的位置,自己位置可以存多个位置,用了链表挂了多个数据
4.1扩容
- 开放地址法的负载因子必须小于1,链地址法的负载因子没有这种规定,可以大于1,但是unordered_xxx中最大负载因子基本控制在1,大于1就会扩容。
4.2基本的框架
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 HashTable{typedef HashTable<K, V> Node;public:// 构造HashTable():_tables(__stl_next_prime(0)),_n(0){}private:vector<Node*> _tables;// 指针数组size_t _n;// 表示存了多少个数据};
}
4.3插入
头插,尾插都可以,这里用了头插
// 插入
bool Insert(const pair<K,V>& kv)
{// 负载因子 == 1时扩容if (_n == _tables.size()){// 这种方法每个节点都要拷贝,影响效率// 并且原数组释放完后,不会自动析构每个节点,因为是内置类型// 还要自己写析构函数,比较麻烦//HashTable<K, V> newht;//newht._tables.resize(_stl_next_prime(tables.size() + 1));////for(size_t i = 0;i < _tables.size();i++)//{// // 旧表的数据扩容后可能不冲突,必须一个一个取// Node* cur = _tables[i];// while (cur)// {// newht.Insert(cur->_kv);// cur = cur->_next;// }//}//_tables.swap(newht._tables);vector<Node*> newTable(_tables.size() * 2);for(size_t i = 0;i < _tables.size();i++){// 表旧表中的数据插入到新表中Node* cur = _tables[i];while (cur){Node* next = cur->_next;// 算cur在新表中的位置size_t hashi = cur->_kv.first % newTable.size();cur->_next = newTable[hashi];newTable[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newTable);}size_t hashi = kv.first % _tables.size();// 头插Node* newnode = new Node(kv);newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return true;
}int main()
{int a2[] = { 19,30,5,36,13,20,21,12,24,96 };hash_bucket::HashTable<int, int> ht;for (auto e : a2){ht.Insert({ e,e });}ht.Insert({ 100,100 });ht.Insert({ 200,200 });return 0;
}
4.4查找
// 查找
Node* Find(const K& key)
{size_t hashi = key % _tables.size();Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){return cur;}cur = cur->_next;}return nullptr;
}
4.5删除
删除分为三种情况:
- 头删,让下一个节点变为头节点
- 删除中间的节点,保留前一个节点的指针指向后一个节点的指针
- 尾删,让最后一个节点的前一个节点的指针指向空
- 2和3可以归为一类,删除中间的节点可以是前一个节点指向空
// 删除
bool Erase(const K& key)
{size_t hashi = key % _tables.size();Node* cur = _tables[hashi];Node* prev = nullptr;while (cur){if (cur->_kv.first == key){if (prev == nullptr){// 头删_tables[hashi] = cur->_next;}else{// 删除中间的节点prev->_next = cur->_next;}delete cur;--_n;return true;}else{prev = cur;cur = cur->_next;}}return false;
}
5.代码
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 HashTable{typedef HashNode<K, V> Node;public:// 构造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;}}// 插入bool Insert(const pair<K,V>& kv){Hash hash;// 如果插入的值存在冗余了返回falseif (Find(kv.first)){return false;}// 负载因子 == 1时扩容if (_n == _tables.size()){// 这种方法每个节点都要拷贝,影响效率// 并且原数组释放完后,不会自动析构每个节点,因为是内置类型// 还要自己写析构函数,比较麻烦//HashTable<K, V> newht;//newht._tables.resize(_stl_next_prime(tables.size() + 1));////for(size_t i = 0;i < _tables.size();i++)//{// // 旧表的数据扩容后可能不冲突,必须一个一个取// Node* cur = _tables[i];// while (cur)// {// newht.Insert(cur->_kv);// cur = cur->_next;// }//}//_tables.swap(newht._tables);vector<Node*> newTable(__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;// 算cur在新表中的位置size_t hashi = hash(cur->_kv.first) % newTable.size();cur->_next = newTable[hashi];newTable[hashi] = cur;cur = next;}_tables[i] = nullptr;}_tables.swap(newTable);}size_t hashi = hash(kv.first) % _tables.size();// 头插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){size_t hashi = key % _tables.size();Node* cur = _tables[hashi];Node* prev = nullptr;while (cur){if (cur->_kv.first == key){if (prev == nullptr){// 头删_tables[hashi] = cur->_next;}else{// 删除中间的节点prev->_next = cur->_next;}delete cur;--_n;return true;}else{prev = cur;cur = cur->_next;}}return false;}private:vector<Node*> _tables;// 指针数组size_t _n;// 表示存了多少个数据};
}