跳到主要内容深入 C++ 哈希表:从除留余数到红黑树优化 | 极客日志C++算法
深入 C++ 哈希表:从除留余数到红黑树优化
直接定址法在数据分散时浪费空间,哈希表通过哈希函数将键映射到有限空间来解决。除法散列法常用,但表长需避开2的幂以减少冲突。冲突处理有开放定址法和链地址法:开放定址法线性探测易群集,状态需标记DELETE以免断链,扩容使用素数表维持均匀性;链地址法用链表存储冲突元素,负载因子可大于1,极端桶长可转红黑树优化,扩容时复用节点避免重建开销。对非整数键如字符串需特化哈希函数。实际实现展示了插入、查找、删除及扩容的完整逻辑。
深海蔚蓝3 浏览 直接定址:当数据很集中时,直接用值做下标又快又省事。但现实里 key 往往零零散散,比如只有 1、20、99 三个数,开 100 个元素的数组就太浪费了。哈希表要解决的就是'如何用一个较小的空间把分散的 key 映射进去'。
哈希函数:除留余数法
最常见的做法是 h(key) = key % M,M 是表长。这能让所有 key 都落在 [0, M-1] 内。但 M 的取值有讲究——如果 M 是 2 的整数次幂,比如 16,那 key % 16 实际上只保留了 key 的低 4 位。像 63(二进制 00111111)和 31(00011111)低 4 位都是 1111,就会撞到同一个槽位。
所以实用中 M 通常选一个不太靠近 2 的整数次幂的素数。不过冲突仍然不可避免,只是尽量让它分布均匀。
负载因子 = 已存储元素个数 N / 表长 M。它越小冲突越少,但空间利用率低;越大空间越省,冲突概率越高。开放定址法下负载因子必须小于 1,链地址法则可以大于 1。
冲突怎么解决?
开放定址法
所有数据都存到哈希表数组里。如果算出的位置已经被占了,就按一定规则找下一个空位。
槽位有三种状态:EMPTY、EXIST、DELETE。删除时不真正清空,而是标记为 DELETE,否则会打断探测链。
enum Status { EMPTY, EXIST, DELETE };
template<class K, class V>
struct HashData {
pair<K, V> _kv;
Status _status = EMPTY;
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable {
public:
HashTable(size_t size = 11) :_tables(size), _n(0) {}
private:
vector<HashData<K, V>> _tables;
size_t _n;
};
线性探测
从冲突位置开始顺序往下找,到底回绕到开头。探测序列是 hashi = (hash0 + i) % M,i = 1, 2, ...。因为负载因子 < 1,最多 M-1 次一定能找到空位。

线性探测的问题是容易产生'群集'——一旦连续几个位置被占,新插入的 key 就会继续往后挤,让原本不相干的 key 也撞到一起。
bool Insert(const pair<K, V>& kv) {
size_t hash0 = kv.first % _tables.size();
size_t hashi = hash0;
size_t i = 1;
while (_tables[hashi]._status == EXIST) {
hashi = (hashi + i) % _tables.size();
i++;
}
_tables[hashi]._kv = kv;
_tables[hashi]._status = EXIST;
++_n;
return true;
}
扩容与素数表
当 _n / _tables.size() 超过负载因子(比如 0.7)时就需要扩容。直接 2 倍扩容会破坏除留余数法的均匀性——表长变为了 2 的幂。
STL 的做法是预先准备一个素数列表,每次扩容时从中选取下一个素数:
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
};
inline unsigned long __stl_next_prime(unsigned long n) {
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;
}
扩容时创建一个新表,把旧表里 EXIST 的元素重新 Insert 进去,最后 swap:
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]._status == EXIST) {
newHT.Insert(_tables[i]._kv);
}
}
_tables.swap(newHT._tables);
}
查找和删除都基于同样的探测规则,删除只是把状态改为 DELETE,不实际释放节点。完整代码:
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
};
inline unsigned long __stl_next_prime(unsigned long n) {
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;
}
enum Status { EMPTY, EXIST, DELETE };
template<class K, class V>
struct HashData {
pair<K, V> _kv;
Status _status = EMPTY;
};
template<class K>
struct HashFunc {
size_t operator()(const K& key) {
return (size_t)key;
}
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable {
public:
HashTable(size_t size = __stl_next_prime(0)) :_tables(size), _n(0) {}
bool Insert(const pair<K, V>& kv) {
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]._status == EXIST) {
newHT.Insert(_tables[i]._kv);
}
}
_tables.swap(newHT._tables);
}
size_t hash0 = kv.first % _tables.size();
size_t hashi = hash0;
size_t i = 1;
while (_tables[hashi]._status == EXIST) {
hashi = (hashi + i) % _tables.size();
i++;
}
_tables[hashi]._kv = kv;
_tables[hashi]._status = EXIST;
++_n;
return true;
}
HashData<K, V>* Find(const K& key) {
size_t hash0 = key % _tables.size();
size_t hash1 = hash0;
size_t i = 1;
while (_tables[hash1]._status != EMPTY) {
if (_tables[hash1]._kv.first == key && _tables[hash1]._status != DELETE) {
return &_tables[hash1];
}
hash1 = (hash1 + i) % _tables.size();
++i;
}
return nullptr;
}
bool Erase(const K& key) {
HashData<K, V>* ret = Find(key);
if (ret) {
ret->_status = DELETE;
return true;
} else {
return false;
}
}
private:
vector<HashData<K, V>> _tables;
size_t _n;
};
链地址法(哈希桶)
开放定址法下所有元素挤在一个数组里,冲突会互相影响。链地址法则让每个槽位变成一个链表头,冲突的元素直接挂到链表上。这样负载因子可以 > 1,空间利用率更高。
极端情况下,如果所有 key 都映射到同一个桶,链表长度变成 O(N),查找退化为线性。常见的优化是当桶长度超过阈值(比如 8)时,把链表转成红黑树。Java 的 HashMap 就是这么做的,C++ 的 unordered_map 也类似,不过标准没强制要求。
template<class K, class V>
struct HashNode {
pair<K, V> _kv;
HashNode<K, V>* _next;
HashNode(const pair<K, V>& kv) :_next(nullptr), _kv(kv) {}
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable {
typedef HashNode<K, V> Node;
public:
HashTable(size_t size = __stl_next_prime(0)) :_tables(size, nullptr), _n(0) {}
private:
vector<Node*> _tables;
size_t _n;
};
非整数 key 的处理
Key 不是整数时,比如浮点数、负数或字符串,需要把它转成 size_t。浮点数和负数可以直接强转,但字符串不行。
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) {
size_t hash0 = 0;
for (auto& ch : key) {
hash0 *= 131;
hash0 += ch;
}
return hash0;
}
};
这样模板参数中默认的 Hash = HashFunc<K> 就能自动适配了。
扩容:复用节点
链地址法扩容时,如果直接遍历旧桶,对每个节点重新 Insert,会涉及 new 新节点 + delete 旧节点,开销太大。更好的做法是直接把旧节点'摘'下来挂到新表的对应位置:
bool Insert(const pair<K, V>& kv) {
if (Find(kv.first)) return false;
Hash hs;
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 hash0 = hs(cur->_kv.first) % newtables.size();
cur->_next = newtables[hash0];
newtables[hash0] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newtables);
}
size_t hash0 = hs(kv.first) % _tables.size();
Node* newnode = new Node(kv);
newnode->_next = _tables[hash0];
_tables[hash0] = newnode;
++_n;
return true;
}
删除
bool Erase(const K& key) {
Hash hs;
size_t hash0 = hs(key) % _tables.size();
Node* prev = nullptr;
Node* cur = _tables[hash0];
while (cur) {
if (cur->_kv.first == key) {
if (prev == nullptr) {
_tables[hash0] = cur->_next;
} else {
prev->_next = cur->_next;
}
--_n;
delete cur;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
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
};
inline unsigned long __stl_next_prime(unsigned long n) {
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, class V>
struct HashNode {
pair<K, V> _kv;
HashNode<K, V>* _next;
HashNode(const pair<K, V>& kv) :_next(nullptr), _kv(kv) {}
};
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) {
size_t hash0 = 0;
for (auto& ch : key) {
hash0 *= 131;
hash0 += ch;
}
return hash0;
}
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable {
typedef HashNode<K, V> Node;
public:
HashTable(size_t size = __stl_next_prime(0)) :_tables(size, nullptr), _n(0) {}
bool Insert(const pair<K, V>& kv) {
if (Find(kv.first)) return false;
Hash hs;
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 hash0 = hs(cur->_kv.first) % newtables.size();
cur->_next = newtables[hash0];
newtables[hash0] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newtables);
}
size_t hash0 = hs(kv.first) % _tables.size();
Node* newnode = new Node(kv);
newnode->_next = _tables[hash0];
_tables[hash0] = 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 hash0 = hs(key) % _tables.size();
Node* prev = nullptr;
Node* cur = _tables[hash0];
while (cur) {
if (cur->_kv.first == key) {
if (prev == nullptr) {
_tables[hash0] = cur->_next;
} else {
prev->_next = cur->_next;
}
--_n;
delete cur;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
~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;
}
}
private:
vector<Node*> _tables;
size_t _n;
};

相关免费在线工具
- 加密/解密文本
使用加密算法(如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