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


非常简单吧。
除了直接定址法,常用的哈希映射方法还有除法散列法,下面我们也使用这种方法实现哈希函数。 除法散列法,也叫除留余数法。假设哈希表的大小为M,那么key除以M的余数作为映射位置的下标,也就是哈希函数为:h(key) = key % M。 使用除法散列法时,要尽量避免使用2的幂次、10的幂次之类的值。因为key%2x相当于留下key的二进制的后x位,key%10x相当于留下key的十进制的后x位,就更容易导致哈希冲突。根据前人的总结,M最好取不接近2的整数次幂的质数。
除留余数法最重要的要求是,key能够取模,因此key的类型必须是整数或能转换为整数。
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};key是无法直接强制转换为整型的类型时,如string,就可以对hashFunc进行模板特化,单独写一个方法:字符串转换为整型,可以选择直接把字符的ASCII值相加,但是这样计算类似“abcd”和“acdb”结果是一样的。前人总结出的一个绝佳方法是,上一次计算的结果乘以一个质数,一般是31或131:
// 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;
}
};哈希冲突是避免不了的,所以需要学会处理哈希冲突。处理方式一般有开放定址法、链地址法。 例如,将一组数据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),两个数据的存储位置冲突了。
开放定址法是,当一个关键字key用哈希函数计算出的位置冲突了,则按照某种规则找到一个没有存储数据的空位置进行存储,开放定址法中负载因子一定是小于1的。寻找空位置的规则有三种:线性探测、二次探测、双重探测。
下面我们主要使用线性探测,用开放定址法实现哈希表: 先搭出框子:
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版本的哈希表方法,提供了一个质数表:
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;
}我们这里也借用这个质数表,每次去这个表里获取哈希表扩容后的下一个大小。
开放定址法的哈希表完整实现:
#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;
};
}开放定址法中,所有元素都放在哈希表中。链地址法中所有的数据不再直接存储在哈希表中,而是哈希表中每一个位置存储一个指针,没有数据映射到这个位置时,指针为空,有多个数据映射到这个位置时,把冲突的数据连接成一个链表,“挂在”这个哈希表位置下面。链地址法也叫拉链法或哈希桶。
举个例子:

开放定址法的负载因子必须小于1,而链地址法的负载因子就没有限制了,可以大于1。负载因子越大,哈希冲突的概率越高,空间利用率也高;负载因子越小,哈希冲突的概率越低,空间利用率也越低。STL中unordered_xxx系列容器的最大负载因子基本控制在1,大于1就扩容,我们下面也使用这个方式。
链地址法的哈希桶完整实现:
#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
本篇完,感谢阅读。