1. unordered系列关联式容器
在了解哈希之前我们先简单了解一下unordered系列的关联式容器,因为其底层就是用哈希来实现的,其实也没啥好说的,C++11中,STL又提供了unordered系列的关联式容器(unordered_map和unordered_set),与红黑树结构的关联式容器(map和set)使用方式基本类似,就是其底层结构不一样而已
感兴趣的可以看一下链接
unordered_map:
https://cplusplus.com/reference/unordered_map/unordered_map/?kw=unordered_map
unordered_set:
https://cplusplus.com/reference/unordered_set/unordered_set/?kw=unordered_set
2.底层结构
2.1哈希概念
哈希/散列其本质是存储的值和存储位置的映射
2.2哈希函数
哈希函数设计原则:
- 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值
- 域必须在0到m-1之间
- 哈希函数计算出来的地址能均匀分布在整个空间中
- 哈希函数应该比较简单
常见的哈希函数:
直接定值法
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B优点:简单、均匀缺点:需要事先知道关键字的分布情况使用场景:适合查找比较小且连续的情况面试题:字符串中第一个只出现一次字符
除留余数法
设散列表中允许的 地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址
2.3哈希冲突
对于两个数据元素的关键字$k_i$和 $k_j$(i != j),有$k_i$ != $k_j$,但有:Hash($k_i$) == Hash($k_j$)即: 不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞 。
注意:哈希冲突是无法避免的(在有限的空间中存无限的值,不论哈希函数多精妙,哈希冲突的结果是必然的),不过哈希函数设计得越精妙那么产生哈希冲突的可能性就越低
2.4解决哈希冲突
2.4.1闭散列解决
闭散列的开放定址法(本质是当前位置冲突了,后面找一个位置继续存储就得了)常见两种1.线性探测 2.二次探测
hash(key)可以看成下标,结合上图如果i位置已经有了,就线性往后查找到空位置放进去
查找
i=key%表的大小
如果i位置不是要找的key,就往后找直到找到或者遇到空,如果找到表示结尾的位置(还没遇到空),就要往头回绕了
插入
通过哈希函数获取待插入元素在哈希表中的位置如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素
删除
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素。
啥是伪删除法呢?在表中的节点(存数据的)增加一个表状态的成员变量(空、存在、删除)
enum State//状态
{EMPTY,//空EXIST, //存在DELETE//删除
};
template<class K, class V>
struct HashData
{pair<K, V> _val;State _state = EMPTY; // 标记
};
线性探测优点:实现非常简单,线性探测缺点: 一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。
上文中说到将无限的数据插入到有限的空间中,随着空间中的元素越来越多,插入元素时产生哈希冲突的概率也就越来越大,既然冲突是必然发生的,那么多次冲突后查找的效率也就必然会降低
由此我们不得不考虑到这个问题:哈希表什么情况下进行扩容?如何扩容?
对于扩容,哈希表中增加了负载因子(载荷因子)
负载因子=表中数据的个数/空间大小
负载因子越大,表明表中填入的元素越多(空间利用率越高),产生冲突的可能性也就越大;反之,负载因子越小,表明表中填入的元素越少(空间利用率越低),产生冲突的可能性也就越小
对于开放定址法来说负载因子是一个特别重要的因素,最好控制在0.7~0.8以下,超过这个值应该对哈希表进行扩容
2.二次探测
研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容。
因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。
2.4.2开散列(哈希桶)
数组里面存冲突的指针然后像链表一样插入
再讲一下极端条件下的情况
所有元素的哈希值均相同,最终都放到同一个哈希桶中,此时这个哈希表的增删查改效率会降低
有的实现方法是当桶中元素个数超过一定长度,会把桶中的链表结构改成红黑树结构
当然不做这个处理也行,反正数据一多负载因子会增长,最后触发扩容条件
3.实现
3.1线性探测的实现
前面线性探测解决哈希冲突中我们提到不能直接删除哈希表中的元素,要采用伪删除
enum State//枚举状态{EMPTY,//空EXIST, //存在DELETE//删除};template<class K, class V>struct HashData{pair<K, V> _val;State _state = EMPTY; // 标记};
哈希表的构造和框架
初始化的时候顺便把扩容的事情搞定
template<class K, class V>
class HashTable
{public:HashTable(size_t size = 10){_ht.resize(size);}
private:vector<HashData<K, V>> _ht;//hashtablesize_t _n;
};
查找和插入的思路在闭散列解决哈希冲突中已经说过我就不重复讲了
查找
// 查找
HashData<K, V>* Find(const K& key)
{size_t hashi = key % _ht.size();while (_ht[hashi]._state != EMPTY)//不等于空说明有存东西{if (_ht[hashi]._val.first == key){return &_ht[hashi];}hashi++;hashi %= _ht.size();}return nullptr;
}
插入
不过扩容的地方要注意,要讲旧表的数据一个个插入新表中,要重新计算哈希值(也就是下标)
bool Insert(const pair<K, V>& val)
{if (Find(val.first))return false;//扩容if (_n * 10 / _ht.size() >= 7){HashTable<K, V> newtable(_ht.size() * 2);for (auto& e : _ht){if (e._state == EXIST){newtable.Insert(e._val);}}_ht.swap(newtable._ht);}//线性探测size_t hashi = val.first % _ht.size();while (_ht[hashi]._state == EXIST)//如果位置被占了往后走{hashi++;hashi %= _ht.size();//中间走到后面,然后超过后面返回前面}// 找到空位放进去_ht[hashi]._val = val;_ht[hashi]._state = EXIST;_n++;return true;
}
删除
删除的步骤也是非常简单啊,find找一下有没有这个值,然后状态改为删除,哈希表中的有效个数-1就得了
bool Erase(const K& key)
{HashData<K, V>* ret = Find(key);if (ret){_n--;ret->_state = DELETE;return true;}return false;
}
源代码
#pragma once
#include<vector>namespace Close_Hash
{enum State//状态{EMPTY,//空EXIST, //存在DELETE//删除};template<class K, class V>struct HashData{pair<K, V> _val;State _state = EMPTY; // 标记};template<class K, class V>class HashTable{public:HashTable(size_t size = 10){_ht.resize(size);}// 查找HashData<K, V>* Find(const K& key){size_t hashi = key % _ht.size();while (_ht[hashi]._state != EMPTY)//不等于空说明有存东西{if (_ht[hashi]._val.first == key){return &_ht[hashi];}hashi++;hashi %= _ht.size();}return nullptr;}// 插入bool Insert(const pair<K, V>& val){if (Find(val.first))return false;//扩容if (_n * 10 / _ht.size() >= 7){HashTable<K, V> newtable(_ht.size() * 2);for (auto& e : _ht){if (e._state == EXIST){newtable.Insert(e._val);}}_ht.swap(newtable._ht);}//线性探测size_t hashi = val.first % _ht.size();while (_ht[hashi]._state == EXIST)//如果位置被占了往后走{hashi++;hashi %= _ht.size();//中间走到后面,然后超过后面返回前面}// 找到空位放进去_ht[hashi]._val = val;_ht[hashi]._state = EXIST;_n++;return true;}// 删除bool Erase(const K& key){HashData<K, V>* ret = Find(key);if (ret){_n--;ret->_state = DELETE;return true;}return false;}private:size_t HashFunc(const K& key){return key % _ht.capacity();}private:vector<HashData<K, V>> _ht;//hashtablesize_t _n;};void TestHT1(){int a[] = { 1,4,24,34,7,44,17,37 };HashTable<int, int> ht;for (auto e : a){ht.Insert(make_pair(e, e));}for (auto e : a){auto ret = ht.Find(e);if (ret){cout << ret->_val.first << endl;}else{cout << ret->_val.first << ":N" << endl;}}cout << endl;ht.Erase(34);ht.Erase(4);for (auto e : a){auto ret = ht.Find(e);if (ret){cout << ret->_val.first << endl;}else{cout << e << ":N" << endl;}}cout << endl;}}
3.2哈希桶的实现
表由多个桶组成(表是数组),桶(链表)里面的节点
template <class K,class V>
struct HashNode//节点
{HashNode<K,V>* _next;pair<K, V> _kv;HashNode(const pair<K,V>& kv):_next(nullptr),_kv(kv){}
};
哈希表的框架
template<class K, class V>
class HashTable
{typedef HashNode<K,V> Node;
public://。。。
private:vector<Node*> _tables; // 指针数组size_t _n;
};
构造
HashTable()
{_tables.resize(10, nullptr);_n=0;
}
查找
对传来的值找下表,找不到返回空,找到了遍历这个桶(链表),如果找到了返回那个节点,找不到继续往下走直到走到空
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;}
插入
思路和线性探测的插入思路差不多
找那个值,如果已经有了那就不用插入了
如果没有那么就找到这个值对应的下标,然后头插入桶(链表),然后有效元素个数+1
判断负载因子看看要不要扩容,扩容还是创建新表,遍历旧表插入新表(节点挂到新表的下标可能不一样),再交换一下新旧表
bool Insert(const pair<K, V>& kv){//插入失败(找不到)if (Find(kv.first))return false;//扩容if (_n == _tables.size())//负载因子极限{vector< Node*> newtable(_tables.size() * 2, nullptr);//把旧桶的节点挂到新表新桶中for (size_t i=0;i<_tables.size();i++){Node* cur = _tables[i];//遍历走一个个旧桶while (cur){Node* next = cur->_next;//存一下next,待会可以往下走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;}
删除
删除的思路不一样(哈希桶可以直接删节点)
找下标,创建一个前驱节点(链表的删除嘛)
遍历对应下标的桶,如果找到的不是头节点,前驱直接指向遍历节点的下一个;如果找到的是头节点,头节点直接成遍历节点的下一个。最后delete删除遍历节点,减一下有效元素个数
bool Erase(const K& key)
{size_t hashi = key % _tables.size();Node* prev = nullptr;//保存遍历节点的上一个Node* cur = _tables[hashi];while (cur) {if (cur->_kv.first == key)//找到{//删除if (prev)//节点有可能是头节点 prev非空,不是头节点{prev->_next = cur->_next;}else//要删除的节点是头节点{_tables[hashi] = cur->_next;}delete cur;_n--;return true;}prev = cur;cur = cur->_next;}return false;
}
源码
#pragma once
#include <vector>
版本1
namespace HashBucket
{template <class K,class V>struct HashNode//表的节点(桶口){HashNode<K,V>* _next;pair<K, V> _kv;HashNode(const pair<K,V>& kv):_next(nullptr),_kv(kv){}};template<class K, class V>class HashTable{typedef HashNode<K,V> Node;public:HashTable(){_tables.resize(10, nullptr);_n=0;}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;}bool Insert(const pair<K, V>& kv){//插入失败(找不到)if (Find(kv.first))return false;//扩容if (_n == _tables.size())//负载因子极限{vector< Node*> newtable(_tables.size() * 2, nullptr);//把旧桶的节点挂到新表新桶中for (size_t i=0;i<_tables.size();i++){Node* cur = _tables[i];//遍历走一个个旧桶while (cur){Node* next = cur->_next;//存一下next,待会可以往下走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;}bool Erase(const K& key){size_t hashi = key % _tables.size();Node* prev = nullptr;//保存遍历节点的上一个Node* cur = _tables[hashi];while (cur) {if (cur->_kv.first == key)//找到{//删除if (prev)//节点有可能是头节点 prev非空,不是头节点{prev->_next = cur->_next;}else//要删除的节点是头节点{_tables[hashi] = cur->_next;}delete cur;_n--;return true;}prev = cur;cur = cur->_next;}return false;}private:vector<Node*> _tables; // 指针数组size_t _n;};void TestHB1(){int a[] = { 15,7,8,32,24,5,6,9,13,14,19,22,24,17};HashTable<int, int> ht;for (auto e : a){ht.Insert(make_pair(e, e));}for (auto e : a){auto ret = ht.Find(e);if (ret){cout << ret->_kv.first <<":EXIST" << endl;}else{cout << ret->_kv.first << ":DELETE"<< endl;}}cout << endl;ht.Erase(24);ht.Erase(32);for (auto e : a){auto ret = ht.Find(e);if (ret){cout << ret->_kv.first << ":EXIST" << endl;}else{cout << e << ":DELETE" << endl;}}}
}