各位大佬好,我是落羽!一个坚持不断学习进步的学生。
如果您觉得我的文章还不错,欢迎多多互三分享交流,一起学习进步!
也欢迎关注我的blog主页: 落羽的落羽
文章目录
- 一、哈希是什么
- 二、哈希表实现
-
-
- 哈希表相关概念
-
- 除法散列法
-
- 将关键字转为整数
-
- 处理哈希冲突
-
- 4.1 开放定址法
- 4.2 链地址法
-
一、哈希是什么
哈希(hash),又称散列,是一种组织数据的方式。本质是通过哈希函数把关键字key跟存储位置建立一个映射关系,查找时通过这个哈希函数计算出key存储的位置,进行快速查找。
一个常见的哈希映射例子是,如果要统计一段小写字母文字中各个字母的出现次数,可以开一块大小为26的数组,每个字母的ASCII值 - a的ASCII值就是存储这个字母的次数的数组位置下标。这样经过一次遍历就能统计完了。
上述这种方法,也叫直接定址法。

非常简单吧。
二、哈希表实现
1. 哈希表相关概念
- 哈希函数:直接定址法的缺点也十分明显,当key值的分布范围比较分散时,会导致开的内存空间极大,甚至有很多浪费。假设,我们的数据范围是0~9999的N个值,我们一开始开一块大小为M的空间,我们需要构造一个哈希函数(hash function)hf,关键字key的数据被放在hf(key)的位置上,hf(key)的值必须在[0, M)之间。
- 哈希冲突:还有一个问题,两个不同的key可能会映射到同一个位置上,这种情况叫哈希冲突,或哈希碰撞。最理想的情况是,设计出一种好的哈希函数避免冲突。但是实际应用中,冲突是不可避免的,我们只能尽可能设计出尽可能优秀的哈希函数,尽可能减少冲突。
- 负载因子:假设哈希表中已经映射存储了N个值,哈希表的大小为M,那么负载因子 = N/M。负载因子越大,哈希冲突的概率越高,空间利用率也高;负载因子越小,哈希冲突的概率越低,空间利用率也越低。
2. 除法散列法
除了直接定址法,常用的哈希映射方法还有除法散列法,下面我们也使用这种方法实现哈希函数。
除法散列法,也叫除留余数法。假设哈希表的大小为M,那么key除以M的余数作为映射位置的下标,也就是哈希函数为:h(key) = key % M 。
使用除法散列法时,要尽量避免使用2的幂次、10的幂次之类的值。因为key%2x相当于留下key的二进制的后x位,key%10x相当于留下key的十进制的后x位,就更容易导致哈希冲突。根据前人的总结,M最好取不接近2的整数次幂的质数。
3. 将关键字转为整数
除留余数法最重要的要求是,key能够取模,因此key的类型必须是整数或能转换为整数。
cpp
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
key是无法直接强制转换为整型的类型时,如string,就可以对hashFunc进行模板特化,单独写一个方法:字符串转换为整型,可以选择直接把字符的ASCII值相加,但是这样计算类似"abcd"和"acdb"结果是一样的。前人总结出的一个绝佳方法是,上一次计算的结果乘以一个质数,一般是31或131:
cpp
// string特化
template<>
struct HashFunc<string>
{
size_t operator()(const string& key) const
{
size_t hash = 0;
for (auto ch : key)
{
hash += ch;
hash *= 131;
}
return hash;
}
};
4. 处理哈希冲突
哈希冲突是避免不了的,所以需要学会处理哈希冲突。处理方式一般有开放定址法、链地址法 。
例如,将一组数据30、19、5、36、13、20、21、12映射到大小为11的表中,则h(key) = key % 11。h(30) = 8,h(19) = 8,h(5) = 5,h(36) = 3,h(13) = 2,h(20) = 9,h(21) = 10,h(12) = 1。哈希函数算出的值即为存储它们的数组下标,注意到h(30) = h(19),两个数据的存储位置冲突了。
4.1 开放定址法
开放定址法是,当一个关键字key用哈希函数计算出的位置冲突了,则按照某种规则找到一个没有存储数据的空位置进行存储,开放定址法中负载因子一定是小于1的。寻找空位置的规则有三种:线性探测、二次探测、双重探测。
-
线性探测:从发生冲突的位置开始,依次线性向后探测,直到找到下一个没有存储数据的位置为止,如果找到哈希表尾,则回到哈希表头的位置。因为负载因子小于1,所以最多探测M-1次,一定能找到一个存储key的位置。
h(key) = hashi = (hash0+i) % M, i = {1, 2, 3, ..., M-1}线性探测比较简单而且容易实现,但缺点是如果出现位置的连续冲突,多个数据按照插入顺序的不同可能造成位置混乱,争夺同一个位置,这种现象叫做群集(堆积)。
-
二次探测:从发生冲突的位置开始,依次左右按二次方跳跃式探测,直到寻找到下一个没有存储数据的位置为止,如果往右走到哈希表尾,则回绕到哈希表头的位置;如果往左走到哈希表头,则回绕到哈希表尾的位置。hash0位置冲突了,则二次探测公式为:
h(key) = hash0 = key % M
hc(key, i) = hashi = (hash0 ± i2) % M, i = {1, 2, 3, ..., M/2 }
当 hashi = (hash0 − i2)%M 时,当hashi<0时,需要hashi += M
下面我们主要使用线性探测,用开放定址法实现哈希表:
先搭出框子:
cpp
namespace open_address
{
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;
};
}
要注意的是,我们需要给每个存储值的位置加一个状态标识,否则删除值时,会影响后面新插入的值无法判断这个位置的状态。
哈希表也需要扩容。
这里我们的哈希表的负载因子可以控制在0.7,当负载因子到0.7时就进行一次扩容。假如还按照2倍的扩容,就不能保证下一个M是质数了。一种解决方法是,SGI版本的哈希表方法,提供了一个质数表:
cpp
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;
}
我们这里也借用这个质数表,每次去这个表里获取哈希表扩容后的下一个大小。
开放定址法的哈希表完整实现:
cpp
#include<iostream>
#include<vector>
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;
}
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) const
{
size_t hash = 0;
for (auto ch : key)
{
hash += ch;
hash *= 131;
}
return hash;
}
};
namespace open_address
{
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 Hash = HashFunc<K>>
class HashTable
{
public:
HashTable(size_t n = __stl_next_prime(0))
:_tables(n)
, _n(0)
{}
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first))
{
return false;
}
//负载因子到了0.7就进行扩容
if ((double)_n / (double)_tables.size() >= 0.7)
{
HashTable<K, V, Hash> newht(__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 hs;
size_t hash0 = hs(kv.first) % _tables.size();
size_t hashi = hash0;
size_t i = 1;
//线性探测
while (_tables[hashi]._state == EXIST)
{
hashi = (hash0 + i) % _tables.size();
i++;
}
_tables[hashi]._kv = kv;
_tables[hashi]._state = EXIST;
_n++;
return true;
}
HashData<K, V>* Find(const K& key)
{
Hash hs;
size_t hash0 = hs(key) % _tables.size();
size_t hashi = hash0;
size_t i = 1;
while (_tables[hashi]._state != EMPTY)
{
if (_tables[hashi]._state == EXIST && _tables[hashi]._kv.first == key)
{
return &_tables[hashi];
}
//线性探测
hashi = (hash0 + i) % _tables.size();
i++;
}
return nullptr;
}
bool Erase(const K& key)
{
HashData<K, V>* ret = Find(key);
if (ret)
{
ret->_state == DELETE;
_n--;
return true;
}
else
{
return false;
}
}
private:
vector<HashData<K, V>> _tables;
//记录表中实际存储数据个数
size_t _n = 0;
};
}
4.2 链地址法
开放定址法中,所有元素都放在哈希表中。链地址法中所有的数据不再直接存储在哈希表中,而是哈希表中每一个位置存储一个指针,没有数据映射到这个位置时,指针为空,有多个数据映射到这个位置时,把冲突的数据连接成一个链表,"挂在"这个哈希表位置下面。链地址法也叫拉链法或哈希桶。
举个例子:
开放定址法的负载因子必须小于1,而链地址法的负载因子就没有限制了,可以大于1。负载因子越大,哈希冲突的概率越高,空间利用率也高;负载因子越小,哈希冲突的概率越低,空间利用率也越低。STL中unordered_xxx系列容器的最大负载因子基本控制在1,大于1就扩容,我们下面也使用这个方式。
链地址法的哈希桶完整实现:
cpp
#include<iostream>
#include<vector>
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;
}
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) const
{
size_t hash = 0;
for (auto ch : key)
{
hash += ch;
hash *= 131;
}
return hash;
}
};
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(size_t n = __stl_next_prime(0))
:_tables(n)
,_n(0)
{ }
//涉及结点空间的开辟,因此需要自己写析构函数
~HashTable()
{
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
delete next;
cur = next;
}
_tables[i] = nullptr;
}
}
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first))
{
return false;
}
Hash hs;
//负载因子到了1,需要扩容
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头插到新表
cur->_next = newtables[hashi];
newtables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newtables);
}
size_t hashi = hs(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 hs;
size_t hashi = hs(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 hs;
size_t hashi = hs(key) % _tables.size();
Node* prev = nullptr;
Node* cur = _tables[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
if (prev == nullptr)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
_n--;
delete cur;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
private:
vector<Node*> _tables;
size_t _n; //记录实际存储数据个数
};
}
本文完整项目代码已上传至我的gitee仓库,欢迎浏览:
https://gitee.com/zhang-yunkai060524/luoyu-c-language
本篇完,感谢阅读。