跳到主要内容C++ 哈希表实现:unordered_map/set、位图与布隆过滤器 | 极客日志C++算法
C++ 哈希表实现:unordered_map/set、位图与布隆过滤器
哈希表通过散列函数建立键值映射关系,存在闭散列和开散列两种冲突解决策略。C++ 标准库中的 unordered_map 和 unordered_set 基于哈希表实现,提供 O(1) 平均查找性能。位图利用比特位存储状态,适用于海量数据去重或存在性判断。布隆过滤器结合多个哈希函数与位图,用于近似集合成员检测。哈希切割则用于处理超出内存限制的大文件数据处理任务。文中包含相关容器封装代码及典型面试题解析。
unordered 系列关联式容器
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 {
:
() { _table.(); }
{
(_n * / _table.() >= ) {
newSize = _table.() * ;
HashTable<K, V, HashFunc> newHT;
newHT._table.(newSize);
( i = ; i < _table.(); i++) {
(_table[i]._state == EXIST) {
newHT.(_table[i]._kv);
}
}
_table.(newHT._table);
}
HashFunc hf;
hashi = (kv.first) % _table.();
(_table[hashi]._state == EXIST) {
++hashi;
hashi %= _table.();
}
_table[hashi]._kv = kv;
_table[hashi]._state = EXIST;
++_n;
;
}
{
HashFunc hf;
hashi = (key) % _table.();
(_table[hashi]._state != EMPTY) {
(_table[hashi]._state == EXIST && _table[hashi]._kv.first == key) {
(HashData< K, V>*)&_table[hashi];
}
++hashi;
hashi %= _table.();
}
;
}
{
HashData< K, V>* ret = (key);
(ret) {
ret->_state = DELETE;
--_n;
;
}
;
}
:
vector<HashData<K, V>> _table;
_n = ;
};
public
HashTable
resize
10
bool Insert(const pair<K, V>& kv)
if
10
size
7
size_t
size
2
resize
for
size_t
0
size
if
Insert
swap
size_t
hf
size
while
size
return
true
HashData<const K, V>* Find(const K& key)
size_t
hf
size
while
if
return
const
size
return
nullptr
bool Erase(const K& key)
const
Find
if
return
true
return
false
private
size_t
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, this); }
const_iterator begin() const {
for (size_t i = 0; i < _table.size(); i++) {
Node* cur = _table[i];
if (cur) return const_iterator(cur, this);
}
return const_iterator(nullptr, this);
}
const_iterator end() const { return const_iterator(nullptr, this); }
HashTable() { _table.resize(10, nullptr); }
~HashTable() {
for (size_t i = 0; i < _table.size(); i++) {
Node* cur = _table[i];
while (cur) {
Node* next = cur->_next;
delete cur;
cur = next;
}
_table[i] = nullptr;
}
}
bool Insert(const T& data) {
KeyOfT kot;
iterator it = Find(kot(data));
if (it != end()) return make_pair(it, false);
HashFunc hf;
if (_n == _table.size()) {
size_t newSize = _table.size() * 2;
vector<Node*> newTable;
newTable.resize(newSize, nullptr);
for (size_t i = 0; i < _table.size(); i++) {
Node* cur = _table[i];
while (cur) {
Node* next = cur->_next;
size_t hashi = hf(kot(cur->_data)) % newSize;
cur->_next = newTable[hashi];
newTable[hashi] = cur;
cur = next;
}
_table[i] = nullptr;
}
_table.swap(newTable);
}
size_t hashi = hf(kot(data)) % _table.size();
Node* newnode = new Node(data);
newnode->_next = _table[hashi];
_table[hashi] = newnode;
++_n;
return make_pair(iterator(newnode, this), true);
}
Node* Find(const K& key) {
HashFunc hf;
KeyOfT kot;
size_t hashi = hf(key) % _table.size();
Node* cur = _table[hashi];
while (cur) {
if (kot(cur->_data) == key) return iterator(cur, this);
cur = cur->_next;
}
return end();
}
bool Erase(const K& key) {
HashFunc hf;
KeyOfT kot;
size_t hashi = hf(key) % _table.size();
Node* prev = nullptr;
Node* cur = _table[hashi];
while (cur) {
if (kot(cur->_data) == key) {
if (prev == nullptr) _table[hashi] = cur->_next;
else prev->_next = cur->_next;
--_n;
delete cur;
return true;
}
prev = cur;
cur = cur->_next;
}
--_n;
return false;
}
private:
vector<Node*> _table;
size_t _n = 0;
};
负载因子达到 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;
}
bool operator!=(const Self& s) { return _node != s._node; }
bool operator==(const Self& s) { return _node == s._node; }
};
迭代器需访问哈希表内部结构,使用前向声明解决循环依赖问题。
unordered_set 的封装
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;
};
}
unordered_map 的封装
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[](const K& key) {
pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
return ret.first->second;
}
private:
hash_bucket::HashTable<K, pair<const 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 true;
}
private:
bitset<N> _bs;
};
布隆过滤器一般不支持删除操作,否则会导致误判。如需删除需引入引用计数。优化参数涉及哈希函数个数 k、位图长度 m 和元素个数 n。
哈希切割
哈希切割的应用
问题:两个 100 亿 query 文件,1G 内存,找交集。
- 近似算法:布隆过滤器。
- 精确算法:哈希切割。两文件用相同哈希函数分片,相同 query 进入同编号文件,分别比对。
若内存不足,说明冲突过多,需更换哈希函数二次切分。
作业部分
-
散列函数有一个共同性质,即函数值应按()取其值域的每一个值。
-
解决散列法中出现冲突问题常采用的方法是(D)
- A. 数字分析法、除余法、平方取中法
- B. 数字分析法、除余法、线性探测法
- C. 数字分析法、线性探测法、多重散列法
- D. 线性探测法、多重散列法、链地址法
-
已知关键字序列:(19,14,23,1,68,20,84,27,55,11,10,79),H(key)=key%7,链地址法,平均查找长度为(A)
-
关于位图说法错误的是(D)
- D. 位图可以很方便的进行字符串的映射以及查找
- 解析:一般不用位图处理字符串,字符串转整型易冲突。
-
力扣 350. 两个数组的交集 II
-
力扣 884. 两句话中的不常见单词
- 引申:只有 multi_map 和 multi_set 的 count 返回出现次数,其他容器返回 1 或 0。
-
关于 unordered_map 和 unordered_set 说法错误的是(D)
- D. 它们在进行元素插入时,都得要通过 key 的比较去找待插入元素的位置
- 解析:插入时通过哈希值定位,无需 key 比较。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online