哈希表实现
1. 哈希概念
哈希(hash)⼜称散列,是⼀种组织数据的⽅式。本质就是通过哈希 函数把关键字Key跟存储位置建⽴⼀个映射关系,查找时通过这个哈希函数计算出Key存储的位置,进 ⾏快速查找。
哈希表底层是vector,是一个数组。为什么叫散列,因为数据放在哈希表中是相对无序的。
1.1 哈希冲突:不可避免的问题
在哈希表中两个不同的key可能会映射到同⼀个位置去,这种问题我们叫做哈希冲突, 或者哈希碰撞。理想情况是找出⼀个好的哈希函数避免冲突,但是实际场景中,冲突是不可避免的, 所以我们尽可能设计出优秀的哈希函数,减少冲突的次数,同时也要去设计出解决冲突的⽅案。
1.2 负载因子:哈希表的 "拥挤程度"
负载因子(Load Factor)是衡量哈希表拥挤程度的指标,决定了哈希表是否需要扩容,公式:

- 负载因子越大:哈希表越拥挤,冲突概率越高,空间利用率越高;
- 负载因子越小:哈希表越宽松,冲突概率越低,空间利用率越低。
1.3 将key转为整数
我们将关键字映射到数组中位置,⼀般是整数好做映射计算,如果不是整数,我们要想办法转换成整 数。我们传的key不可能都是无符号整数,因此我们得自己设计一个仿函数将key转为无符号整数。
2.哈希函数
⼀个好的哈希函数应该让N个关键字被等概率的均匀的散列分布到哈希表的M个空间中,但是实际中却 很难做到,但是我们要尽量往这个⽅向去考量设计。
2.1除法散列法/除留余数法
• 除法散列法也叫做除留余数法,顾名思义,假设哈希表的⼤⼩为M,那么通过key除以M的余数作为 映射位置的下标,也就是哈希函数为:h(key)=key%M。
当使⽤除法散列法时,要尽量避免M为某些值,如2的幂,10的幂等。如果是 ,那么key%2^x 本质相当于保留key的后X位,那么后x位相同的值,计算出的哈希值都是⼀样的,就冲突了。
所以当我们使⽤除法散列法时,建议M取远离2的整数次幂且为质数(素数)的值。
2.2 乘法散列法(了解)
• 乘法散列法对哈希表⼤⼩M没有要求,他的⼤思路第⼀步:⽤关键字K乘上常数A[0<A<1),
取出k*A的⼩数部分。第⼆步:后再⽤M乘以k*A的⼩数部分,再向下取整。
• h(key) = floor(M ×((A ×key)%1.0)) ,其中floor表⽰对表达式进⾏下取整,A∈[0,1),这⾥ 最重要的是A的值应该如何设定,Knuth认为 A =( −5 1)/2 = 0.6180339887.... (⻩⾦分割点) ⽐较好。
3.处理哈希冲突
实践中哈希表⼀般还是选择除法散列法作为哈希函数,当然哈希表⽆论选择什么哈希函数也避免不了 冲突,那么插⼊数据时,如何解决冲突呢?主要有两种两种⽅法,开放定址法和链地址法。
3.1 开放定址法
在开放定址法中所有的元素都放到哈希表⾥,当⼀个关键字key⽤哈希函数计算出的位置冲突了,则按 照某种规则找到⼀个没有存储数据的位置进⾏存储,开放定址法中负载因⼦⼀定是⼩于的。这⾥的规 则有三种:线性探测、⼆次探测、双重探测。
线性探测
• 从发⽣冲突的位置开始,依次线性向后探测,直到寻找到下⼀个没有存储数据的位置为⽌,如果⾛ 到哈希表尾,则回绕到哈希表头的位置。
• h(key) = hash0 = key % M hc(key,i) = hashi = (hash0+i) % M , i = {1,2,3,...,M −1} , hash0位置冲突了,则线性探测公式为: ,因为负载因⼦⼩于1, 则最多探测M-1次,⼀定能找到⼀个存储key的位置。
• 线性探测的⽐较简单且容易实现,线性探测的问题假设,hash0位置连续冲突,hash0,hash1, hash2位置已经存储数据了,后续映射到hash0,hash1,hash2,hash3的值都会争夺hash3位 置,这种现象叫做群集/堆积。下⾯的⼆次探测可以⼀定程度改善这个问题。
• 下⾯演⽰{19,30,5,36,13,20,21,12} 等这⼀组值映射到M=11的表中。

h(19) = 8 , h(30) = 8 , h(5) = 5 , h(36) = 3 , h(13) = 2 , h(20) = 9 , h(21) = 10 , h(12) = 1

⼆次探测
• 从发⽣冲突的位置开始,依次左右按**⼆次⽅跳跃**式探测,直到寻找到下⼀个没有存储数据的位置为 ⽌,如果往右⾛到哈希表尾,则回绕到哈希表头的位置;如果往左⾛到哈希表头,则回绕到哈希表 尾的位置;
• h(key) = hash0 = key % M , hash0位置冲突了,则⼆次探测公式为: 2 hc(key,i) = hashi = (hash0±i ) % M , i = 2 hashi = (hash0−i )%M M {1,2,3,..., } 2
• ⼆次探测当时,当hashi<0时,需要hashi+=M
• 下⾯演⽰ {19,30,52,63,11,22} 等这⼀组值映射到M=11的表中。

h(19) = 8, h(30) = 8, h(52) = 8, h(63) = 8, h(11) = 0, h(22) = 0

优点:减少堆积;缺点:有空间浪费。
开放定址法代码实现
开放定址法在实践中,不如链地址法,因为开放定址法解决冲突不管使⽤哪种⽅法,占⽤的 都是哈希表中的空间,始终存在互相影响的问题。所以开放定址法我们简单选择线性探测实现即 可。
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 Hash=HashFunc<K>>
class HashTable
{
public:
HashTable()
:_tables(__stl_next_prime(0))
,_n(0)
{ }
bool Insert(const pair<K, V>& kv)
{
//不插入重复值
if (Find(kv.first))
return false;
//负载因子>=0.7就扩容
if (_n*10/_tables.size()>=7)
{
//扩容逻辑...
}
Hash hash;
size_t hash0 = hash(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 hash;
size_t hash0 = hash(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;
}
private:
vector<HashData<K, V>> _tables;
size_t _n;//记录数据个数
};
}
3.2链地址法
解决冲突的思路
放定址法中所有的元素都放到哈希表⾥,链地址法中所有的数据不再直接存储在哈希表中,哈希表 中存储⼀个指针,没有数据映射这个位置时,这个指针为空,有多个数据映射到这个位置时,我们把 这些冲突的数据链接成⼀个链表,挂在哈希表这个位置下⾯,链地址法也叫做拉链法或者哈希桶。
• 下⾯演⽰{19,30,5,36,13,20,21,12,24,96} 等这⼀组值映射到M=11的表中。

h(19) = 8 , h(30) = 8 , h(5) = 5 , h(36) = 3 , h(13) = 2 , h(20) = 9 , h(21) = 10 , h(12) = 1,h(24) = 2,h(96) = 88
链地址法代码实现
cpp
namespace hash_bucket
{
template<class T>
struct HashNode
{
T _data;
HashNode<T>* _next;
HashNode(const T& data)
: _data(data)
, _next(nullptr)
{}
};
template<class K, class T, class KeyOfT, class Hash>
class HashTable
{
private:
vector<HashNode<T>*> _tables;
size_t _n = 0;
public:
bool Insert(const T& data)
{
KeyOfT kot;
if (Find(kot(data))) return false;
// 负载因子 = 1 时扩容
if (_n == _tables.size())
{
vector<HashNode<T>*> new_tables(GetNextPrime(_tables.size()), nullptr);
_tables.swap(new_tables);
}
Hash hs;
size_t hashi = hs(kot(data)) % _tables.size();
// 头插
HashNode<T>* new_node = new HashNode<T>(data);
new_node->_next = _tables[hashi];
_tables[hashi] = new_node;
++_n;
return true;
}
};
}
4.实现哈希表的难点
4.1扩容
这⾥我们哈希表负载因⼦控制在0.7,当负载因⼦到0.7以后我们就需要扩容了,我们还是按照2倍扩 容,但是同时我们要保持哈希表⼤⼩是⼀个质数,第⼀个是质数,2倍后就不是质数了。
那么如何解决呢,有⼀种⽅案是sgi版本的哈希表使⽤的⽅法,给了⼀个近似2倍的质数表,每次去 质数表获取扩容后的⼤⼩。
cpp
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;
}
4.2Key不是整型的问题
当key是string/Date等类型时,key不能取模,那么我们需要给HashTable增加⼀个仿函数,这个仿函 数⽀持把key转换成⼀个可以取模的整形,如果key可以转换为整形并且不容易冲突,那么这个仿函数 就⽤默认参数即可,如果这个Key不能转换为整形,我们就需要⾃⼰实现⼀个仿函数传给这个参数,实 现这个仿函数的要求就是尽量key的每值都参与到计算中,让不同的key转换出的整形值不同。
cpp
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
//特化
template<>
struct HashFunc<string>
{
//BKDR算法
size_t operator()(const string & key)
{
size_t hash = 0;
for (auto e : key)
{
hash *= 131;
hash += e;
}
return hash;
}
};
封装实现unordered_map和unordered_set
C++ STL 中的unordered_map和unordered_set,其底层就是链地址法实现的哈希桶,与本文实现的哈希表核心逻辑一致,仅做了更多工程优化:
- 支持迭代器遍历(单向迭代器,因链表是单向的);
- 实现了更多接口(如
size()、empty()等); - 对哈希函数做了更全面的特化,支持更多类型;
- 对扩容和哈希冲突做了更细致的优化。
可以说,吃透了哈希桶实现,就等于吃透了unordered_map/unordered_set的底层原理。
unordered_map和unordered_set复⽤之前我们实现的哈希表。
cs
// MyUnorderedSet.h
template<class K, class Hash = HashFunc<K>>
class unordered_set {
struct SetKeyOfT {
const K& operator()(const K& key) {
return key;
}
};
public:
bool insert(const K& key) {
return _ht.Insert(key);
}
private:
hash_bucket::HashTable<K, K, SetKeyOfT, Hash> _ht;
};
// MyUnorderedMap.h
template<class K, class V, class Hash = HashFunc<K>>
class unordered_map {
struct MapKeyOfT {
const K& operator()(const pair<K, V>& kv) {
return kv.first;
}
};
public:
bool insert(const pair<K, V>& kv) {
return _ht.Insert(kv);
}
V& operator[](const K& key) {
auto ret = _ht.Insert(make_pair(key, V()));
return ret.first->second;
}
private:
hash_bucket::HashTable<K, pair<K, V>, MapKeyOfT, Hash> _ht;
};
unordered_map和unordered_set的功能和map和set的大致一样,用封装实现时也是调用哈希表的功能,主要难点是迭代器的operator++。
iterator中有⼀个指向结点的指针,如果当前桶下⾯还有结点, 则结点的指针指向下⼀个结点即可。如果当前桶⾛完了,则需要想办法计算找到下⼀个桶。
++有以下两种情况:
1.在每个哈希节点的链表里面++;
2.已到当前链表的尾部,需要跳到下一个不为空的链表中。
operator++的实现:
cpp
Self& operator++()
{
if (_node->_next)
{
//当前桶还有数据,走到当前桶下一个节点
_node = _node->_next;
}
else
{
//当前桶走完了,找下一个不为空的桶
KeyOfT kot;
Hash hash;
size_t hashi = hash(kot(_node->_data)) % _ht->_tables.size();
++hashi;
while (hashi < _ht->_tables.size())
{
_node = _ht->_tables[hashi];
if (_node)
break;
else
++hashi;
}
//走完所有桶
if (hashi == _ht->_tables.size())
{
_node = nullptr;
}
}
return *this;
}
哈希表的完整实现:
cpp
#include<iostream>
#include<vector>
using namespace std;
enum State
{
EXIST,//存在
EMPTY,//空
DELETE//删除
};
template<class K, class V>
struct HashData
{
pair<K, V> _kv;
State _state = EMPTY;
};
//key转成整形的虚函数
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
//特化后的string转整形的虚函数
template<>
struct HashFunc<string>
{
size_t operator()(const string& s)
{
//BKDR算法
size_t hash = 0;
for (auto ch : s)
{
hash += ch;
hash *= 131;
}
return hash;
}
};
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;
}
namespace hash_bucket
{
template<class T>
struct HashNode
{
T _data;
HashNode<T>* _next;
HashNode(const T& data)
:_data(data)
,_next(nullptr)
{ }
};
//前置声明,以防编译器找不到
template<class K,class T,class KeyOfT,class Hash>
class HashTable;
template<class K,class T,class Ref,class Ptr,class KeyOfT,class Hash>
//迭代器部分
struct HTIterator
{
typedef HashNode<T> Node;
typedef HashTable<K, T, KeyOfT, Hash> HT;
typedef HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;
Node* _node;//
const HT* _ht;//
HTIterator(Node* node,const HT* ht)
:_node(node)
,_ht(ht)
{ }
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
bool operator!=(const Self&s)
{
return _node != s._node;
}
Self& operator++()
{
if (_node->_next)
{
//当前桶还有数据,走到当前桶下一个节点
_node = _node->_next;
}
else
{
//当前桶走完了,找下一个不为空的桶
KeyOfT kot;
Hash hash;
size_t hashi = hash(kot(_node->_data)) % _ht->_tables.size();
++hashi;
while (hashi < _ht->_tables.size())
{
_node = _ht->_tables[hashi];
if (_node)
break;
else
++hashi;
}
//走完所有桶
if (hashi == _ht->_tables.size())
{
_node = nullptr;
}
}
return *this;
}
};
template<class K,class T,class KeyOfT,class Hash>
class HashTable
{
template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
friend struct HTIterator;
typedef HashNode<T> Node;
public:
typedef HTIterator<K, T, T&, T*, KeyOfT, Hash>Iterator;
typedef HTIterator<K, T, const T&, const T*, KeyOfT, Hash>ConstIterator;
Iterator Begin()
{
if (_n == 0)
return End();
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
if (cur)
{
return Iterator(cur, this);
}
}
return End();
}
Iterator End()
{
return Iterator(nullptr, this);
}
ConstIterator Begin() const
{
if (_n == 0)
return End();
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
if (cur)
{
return ConstIterator(cur, this);
}
}
return End();
}
ConstIterator End() const
{
return ConstIterator(nullptr, this);
}
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;
}
}
pair<Iterator, bool> Insert(const T& data)
{
KeyOfT kot;
Iterator it = Find(kot(data));
if (it != End())
return { it,false };
Hash hash;
//负载因子==1时扩容
if (_n == _tables.size())
{
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;
//头插到新表
size_t hashi = hash(kot(cur->_data)) % newTable.size();
cur->_next = newTable[hashi];
newTable[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTable);
}
size_t hashi = hash(kot(data)) % _tables.size();
//头插
Node* newnode = new Node(data);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_n;
return { Iterator(newnode,this),false };
}
Iterator Find(const K& key)
{
KeyOfT kot;
Hash hash;
size_t hashi = hash(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
return Iterator(cur, this);
}
cur = cur->_next;
}
return End();
}
bool Erase(const K& key)
{
KeyOfT kot;
size_t hashi = key % _tables.size();
Node* prev = nullptr;
Node* cur = _tables[hashi];
while (cur)
{
if (kot(cur->_data) == 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 = 0;
};
}
unordered_map的完整实现:
cpp
#include"HashTable.h"
namespace my
{
template<class K,class V,class Hash=HashFunc<K>>
class unordered_map
{
struct MapKeyOfT
{
const K& operator()(const pair<K, V>& kv)
{
return kv.first;
}
};
public:
typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::Iterator iterator;
typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::ConstIterator const_iterator;
iterator begin()
{
return _ht.Begin();
}
iterator end()
{
return _ht.End();
}
const_iterator begin() const
{
return _ht.Begin();
}
const_iterator end() const
{
return _ht.End();
}
V& operator[](const K& key)
{
pair<iterator, bool> ret = insert({ key, V() });
return ret.first->second;
}
pair<iterator, bool> insert(const pair<K, V>& kv)
{
return _ht.Insert(kv);
}
iterator Find(const K& key)
{
return _ht.Find(key);
}
bool Erase(const K& key)
{
return _ht.Erase(key);
}
private:
hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
};
void test_map1()
{
unordered_map<string, string> dict;
dict.insert({ "sort", "排序" });
dict.insert({ "字符串", "string" });
dict.insert({ "sort", "排序" });
dict.insert({ "left", "左边" });
dict.insert({ "right", "右边" });
dict["left"] = "左边,剩余";
dict["insert"] = "插入";
dict["string"];
for (auto& kv : dict)
{
cout << kv.first << ":" << kv.second << endl;
}
cout << endl;
unordered_map<string, string>::iterator it = dict.begin();
while (it != dict.end())
{
// 不能修改first,可以修改second
//it->first += 'x';
it->second += 'x';
cout << it->first << ":" << it->second << endl;
++it;
}
cout << endl;
}
}
unordered_set的完整实现:
cpp
#include"HashTable.h"
namespace my
{
template<class K,class Hash=HashFunc<K>>
class unordered_set
{
struct SetKeyOfT
{
const K& operator()(const K& key)
{
return key;
}
};
public:
typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::Iterator iterator;
typedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::ConstIterator const_iterator;
iterator begin()
{
return _ht.Begin();
}
iterator end()
{
return _ht.End();
}
const_iterator begin() const
{
return _ht.Begin();
}
const_iterator end() const
{
return _ht.End();
}
pair<iterator, bool> insert(const K& key)
{
return _ht.Insert(key);
}
iterator Find(const K& key)
{
return _ht.Find(key);
}
bool Erase(const K& key)
{
return _ht.Erase(key);
}
private:
hash_bucket::HashTable<K, const K, SetKeyOfT, Hash> _ht;
};
void print(const unordered_set<int>& s)
{
unordered_set<int>::const_iterator it = s.begin();
while (it != s.end())
{
//*it = 1;
cout << *it << " ";
++it;
}
cout << endl;
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
}
void test_set1()
{
int a[] = { 3,11,86,7,88,82,1,881,5,6,7,6 };
unordered_set<int> s;
for (auto e : a)
{
s.insert(e);
}
unordered_set<int>::iterator it = s.begin();
while (it != s.end())
{
//*it = 1;
cout << *it << " ";
++it;
}
cout << endl;
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
print(s);
}
}
