C++ 哈希表底层实现:unordered_map/set、位图与布隆过滤器
C++ 哈希表底层原理详解。涵盖闭散列与开散列冲突解决策略,模拟实现 unordered_set 与 unordered_map。深入讲解位图数据结构及其在海量数据处理中的应用,如交集查找与唯一性判断。介绍布隆过滤器的多哈希函数机制及空间优化方案。最后通过哈希切割解决大文件内存限制下的数据匹配问题,并提供相关算法习题解析。

C++ 哈希表底层原理详解。涵盖闭散列与开散列冲突解决策略,模拟实现 unordered_set 与 unordered_map。深入讲解位图数据结构及其在海量数据处理中的应用,如交集查找与唯一性判断。介绍布隆过滤器的多哈希函数机制及空间优化方案。最后通过哈希切割解决大文件内存限制下的数据匹配问题,并提供相关算法习题解析。

包括 unordered_map, unordered_set, unordered_multimap, unordered_multiset。
这些容器基于哈希表实现,用法与 set 类似,接口基本相同,支持范围 for 遍历。
与 set 的区别:
unordered 系列容器的迭代器是单向迭代器。unordered 系列中序遍历结果无序。unordered 系列通常优于 set;但在有序数据插入场景下,set 表现更好。注意:比较性能应在 Release 模式下进行。
哈希(散列)是建立存储值与存储位置对应关系的方法,类似于计数排序。模拟实现时不要存入相同的 key。 哈希以牺牲空间为代价提高查询效率。
常用方法:
value % n。不同值映射到相同位置导致冲突。解决方式:
采用除留余数法加线性探测。
i。i^2。enum State { EXIST, EMPTY, DELETE };
template<class K, class V>
struct HashData {
pair<K, V> _kv;
State _state = EMPTY;
};
template<class K>
struct DefaultHashFunc {
size_t operator()(const K& key) {
return (size_t)key;
}
};
template<class K, class V, class HashFunc = DefaultHashFunc<K>>
class HashTable {
public:
HashTable() { _table.resize(10); }
bool Insert(const pair<K, V>& kv) {
if (_n * 10 / _table.size() >= 7) {
size_t newSize = _table.size() * 2;
HashTable<K, V, HashFunc> newHT;
newHT._table.resize(newSize);
for (size_t i = 0; i < _table.size(); ++i) {
if (_table[i]._state == EXIST) {
newHT.Insert(_table[i]._kv);
}
}
_table.swap(newHT._table);
}
HashFunc hf;
size_t hashi = hf(kv.first) % _table.size();
while (_table[hashi]._state == EXIST) {
++hashi;
hashi %= _table.size();
}
_table[hashi]._kv = kv;
_table[hashi]._state = EXIST;
++_n;
return true;
}
HashData<const K, V>* Find(const K& key) {
HashFunc hf;
size_t hashi = hf(key) % _table.size();
while (_table[hashi]._state != EMPTY) {
if (_table[hashi]._state == EXIST && _table[hashi]._kv.first == key) {
return (HashData<const K, V>*)&_table[hashi];
}
++hashi;
hashi %= _table.size();
}
return nullptr;
}
bool Erase(const K& key) {
HashData<const K, V>* ret = Find(key);
if (ret) {
ret->_state = DELETE;
--_n;
return true;
}
return false;
}
private:
vector<HashData<K, V>> _table;
size_t _n = 0;
};
使用 EXIST, EMPTY, DELETE 标记状态。删除节点标记为 DELETE 而非 EMPTY,以便在查找时继续向后探测。扩容时不迁移 DELETE 状态的节点。
负载因子越大,冲突概率越高,空间利用率越高;反之亦然。
template<class K, class T, class KeyOfT, class HashFunc = DefaultHashFunc<K>>
class HashTable {
typedef HashNode<T> Node;
template<class K, class T, class Ptr, class Ref, class KeyOfT, class HashFunc>
friend struct HTIterator;
public:
typedef HTIterator<K, T, T*, T&, KeyOfT, HashFunc> iterator;
typedef HTIterator<K, T, const T*, const T&, KeyOfT, HashFunc> const_iterator;
iterator begin() {
for (size_t i = 0; i < _table.size(); ++i) {
Node* cur = _table[i];
if (cur) return iterator(cur, this);
}
return iterator(nullptr, this);
}
iterator end() { return iterator(nullptr, ); }
{
( i = ; i < _table.(); ++i) {
Node* cur = _table[i];
(cur) (cur, );
}
(, );
}
{ (, ); }
() { _table.(, ); }
~() {
( i = ; i < _table.(); ++i) {
Node* cur = _table[i];
(cur) {
Node* next = cur->_next;
cur;
cur = next;
}
_table[i] = ;
}
}
{
KeyOfT kot;
iterator it = ((data));
(it != ()) (it, );
HashFunc hf;
(_n == _table.()) {
newSize = _table.() * ;
vector<Node*> newTable;
newTable.(newSize, );
( i = ; i < _table.(); ++i) {
Node* cur = _table[i];
(cur) {
Node* next = cur->_next;
hashi = ((cur->_data)) % newSize;
cur->_next = newTable[hashi];
newTable[hashi] = cur;
cur = next;
}
_table[i] = ;
}
_table.(newTable);
}
hashi = ((data)) % _table.();
Node* newnode = (data);
newnode->_next = _table[hashi];
_table[hashi] = newnode;
++_n;
((newnode, ), );
}
{
HashFunc hf;
KeyOfT kot;
hashi = (key) % _table.();
Node* cur = _table[hashi];
(cur) {
((cur->_data) == key) cur;
cur = cur->_next;
}
;
}
{
HashFunc hf;
KeyOfT kot;
hashi = (key) % _table.();
Node* prev = ;
Node* cur = _table[hashi];
(cur) {
((cur->_data) == key) {
(prev == ) _table[hashi] = cur->_next;
prev->_next = cur->_next;
--_n;
cur;
;
}
prev = cur;
cur = cur->_next;
}
;
}
:
vector<Node*> _table;
_n = ;
};
哈希桶打印建议将链表打在一行。负载因子达到 1 时扩容。新旧表转移需重新计算哈希值。节点可优化为树结构(红黑树)。
template<class K, class T, class Ptr, class Ref, class KeyOfT, class HashFunc>
struct HTIterator {
typedef HashNode<T> Node;
typedef HTIterator<K, T, Ptr, Ref, KeyOfT, HashFunc> Self;
typedef HTIterator<K, T, T*, T&, KeyOfT, HashFunc> Iterator;
Node* _node;
HashTable<K, T, KeyOfT, HashFunc>* _pht;
HTIterator(Node* node, const HashTable<K, T, KeyOfT, HashFunc>* pht)
: _node(node), _pht((HashTable<K, T, KeyOfT, HashFunc>*)pht) {}
HTIterator(const Iterator& it) : _node(it._node), _pht(it._pht) {}
Ref operator*() { return _node->_data; }
Ptr operator->() { return &_node->_data; }
Self& operator++() {
if (_node->_next) {
_node = _node->_next;
} else {
KeyOfT kot;
HashFunc hf;
size_t hashi = hf(kot(_node->_data)) % _pht->_table.size();
++hashi;
while (hashi < _pht->_table.size()) {
if (_pht->_table[hashi]) {
_node = _pht->_table[hashi];
return *this;
} else {
++hashi;
}
}
_node = nullptr;
}
return *this;
}
!=( Self& s) { _node != s._node; }
==( Self& s) { _node == s._node; }
};
namespace renshen {
template<class K>
class unordered_set {
struct SetKeyOfT {
const K& operator()(const K& key) { return key; }
};
public:
typedef typename hash_bucket::HashTable<K, K, SetKeyOfT>::const_iterator iterator;
typedef typename hash_bucket::HashTable<K, K, SetKeyOfT>::const_iterator const_iterator;
iterator begin() { return _ht.begin(); }
iterator end() { return _ht.end(); }
pair<const_iterator, bool> insert(const K& key) {
pair<typename hash_bucket::HashTable<K, K, SetKeyOfT>::iterator, bool> ret = _ht.Insert(key);
return pair<const_iterator, bool>(ret.first, ret.second);
}
private:
hash_bucket::HashTable<K, K, SetKeyOfT> _ht;
};
}
namespace renshen {
template<class K, class V>
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>::iterator iterator;
typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT>::const_iterator 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 pair<K, V>& kv) { return _ht.Insert(kv); }
V& operator[]( K& key) {
pair<iterator, > ret = _ht.((key, ()));
ret.first->second;
}
:
hash_bucket::HashTable<K, pair< K, V>, MapKeyOfT> _ht;
};
}
用数的比特位表达信息,库中对应 bitset。常用接口:test, set, reset。
面试题: 给 40 亿个不重复无符号整数,快速判断一个数是否存在。
使用 set 或排序 + 二分查找空间过大,位图更优。一个 int 有 32 比特位,可用 32 个数表示一组状态。
template<size_t N>
class bitset {
public:
bitset() { _a.resize(N / 32 + 1); }
void set(size_t x) {
size_t i = x / 32;
size_t j = x % 32;
_a[i] |= (1 << j);
}
void reset(size_t x) {
size_t i = x / 32;
size_t j = x % 32;
_a[i] &= (~(1 << j));
}
bool test(size_t x) {
size_t i = x / 32;
size_t j = x % 32;
return _a[i] & (1 << j);
}
private:
vector<int> _a;
};
应用场景:
利用多个独立哈希函数 + 位图实现的高效存在性判断结构。若所有哈希位置均为 1,则可能存在;否则一定不在。
应用场景: 快速判断昵称是否注册过。精确查询需回查数据库。
template<size_t N, class K, class Hash1, class Hash2, class Hash3>
class BloomFilter {
public:
void Set(const K& key) {
size_t hash1 = Hash1()(key) % N;
_bs.set(hash1);
size_t hash2 = Hash2()(key) % N;
_bs.set(hash2);
size_t hash3 = Hash3()(key) % N;
_bs.set(hash3);
}
bool Test(const K& key) {
size_t hash1 = Hash1()(key) % N;
if (!_bs.test(hash1)) return false;
size_t hash2 = Hash2()(key) % N;
if (!_bs.test(hash2)) return false;
size_t hash3 = Hash3()(key) % N;
if (!_bs.test(hash3)) return false;
return true;
}
private:
bitset<N> _bs;
};
注意:布隆过滤器一般不支持删除操作,否则会导致误判。如需删除,可使用引用计数。
优化参数:k 为哈希函数个数,m 为长度,n 为元素个数。
哈希函数示例:
struct BKDRHash {
size_t operator()(const string& str) {
size_t hash = 0;
for (auto ch : str) {
hash = hash * 131 + ch;
}
return hash;
}
};
运用哈希函数将大文件数据分到多个小文件。
问题: 两个文件各 100 亿 query,内存 1G,找交集。
若内存不足:
set,若抛出 bad_alloc 说明冲突太多,更换哈希函数二次切分。其他题目: 100G log file 找出现次数最多的 IP。对小文件前 k 多的地址保留到堆和 map,最后汇总比较。
力扣 350. 两个数组的交集 II 核心逻辑:统计频次后取最小值,处理重复统计。
class Solution {
public:
vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
unordered_map<int, int> hash1;
unordered_map<int, int> hash2;
vector<int> v;
for (auto e : nums1) hash1[e]++;
for (auto e : nums2) hash2[e]++;
for (auto e : nums1) {
if (hash1.count(e) && hash2.count(e)) {
for (int i = 0; i < min(hash1[e], hash2[e]); ++i) {
v.push_back(e);
}
hash1[e] = 0;
hash2[e] = 0;
}
}
return v;
}
};
力扣 884. 两句话中的不常见单词 核心逻辑:统计词频,找出只出现一次的单词。
选择题:关于 unordered_map 和 unordered_set 说法错误的是(D)。 D. 它们在进行元素插入时,都得要通过 key 的比较去找待插入元素的位置。 实际上插入时通过哈希值定位 bucket,无需 key 比较即可确定位置,仅在冲突处理时才涉及比较。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online