跳到主要内容C++ STL unordered_set/unordered_map 模拟实现 | 极客日志C++算法
C++ STL unordered_set/unordered_map 模拟实现
综述由AI生成C++ STL 中 unordered_set 和 unordered_map 的底层原理及模拟实现。基于哈希表(HashTable)使用链地址法处理冲突,实现了自定义哈希函数、迭代器遍历、扩容机制以及插入删除操作。重点讲解了仿函数 KeyOfT 的设计以适配不同容器类型,以及 map 中 [] 运算符的重载逻辑。通过完整代码示例展示了从节点结构到容器接口的构建过程。
栈溢出30 浏览 标准介绍
1. 标准库中的 unordered_set/map 是怎么实现的呢?
SGI - STL30 版本的源代码里,不存在 unordered_set 和 unordered_map。
这是因为 SGI - STL30 版本属于 C++11 之前的 STL 版本,而 unordered_set 和 unordered_map 这两个容器,是在 C++11 之后才更新加入标准的。不过,SGI - STL30 实现了哈希表相关功能,对应的容器名为 hash_set 和 hash_map,它们属于非标准容器(非标准指并非 C++ 标准规定必须实现的容器)。其源代码可在 stl_hash_set/、stl_hash_map/、stl_hashtable.h 这些文件中找到。
下面截取 hash_set 和 hash_map 实现结构框架的核心部分,展示如下:
stl_hashtable.h
template<class Value,class Key,class HashFcn,class ExtractKey,class EqualKey,class Alloc>
class hashtable {
public:
typedef Key key_type;
typedef Value value_type;
typedef HashFcn hasher;
typedef EqualKey key_equal;
private:
hasher hash;
key_equal equals;
ExtractKey get_key;
typedef __hashtable_node<Value> node;
vector<node*, Alloc> buckets;
size_type num_elements;
public:
typedef __hashtable_iterator<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc> iterator;
pair<iterator,bool>insert_unique(const value_type& obj);
const_iterator ;
};
< >
{
__hashtable_node* next;
Value val;
};
find
(const key_type& key)
const
template
class
Value
struct
__hashtable_node
stl_hash_set
template<class Value,class HashFcn= hash<Value>,class EqualKey= equal_to<Value>,class Alloc= alloc>
class hash_set{
private:
typedef hashtable<Value, Value, HashFcn, identity<Value>, EqualKey, Alloc> ht;
ht rep;
public:
typedef typename ht::key_type key_type;
typedef typename ht::value_type value_type;
typedef typename ht::hasher hasher;
typedef typename ht::key_equal key_equal;
typedef typename ht::const_iterator iterator;
typedef typename ht::const_iterator const_iterator;
hasher hash_funct()const{return rep.hash_funct();}
key_equal key_eq()const{return rep.key_eq();}
};
stl_hash_map
template<class Key,class T,class HashFcn= hash<Key>,class EqualKey= equal_to<Key>,class Alloc= alloc>
class hash_map{
private:
typedef hashtable<pair<const Key, T>, Key, HashFcn, select1st<pair<const Key, T>>, EqualKey, Alloc> ht;
ht rep;
public:
typedef typename ht::key_type key_type;
typedef T data_type;
typedef T mapped_type;
typedef typename ht::value_type value_type;
typedef typename ht::hasher hasher;
typedef typename ht::key_equal key_equal;
typedef typename ht::iterator iterator;
typedef typename ht::const_iterator const_iterator;
};
从上面的源码中我们可以清晰的看出:在结构设计上,hash_set 和 hash_map 与 set 和 map 高度相似,它们复用同一个 hashtable 来实现关键结构,以此适配不同的存储与查找需求。对于 hash_set:传递给 hashtable 的是单纯的 key;对于 hash_map:传递给 hashtable 的则是 pair<const key, value> 这种键值对形式。
代码实现
unordered_set/map 容器的结构
头文件
HashTable.h
#pragma once
#include<iostream>
#include<vector>
using namespace std;
template<class K>
struct HashFunc{
size_t operator()(const K& key){
return (size_t)key;
}
};
template<>
struct HashFunc<string>{
size_t operator()(const string& s){
size_t hash = 0;
for(auto it : s){
hash += it;
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{
HashNode<T>* _node;
const HashTable<K, T, KeyOfT, Hash>* _ht;
typedef HashNode<T> Node;
typedef HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;
typedef HashTable<K, T, KeyOfT, Hash> 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& ht){return _node != ht._node;}
bool operator==(const Self& ht){return _node == ht._node;}
Self& operator++(){
if(_node->_next){
_node = _node->_next;
}
else{
KeyOfT kot;
Hash hashFunc;
size_t hash_i = hashFunc(kot(_node->_data)) % _ht->_tables.size();
++hash_i;
while(hash_i < _ht->_tables.size())
{
_node = _ht->_tables[hash_i];
if(_node){break;
else{++hash_i;}}
if(hash_i == _ht->_tables.size()){
_node = nullptr;
}
return *this;
}
}
};
template<class K,class T,class KeyOfT,class Hash>
{
private:
vector<HashNode<T>*> _tables;
size_t _n;
typedef HashNode<T> Node;
template<class K,class T,class Ref,class Ptr,class KeyOfT,class Hash>
friend struct HTIterator;
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* current = _tables[i];
if(current){
return Iterator(current,this);
}
}
}
Iterator End(){return Iterator(nullptr,this);}
ConstIterator Begin()const{
if(_n == 0){return End();}
for(size_t i = 0; i < _tables.size(); i++){
Node* current = _tables[i];
if(current){
return ConstIterator(current,this);
}
}
}
ConstIterator End()const{return ConstIterator(nullptr,this);}
HashTable():_tables(_stl_next_prime(0))
~HashTable(){
for(size_t i = 0; i < _tables.size();++i){
Node* current = _tables[i];
while(current){
Node* next = current->_next;
delete current;
current = next;
}
_tables[i]=nullptr;
}
}
Iterator Find(const K& key)
{
KeyOfT kot;
Hash hashFunc;
size_t hash_i = hashFunc(key) % _tables.size();
Node* current = _tables[hash_i];
while(current)
{
if(kot(current->_data)== key){
return Iterator(current,this);
}
else{
current = current->_next;
}
}
return End();
}
bool Erase(const K& key){
KeyOfT kot;
Hash hashFunc;
size_t hash_i = hashFunc(key) % _tables.size();
Node* curr = _tables[hash_i];
Node* prev = nullptr;
while(curr){
if(kot(curr->_data)== key){
if(prev == nullptr){
_tables[hash_i]= curr->_next;
}
else{
prev->_next = curr->_next;
}
delete curr;
return true;
}
prev = curr;
curr = curr->_next;
}
return false;
}
pair<Iterator,bool>Insert(const T& data)
{
KeyOfT kot;
Iterator it = Find(kot(data));
if(it != End()){
return{ it,false};
}
if(_n == _tables.size())
{
vector<Node*>newVector(_tables.size()*2);
for(size_t i = 0; i < _tables.size(); i++){
Node* current = _tables[i];
while(current){
Node* next = current->_next;
Hash hashFunc;
size_t hash_i = hashFunc(kot(current->_data)) % newVector.size();
current->_next = newVector[hash_i];
newVector[hash_i]= current;
current = next;
}
_tables[i]=nullptr;
}
_tables.swap(newVector);
}
Node* newNode = newNode(data);
Hash hashFunc;
size_t hash_i = hashFunc(kot(data)) % _tables.size();
++_n;
return{Iterator(newNode,this),true};
}};
}
};
Myunordered_set.h
#pragma once
#include"HashTable.h"
namespace Myunordered_set {
enum State{
EXIST,
EMPTY,
DELETE
};
template<class K,class V>
struct HashData{
pair<K, V> _kv;
State _state = EMPTY;
};
template<class K,class Hash= HashFunc<K>>
class unordered_set{
private:
struct SetKeyOfT{
const K&operator()(const K& key){return key;}
};
hash_bucket::HashTable<K,const K, SetKeyOfT, Hash> _ht;
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();}
iterator find(const K& key){return _ht.Find(key);}
bool erase(const K& key){return _ht.Erase(key);}
pair<iterator,bool>insert(const K& key){return _ht.Insert(key);}
};
}
Myunordered_map.h
#pragma once
#include"HashTable.h"
namespace Myunordered_map {
template<class K,class V,class Hash= HashFunc<K>>
class unordered_map{
private:
struct MapKeyOfT{
const K&operator()(const pair<K, V>& kv){return kv.first;}
};
hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
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();}
iterator find(const K& key){return _ht.Find(key);}
bool erase(const K& key){return _ht.Erase(key);}
pair<iterator,bool>insert(const pair<K, V>& kv){return _ht.Insert(kv);}
V&operator[](const K& key){
pair<iterator,bool> ret =insert({ key,V()});
return ret.first->second;
}
};
}
测试文件:Test.cpp
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>
#include"Myunordered_set.h"
#include"Myunordered_map.h"
using namespace std;
void test01(){
cout <<"=== 测试 unordered_set 基本功能 ==="<< endl;
Myunordered_set::unordered_set<int> s;
s.insert(3);
s.insert(1);
s.insert(4);
s.insert(1);
s.insert(2);
s.insert(5);
cout <<"遍历元素:";
for(auto it = s.begin(); it != s.end();++it){
cout <<*it <<" ";
}
cout << endl;
int key =3;
auto find_ret = s.find(key);
if(find_ret != s.end()){
cout <<"找到元素:"<<*find_ret << endl;
}else{
cout <<"未找到元素:"<< key << endl;
}
key =4;
bool erase_ret = s.erase(key);
if(erase_ret){
cout <<"删除元素 "<< key <<" 成功"<< endl;
}else{
cout <<"删除元素 "<< key <<" 失败"<< endl;
}
cout <<"删除后遍历:";
for(auto val : s)
{
cout << val <<" ";
}
cout << endl << endl;
}
void test02(){
cout <<"=== 测试 unordered_set 字符串类型 ==="<< endl;
Myunordered_set::unordered_set<string> str_set;
str_set.insert("apple");
str_set.insert("banana");
str_set.insert("cherry");
str_set.insert("apple");
cout <<"字符串集合元素:";
for(constauto& str : str_set){
cout << str <<" ";
}
cout << endl;
string target ="banana";
auto it = str_set.find(target);
if(it != str_set.end()){
cout <<"找到字符串:"<<*it << endl;
}else{
cout <<"未找到字符串:"<< target << endl;
}
str_set.erase("cherry");
cout <<"删除 cherry 后:";
for(constauto& str : str_set){
cout << str <<" ";
}
cout << endl << endl;
}
void test03(){
cout <<"=== 测试 unordered_map 基本功能 ==="<< endl;
Myunordered_map::unordered_map<string,int> m;
m.insert({"apple",10});
m.insert({"banana",20});
m.insert({"cherry",30});
m.insert({"apple",15});
m["date"]=40;
m["banana"]=25;
cout <<"遍历键值对:"<< endl;
for(auto it = m.begin(); it != m.end();++it){
cout << it->first <<": "<< it->second << endl;
}
string key ="cherry";
auto it = m.find(key);
if(it != m.end()){
cout <<"找到 "<< key <<": "<< it->second << endl;
}else{
cout <<"未找到 "<< key << endl;
}
key ="banana";
bool erase_ret = m.erase(key);
if(erase_ret){
cout <<"删除 "<< key <<" 成功"<< endl;
}else{
cout <<"删除 "<< key <<" 失败"<< endl;
}
cout <<"删除后 "<< key <<" 是否存在:"<<(m.find(key)!= m.end()?"是":"否")<< endl << endl;
}
void test04(){
cout <<"=== 测试 unordered_map [] 运算符 ==="<< endl;
Myunordered_map::unordered_map<int, string> m;
m[1];
m[2]="two";
m[2]="second";
m[3]="three";
for(auto& kv : m){
cout << kv.first <<": "<< kv.second << endl;
}
cout << endl;
}
void test05(){
cout <<"=== 测试边界情况 ==="<< endl;
Myunordered_set::unordered_set<int> s_empty;
cout <<"空 set 的 begin() == end(): "<<(s_empty.begin()== s_empty.end()?"true":"false")<< endl;
cout <<"删除空 set 中的元素:"<<(s_empty.erase(100)?"成功":"失败")<< endl << endl;
Myunordered_map::unordered_map<int,int> mp_empty;
cout <<"空 map 的 begin() == end(): "<<(mp_empty.begin()== mp_empty.end()?"true":"false")<< endl;
cout <<"访问空 map 的 []: "<< mp_empty[100]<< endl;
}
int main(){
test01();
test02();
test03();
test04();
test05();
return 0;
}
代码解释
片段一:仿函数的设计
与 set、map 相比,unordered_set 和 unordered_map 的模拟实现类结构要更复杂一些,但整体大框架和思路是完全类似的。
由于 HashTable 是泛型实现,无法确定模板参数 T 具体是 K(像 unordered_set 的元素类型),还是 pair<K, V>(像 unordered_map 的键值对类型)。
在插入操作(insert)时,需要用 K 对象转换为整数取模,以及进行 K 的相等性比较。可 pair 的 value 本就不参与取模计算而且默认比较逻辑是对 key 和 value 一起比较是否相等,但我们实际只需要比较 K 对象。
所以:在 unordered_set 和 unordered_map 这一层,我们分别实现 SetKeyOfT 和 MapKeyOfT 这两个仿函数,传递给 HashTable 的 KeyOfT。
之后:HashTable 就能通过 KeyOfT 仿函数,从 T 类型对象里提取出 K 对象,再完成转换为整数取模以及 K 相等性比较的操作。
片段二:迭代器的设计
第一点:unordered_set 和 unordered_map 的 iterator 的实现框架,和 list 的 iterator 思路一致:用一个类型封装'结点指针',再通过重载运算符,让迭代器能像指针一样完成访问操作。需要注意的是,哈希表的迭代器是单向迭代器(只能向后遍历,无法反向)。
第二点:unordered_set 和 unordered_map 的 iterator 的容器特性的满足:
unordered_set 迭代器的'不可修改性'
unordered_map 迭代器的'部分可修改性'
总结:这样一来,迭代器遍历、修改的规则就和容器特性(unordered_set/unordered_map 的'键是否可改'需求)匹配上了,既保证逻辑合理,也符合 C++ 对容器迭代器的设计习惯。
unordered_map 的迭代器不允许修改 key,但允许修改 value。只需把 unordered_map 的键值对中'第一个参数(key)'设为 const K 即可,对应哈希表声明为:
HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
unordered_set 的迭代器不支持修改 key,只需把 unordered_set 的第二个模板参数改为 const K 即可,对应哈希表声明为:
HashTable<K, const K, SetKeyOfT, Hash> _ht;
片段三:operator++ 的设计
迭代器内部会维护一个'指向结点的指针',operator++ 实现时,要分两种情况:
- 如果当前桶还有后续结点,直接让指针指向下一个结点即可
- 如果当前桶已经遍历完毕,就需要'寻找下一个桶'
这里的关键设计是:迭代器中除了存'结点指针',还会存'哈希表对象的指针'。这样,当当前桶遍历完时:可通过哈希表对象,结合 key 值计算当前桶位置,然后往后找'第一个不为空的桶',就能拿到下一批结点。
片段四:begin() 和 end() 的设计
begin():返回'第一个非空桶的第一个结点指针'构造的迭代器
end():返回一个代表'遍历结束'的空迭代器(可用空指针等方式表示)
片段五:map 支持 [] 运算符的重载
要让 unordered_map 支持 [] 运算符,关键在于调整 insert 相关的返回值逻辑。这样的修改,能为 unordered_map 实现 [] 运算符提供必要的返回值支撑,让插入操作的结果可以被合理利用。
具体来说,需要修改 HashTable 中的 Insert 函数,使其返回值类型变为 pair<Iterator, bool>,也就是把 Insert 函数定义改为:
pair<Iterator,bool>Insert(const T& data)
相关免费在线工具
- 加密/解密文本
使用加密算法(如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