C++ 哈希表实现:unordered_map/set、位图与布隆过滤器
哈希表通过散列函数建立键值映射关系,存在闭散列和开散列两种冲突解决策略。C++ 标准库中的 unordered_map 和 unordered_set 基于哈希表实现,提供 O(1) 平均查找性能。位图利用比特位存储状态,适用于海量数据去重或存在性判断。布隆过滤器结合多个哈希函数与位图,用于近似集合成员检测。哈希切割则用于处理超出内存限制的大文件数据处理任务。文中包含相关容器封装代码及典型面试题解析。

哈希表通过散列函数建立键值映射关系,存在闭散列和开散列两种冲突解决策略。C++ 标准库中的 unordered_map 和 unordered_set 基于哈希表实现,提供 O(1) 平均查找性能。位图利用比特位存储状态,适用于海量数据去重或存在性判断。布隆过滤器结合多个哈希函数与位图,用于近似集合成员检测。哈希切割则用于处理超出内存限制的大文件数据处理任务。文中包含相关容器封装代码及典型面试题解析。

unordered_map, unordered_set, unordered_multimap, unordered_multiset 均基于哈希表实现。用法与 set 类似,接口基本相同,支持范围 for 遍历。
与 set 等红黑树容器的区别:
set,但在有序数据插入场景下 set 表现更好。注意:性能比较应在 Release 模式下进行。
哈希(散列)通过函数建立存储值与存储位置的对应关系,以空间换时间提高查询效率。
常用方法:
key % 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;
}
--_n;
;
}
:
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(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 比特位,可用比特位表示数值存在性。
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 则为交集。内存限制下可使用哈希切割配合位图。
布隆过滤器利用多个独立哈希函数 + 位图实现高效存在性判断。若所有哈希位置均为 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) == false) return false;
size_t hash2 = Hash2()(key) % N;
if (_bs.test(hash2) == false) return false;
size_t hash3 = Hash3()(key) % N;
if (_bs.test(hash3) == false) return false;
return ;
}
:
bitset<N> _bs;
};
布隆过滤器一般不支持删除操作,否则会导致误判。如需删除需引入引用计数。优化参数涉及哈希函数个数 k、位图长度 m 和元素个数 n。
运用哈希函数将大文件数据分片到多个小文件。
问题:两个 100 亿 query 文件,1G 内存,找交集。
若内存不足,说明冲突过多,需更换哈希函数二次切分。
散列函数有一个共同性质,即函数值应按()取其值域的每一个值。
解决散列法中出现冲突问题常采用的方法是(D)
已知关键字序列:(19,14,23,1,68,20,84,27,55,11,10,79),H(key)=key%7,链地址法,平均查找长度为(A)
关于位图说法错误的是(D)
力扣 350. 两个数组的交集 II
力扣 884. 两句话中的不常见单词
关于 unordered_map 和 unordered_set 说法错误的是(D)

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 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