目录
一,引言
哈希(hash)又称散列,是⼀种组织数据的方式。从译名来看,有散乱排列的意思。本质就是通过哈希 函数把关键字Key跟存储位置建立⼀个映射关系,查找时通过这个哈希函数计算出Key存储的位置,进行快速查找。实现哈希表的主要方法有两大种:**开放地址法,链地址法。**其次哈希表涉到一些专属名词如:哈希冲突,负载因子,将关键字转化成整形,哈希函数,以及一系列哈希表的限制原因。
二,哈希冲突
在上面的概念讲解中,提及到映射关系。本质上来说哈希就是一种对应关系,而哈希冲突就是对应关系的冲突。举个例子:现在有一个大小为10的数组来存放数据,现在有5数分别为0,10,1,11,2 。
通过对应关系:如0对应第一个方格,1对应第二个方格等等。此时会发现,一共有十个格子,10通过取模回到了0的位置。此时0已经存放过数据。此时就出现了哈希冲突。一个好的哈希函数要尽量去减少冲突,但是哈希冲突是不可避免的。
三,负载因子
假设哈希表中已经映射存储了N个值,哈希表的大小为M,那么 N 负载因子 =N/M 。负载因子越大,哈希冲突的概率越高,空间 利⽤率越高;负载因子越小,哈希冲突的概率越低,空间利⽤率越低;

四,将关键词转发为整数
在哈希表的映射关系中一般来说是要通过整数取模等等操作来进行映射。若不是整数要通过一系列仿函数来进行转化。最终转化为整数。
五,哈希函数
一个好的哈希函数要让数据均匀的分配到哈希空间中。在设计思路上要尽力靠近这个原则。下面来看常见的几种设计方法。
1,除法散列法
除法散列法也叫做除留余数法,顾名思义,假设哈希表的大小为M,那么通过key除以M的余数作为映射位置的下标,也就是哈希函数为:h(key)=key%M。

2,乘法散列法

以及全域散列法等等,这些不需要详细进行掌握。了解一下即可。
六,处理哈希冲突
在实践中一般来说选择除法散列作为哈希函数。那么来说处理哈希冲突的方法一般有两种:开放定址法,链地址法。
1,开放地址法
在开放定址法中所有的元素都放到哈希表⾥,当⼀个关键字key用哈希函数计算出的位置冲突了,则按照某种规则找到⼀个没有存储数据的位置进行存储,开放定址法中负载因子⼀定是小于的。这里的规则有三种:线性探测、⼆次探测、双重探测。
1,线性探测
即从发生冲突的位置开始以此向后探测,若走到哈希表尾部则回绕到开头,直到空的位置来存放数据。
线性探测会存在连续堆积的问题,即hash0,hash1,hash2都已经存在数据,则以后的数据如果是这三个位置的数据,则都会去争抢hash3这个位置,进而造成连续堆积的问题。下面列举一组数据来演示线性探测:

这八个数据存储在是一个空间的哈希表:
h(19) = 8 , h(30) = 8 , h(5) = 5 , h(36) = 3 , h(13) = 2 , h(20) = 9 , h(21) = 10 , h(12) = 1。

由上文的计算表来进行依次存储。

按照顺序进行此次存储。
2,二次探测
从发生冲突的位置开始,依次左右按二次方跳跃式探测,直到寻找到下⼀个没有存储数据的位置为为止,如果往右走到哈希表尾,则回绕到哈希表头的位置;如果往左走到哈希表头,则回绕到哈希表尾的位置。

二次探测就是跳着来进行探测,可以减少线性探测数据堆积的现象。在原理上都差不多。
2,开放定址法的代码实现
首先是开放定址法的基本结构在一个数组的基础上,将本来数组内部的节点换成节点的指针,当遇到相同位置的多组数据时,以链式形式向下链接。就避免了数据堆积的问题。
数据节点:
cpp
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)
{}
};
仿函数:返回一个整形,来确定在数组位置
cpp
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
// 特化
template<>
struct HashFunc<string>
{
size_t operator()(const string& key)
{
size_t hash = 0;
for (auto e : key)
{
hash *= 131;
hash += e;
}
return hash;
}
};
哈希表的基础结构:
cpp
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:
HashTable()
{
_tables.resize(__stl_next_prime(0), nullptr);
}
private:
vector<Node*> _tables;
size_t _n = 0;
};
在上文中讲到为了使得每次扩容的大小都更加接近质数。通过上述质数表来控制扩容的大小。
插入部分:
cpp
bool Insert(const pair<K, V>& kv)
{
Hash hs;
size_t hashi = hs(kv.first) % _tables.size();
if (_n == _tables.size())
{
vector<Node*>
newtables(__stl_next_prime(_tables.size() + 1), nullptr);
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
// 旧表中节点,挪动新表重新映射的位置
size_t hashi = hs(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;
}