当前位置: 首页 > news >正文

《C++进阶之STL》【unordered_set/unordered_map 模拟实现】

【unordered_set/unordered_map 模拟实现】目录

  • 前言:
  • ------------标准介绍------------
    • 1. 标准库中的unordered_set/map是怎么实现的呢?
      • stl_hashtable.h
      • stl_hash_set
      • stl_hash_map
  • ------------代码实现------------
    • unordered_set/map容器的结构
    • 头文件:
      • HashTable.h
      • Myunordered_set.h
      • Myunordered_map.h
    • 测试文件:Test.cpp
    • 运行结果:
  • ------------代码解释------------
    • 片段一:仿函数的设计
    • 片段二:迭代器的设计
    • 片段三:operator++的设计
    • 片段四:begin()和end()的设计
    • 片段五:map支持[]运算符的重载

在这里插入图片描述

往期《C++初阶》回顾:

《C++初阶》目录导航


往期《C++进阶》回顾:
/------------ 继承多态 ------------/
【普通类/模板类的继承 + 父类&子类的转换 + 继承的作用域 + 子类的默认成员函数】
【final + 继承与友元 + 继承与静态成员 + 继承模型 + 继承和组合】
【多态:概念 + 实现 + 拓展 + 原理】
/------------ STL ------------/
【二叉搜索树】
【AVL树】
【红黑树】
【set/map 使用介绍】
【set/map 模拟实现】
【哈希表】
【unordered_set/unordered_map 使用介绍】

前言:

hi~ 小伙伴们大家好呀! (っ◔◡◔)っ ♥
最近的天气真是越来越有秋意了,不知道你们所在的城市有没有下雨呀?☔️🍂
鼠鼠这儿已经连着下了三四天雨啦,打开天气预报一看,未来几天不是阴沉沉的阴天 (´-ω-`),就是小雨淅淅沥沥飘个不停 (。•́︿•̀。),偶尔还会来一场大雨哗哗啦啦下个痛快 (ノω・、),气温也跟着一点点降了下来,大家可得记得及时添衣服哦~(。・ω・。)ノ♡

不过就算天气有些阴冷,学习的热情可不能减!🔥 今天我们要解锁的新内容是…… 哎?这位小伙伴怎么一下子就说出是 【unordered_set/unordered_map 模拟实现】 了!Σ(っ °Д °;)っ 这绝对是鼠鼠的铁杆忠实粉没跑了,必须给你点个大大的赞!(づ ̄ ³ ̄)づ👍✨
说起来,这篇博客可是咱们 STL 系列的最后一篇啦,算得上是收尾的重磅内容。里面藏了不少实打实的干货,从底层原理到模拟实现的关键细节都给大家梳理清楚了,大家一定要慢慢看、细细品,可别错过这些精华哦!(๑•̀ㅂ•́)و✧

------------标准介绍------------

1. 标准库中的unordered_set/map是怎么实现的呢?

SGI - STL30 版本的源代码里,不存在unordered_setunordered_map

这是因为 SGI - STL30 版本属于 C++11 之前的 STL 版本,而unordered_setunordered_map这两个容器,是在 C++11 之后才更新加入标准的

  • 不过,SGI - STL30 实现了哈希表相关功能,对应的容器名为hash_sethash_map,它们属于非标准容器(非标准指并非 C++ 标准规定必须实现的容器 )

  • 其源代码可在stl_hash_set/stl_hash_map/stl_hashtable.h这些文件中找到


下面截取 hash_sethash_map实现结构框架的核心部分,展示如下:

stl_hashtable.h

/*-------------------------- stl_hashtable.h --------------------------*/// 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 find(const key_type& key) const;
};template <class Value>
struct __hashtable_node
{__hashtable_node* next;Value val;
};

stl_hash_set

/*-------------------------- stl_hash_set --------------------------*/// 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

/*-------------------------- stl_hash_map --------------------------*/// 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_sethash_mapsetmap高度相似,它们复用同一个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
{//1.重载()运算符 ---> 作用:将K类型转化为size_t类型,用于计算哈希值size_t operator()(const K& key){return (size_t)key;  //注意:默认为直接转换,适用于int、long等整数类型}
};/*------------------任务:定义哈希函数的“模板特化”------------------*/
template <>
struct HashFunc<string>
{//1.实现:“()运算符的重载” ---> 作用:将string类型的变量转化为哈希值size_t operator()(const string& s){//1.定义size_t类型变量记录string类型的变量计算的哈希值size_t hash = 0;//2.使用范围for循环遍历字符串并用BKDR算法计算其哈希值for (auto it : s){//2.1:先将字符的ASCII值累加到哈希值中hash += it;//2.2:再让哈希值乘以质数131(BKDR哈希算法认为:131可有效减少冲突)hash *= 131;}//3.返回最终计算的哈希值return hash;}
};/*------------------任务:实现“获取下一个 >=n 的质数的函数”---> “用于哈希表扩容”------------------*/
inline unsigned long _stl_next_prime(unsigned long n)
{//1.指定素数表的大小static const int _stl_num_primes = 28;//2.定义素数表覆盖常见哈希表大小static const unsigned long _stl_prime_list[_stl_num_primes] =   //注意:这里的素数表的类型是unsigned long 类型{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};//3.使用二分查找找到第一个 >=n 的素数//3.1:使用一个指针指向素数表中的“第一个素数”const unsigned long* first = _stl_prime_list;//3.1:使用一个指针指向素数表中的“最后一素数的下一位置”const unsigned long* last = _stl_prime_list + _stl_num_primes;//3.3:使用lower_bound()接口函数求出第一个 >=n 的素数const unsigned long* pos = lower_bound(first, last, n);//3.4:适合作为哈希表容量的质数return pos == last ? *(last - 1) : *pos;/**  说明遍历完质数表,所有预定义的质数都比 n 小*  此时返回最大的质数 *(last - 1),因为 last 是数组末尾的下一个位置,last - 1 指向最后一个有效质数*/
}/*------------------任务:使用“链地址法”实现哈希表------------------*/
namespace hash_bucket
{/*------------------任务:定义“哈希表节点的结构体模板”------------------*///template<class K, class V>template<class T>  //注意:为了封装unordered_set/unordered_map 容器这里的模板参数已经从<class K, class V> ---> <class T>struct HashNode{/*------------------成员变量------------------*///1.存储的数据//2.指向下一个节点的指针//pair<K, V> _kv;//HashNode<K, V>* _next;T _data;HashNode<T>* _next;/*------------------成员函数------------------*///1.实现:哈希桶节点的“构造函数”//HashNode(const pair<K, V>& kv)HashNode(const T& data)//:_kv(kv):_data(data), _next(nullptr){}};//前置声明,因为 HTIterator 中要用到 HashTable 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{/*------------------成员变量------------------*///1.指向当前节点的指针//2.指向哈希表的指针 ---> 用于遍历的桶HashNode<T>* _node;const HashTable<K, T, KeyOfT, Hash>* _ht; // 注意:迭代器这里需要使用到哈希表,所以在定义迭代器之前我们先进行了哈希表的前置声明/*------------------类型别名------------------*///1.重命名“哈希表节点”的类型:HashNode<T> ---> Nodetypedef HashNode<T> Node;//2.重命名“哈希表的迭代器”的类型:HTIterator<K,T,Ref,Ptr,SetKeyOfT,Hash> ---> Selftypedef HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;//3.重命名“哈希表”的类型:HashTable<K,T,KeOfT,Hash> ---> HTtypedef HashTable<K, T, KeyOfT, Hash> HT;/*------------------成员函数------------------*///1.实现:“迭代器的构造函数”HTIterator(Node* node, const HT* ht):_node(node), _ht(ht){}//2.实现:“*运算符的重载” ---> 返回数据的引用(本身)Ref operator*(){return _node->_data;}//3.实现:“->运算符的重载”---> 返回数据的指针(地址)Ptr operator->(){return &_node->_data;}//4.实现:“!=运算符的重载” ---> 用于判断两个迭代器是否指向不同节点bool operator!=(const Self& ht){return _node != ht._node;}//5.实现:“==运算符的重载” ---> 用于判断两个迭代器是否指向相同节点bool operator==(const Self& ht){return _node == ht._node;}//6.实现:“前置++运算符的重载”---> 用于遍历哈希表Self& operator++(){//情况1:当前链表中的还有后序节点 ---> 访问下个节点if (_node->_next){_node = _node->_next;}//情况2:当前链表中的所有节点都已经遍历完了 --->  寻找下一个非空桶else{/*----------------第一步:定义仿函数----------------*///1.定义提取当前节点的键的仿函数KeyOfT kot;//2.定义将键转化为size_t类型的仿函数Hash hashFunc;/*----------------第二步:寻找空桶----------------*///1.计算当前键对应桶索引size_t hash_i = hashFunc(kot(_node->_data)) % _ht->_tables.size();  //注意:第一步一定要先计算桶索引,因为元素可能分布在不同的桶中//2.从当前桶的下一个位置开始线性搜索空桶的位置++hash_i;  //注意这一步哟,如果你不添加这一不步的话,程序会死循环,别问我是怎么知道的哈哈哈//3.使用while循环遍历后序的桶 ---> 直到找到非空桶或结束while (hash_i < _ht->_tables.size()) //未找到:遍历到最后一桶结束{//3.1:获取当桶的头节点_node = _ht->_tables[hash_i];//3.2:情况1:当前桶是非空桶 ---> 停止搜索if (_node){break; //找到了:找到非空桶}//3.3:情况2:当前桶是空桶 ---> 继续检查下一桶else{++hash_i;}}//4.处理未找到空桶的情况 ---> 将迭代器置为end()的状态if (hash_i == _ht->_tables.size()){_node = nullptr;}/*----------------第三步: 返回自身的引用----------------*/return *this;}}};/*------------------任务:定义“哈希表的类模板”------------------*///template<class K, class V, class Hash = HashFunc<K>>template<class K, class T, class KeyOfT, class Hash>  //注意:这里要封装unordered_set/unordered_map 这里的模板参数已经从class HashTable										  //<class K, class V, class Hash = HashFunc<K>> ---> <class K, class T, class KeyOfT, class Hash>{private:/*------------------成员变量------------------*///1.存储HashNode<K, V>*类型数据的数组,每个元素都是桶的头指针//2.记录哈希表中有效元素的变量 //vector<HashNode<K, V>*> _tables;vector<HashNode<T>*> _tables;size_t _n;/*注意现在的结构的形式是:*    1.哈希表*      1.1:成员变量:容器vector*			   1.1.1:哈希表节点类型的指针:HashNode<K,V>**					  1.1.1.1:键值对:_kv*					  1.1.1.2:下一个节点的指:_next*	   1.2:成员变量:变量_n*//*------------------类型别名------------------*/////1.重命名哈希表节点的类型:HashNode<K,V> ---> Node//typedef HashNode<K, V> Node;//1.重命名哈希表节点的类型:HashNode<T> ---> Nodetypedef HashNode<T> Node;/*------------------友元声明------------------*///1.将“哈希表迭代器的类模板”声明为“哈希表类模板”的友元类template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash> //注意:类模板的友元的声明friend struct HTIterator;public:/*------------------类型别名------------------*///1.重命名哈希表的“普通迭代器”的类型:HTIterator<K,T,T&,T*,KeyOfT,Hash> ---> Iteratortypedef HTIterator<K, T, T&, T*, KeyOfT, Hash> Iterator;//2.重命名哈希表的“常量迭代器”的类型:HTIterator<K,T,const T&,const T*,KeyOfT,Hash> ---> ConstIteratortypedef HTIterator<K, T, const T&, const T*, KeyOfT, Hash> ConstIterator;/*------------------成员函数------------------*///1.实现:“获取普通迭代器的起始位置”---> 找到第一个非空桶的第一个节点Iterator Begin(){//1.若哈希表中没有任何有效数据,直接返回结束迭代器if (_n == 0){return End();}//2.使用for循环遍历哈希桶数组寻找第一个非空桶for (size_t i = 0; i < _tables.size(); i++){//2.1:获取第i的桶的头节点Node* current = _tables[i];//2.2:若当前桶的头节点不为空,说明该桶有数据if (current){return Iterator(current, this); //注意:返回指向该桶第一个节点的迭代器,同时传入当前哈希表指针(供迭代器内部使用)}}}//2.实现:“获取普通迭代器的终止位置”---> 用nullptr表示Iterator End(){return Iterator(nullptr, this);}//3.实现:“获取常量迭代器的起始位置”---> 找到第一个非空桶的第一个节点(只读)ConstIterator Begin()const{//1.若哈希表中没有任何有效数据,直接返回结束迭代器if (_n == 0){return End();}//2.使用for循环遍历哈希桶数组寻找第一个非空桶for (size_t i = 0; i < _tables.size(); i++){//2.1:获取第i的桶的头节点Node* current = _tables[i];//2.2:若当前桶的头节点不为空,说明该桶有数据if (current){return ConstIterator(current, this); //注意:返回指向该桶第一个节点的迭代器,同时传入当前哈希表指针(供迭代器内部使用)}}}//4.实现:“获取常量迭代器的终止位置”---> 用nullptr表示ConstIterator End()const{return ConstIterator(nullptr, this);}//5.实现:“哈希表的构造函数”HashTable():_tables(_stl_next_prime(0)) //初始化为11个桶, _n(0){}//6.实现:“哈希表的析构函数”~HashTable(){//1.遍历哈希表的每个桶(注:_tables 是存储桶头节点指针的容器)for (size_t i = 0; i < _tables.size(); ++i){//2.获取当前桶的头节点指针,从第一个桶开始清理Node* current = _tables[i];//3.遍历当前桶对应的链表,逐个释放节点内存while (current){//3.1:提前保存当前节点的下一个节点指针 ---> 防止删除当前节点后丢失链表连接Node* next = current->_next;//3.2:释放当前节点的内存(避免内存泄漏)delete current;//3.3:移动到下一个节点,继续清理current = next;}//4.将当前桶的头节点指针置空,确保析构后桶不会指向无效内存_tables[i] = nullptr;}}//3.实现:“查找操作”---> 根据键查找对应的节点,找到返回节点指针,未找到返回 nullptr//Node* Find(const K& key)Iterator Find(const K& key)  //注意:这里要封装unordered_set / unordered_map 这里函数的返回类型已经从:Node* ---> Iterator{/*----------------第一步:定义仿函数----------------*///1. 定义提取当前节点的键的仿函数KeyOfT kot;//2. 定义将键转化为size_t类型的仿函数Hash hashFunc;/*----------------第二步:寻找键对应的桶----------------*///1. 计算键的哈希值并取模,得到对应的桶索引size_t hash_i = hashFunc(key) % _tables.size();/* 代码解释:*      1. hashFunc(key):调用哈希函数,将键转换为 size_t 类型的哈希值*	   2. % _tables.size():对哈希表的桶数量取模,确定节点应该落在哪个桶中*	   3.目的:将任意键映射到 [0, _tables.size()-1] 范围内的桶索引*///2. 获取对应桶的头节点,开始遍历链表Node* current = _tables[hash_i]; //注意:_tables[hash_i] 是桶的头节点指针/*----------------第三步:遍历当前桶的链表----------------*///1. 遍历当前桶中的链表,查找目标键while (current) //注意:若 current 为 nullptr,说明桶为空,直接跳过循环{//1.1 检查当前节点的键是否匹配目标 key//if (current->_kv.first == key)if (kot(current->_data) == key){//return current;return Iterator(current, this);}//1.2 若不匹配,移动到链表的下一个节点else{current = current->_next;}}////2. 遍历完链表后仍未找到匹配的键,返回 nullptr//return nullptr;//2. 遍历完整个桶的链表仍未找到,返回 End() 迭代器return End();}//4.实现:“删除操作”---> 根据键删除哈希表中的节点,成功返回 true,失败返回 falsebool Erase(const K& key){/*----------------第一步:定义仿函数----------------*///1.定义提取当前节点的键的仿函数KeyOfT kot;//2.定义将键转化为size_t类型的仿函数Hash hashFunc;/*----------------第二步:寻找键对应的桶----------------*///1.计算键对应的桶索引size_t hash_i = hashFunc(key) % _tables.size();//2.定义一个指向桶的头节点的指针Node* curr = _tables[hash_i];//3.定义一个指向当前节点的前驱节点的指针Node* prev = nullptr;  //作用记录待删除节点的前驱节点/*----------------第三步:遍历当前桶的链表----------------*///1.遍历当前桶的链表,查找目标键while (curr){//情况一:找到目标节点//if (curr->_kv.first == key)if (kot(curr->_data) == key){//1.删除目标节点///场景一:待删除节点是桶的头节点if (prev == nullptr){_tables[hash_i] = curr->_next; // 直接将桶的头指针指向头节点的下一个节点}//场景二:待删除节点在链表中间或末尾else{prev->_next = curr->_next; // 让前驱节点的 next 指针跳过当前节点,指向后一个节点}//2.释放目标节点的内存delete curr;//3.更新有效元素数量--_n;//4.删除成功,返回 truereturn true;}//情况二:未找到目标节点,继续遍历//1.当前节点的前驱节点指向当前节点prev = curr;//2.当前节点指向下一个节点的位置curr = curr->_next;}//2.遍历完整个桶的链表仍未找到目标键,删除失败return false;}//5.实现:“插入操作”---> 插入键值对,成功返回true,键已存在返回false//bool Insert(const pair<K, V>& kv)pair<Iterator, bool> Insert(const T& data) //注意:封装unordered_map 时候会添加operator[]操作,所以这里的Insert函数的返回类型从bool ---> pair<Iterator bool>{/*----------------第一步:查重判断----------------*///1.定义提取当前节点的键的仿函数KeyOfT kot;//2.使用Find函数判断键是否已经存在Iterator it = Find(kot(data));if (it != End()){//return false; // 当键kv.first已经存在时,插入失败return { it,false }; //若存在,直接返回已有迭代器和插入失败标志}/*----------------第二步:扩容操作----------------*///1.进行扩容判断:负载因子(元素数/桶数)等于1时触发扩容if (_n == _tables.size()) //目的:避免哈希冲突过多导致链表过长,影响性能{//2.创建新数组,容量为大于当前size的最小质数(减少哈希冲突)//vector<Node*> newVector(_stl_next_prime(_tables.size() + 1));vector<Node*> newVector(_tables.size() * 2);//3.使用for循环遍历旧表中的所有桶 ---> 将旧表中的所有节点重新插入到新表中for (size_t i = 0; i < _tables.size(); i++){//4.定义一个指针指向当前节点的指针Node* current = _tables[i];//5.遍历链表 ---> 将遍历的到的每个节点节点进行插入while (current){//6.定义一个指针指向当前节点的下一个节点的指针 ---> 暂存下一节点避免链表断裂Node* next = current->_next;//7.定义将键转化为size_t类型的仿函数Hash hashFunc;//8.重新计算“遍历到节点”在新表中的桶索引 ---> 因表长变化,哈希值需重新取模//size_t hash_i = hashFunc(current->_kv.first) % newVector.size();size_t hash_i = hashFunc(kot(current->_data)) % newVector.size();//9.使用头插入法将当前节点插入新表对应桶的头部//第一步:新插入的节点的下一个节点 ---> 该节点的桶索引位置的链表头节点current->_next = newVector[hash_i];//第二步:将新插入的节点的指针赋值给其对应的桶newVector[hash_i] = current;//第三步:指向当前节点的current指针向后移动一位current = next;}//10.清空旧表的当前桶_tables[i] = nullptr;  //注意:当前桶在旧表中的哈希值是i,在新表中的哈希值是hash_i}//11.交换新旧哈希表:旧表.swap(新表)_tables.swap(newVector);   //注意:当前表指向新表,旧表由newht接管(离开作用域时自动释放)}/*----------------第三步:插入数据----------------*///1.创建新节点//Node* newNode = new Node(kv);Node* newNode = new Node(data);//2.定义将键转化为size_t类型的仿函数Hash hashFunc;//3.计算“新插入数据”的哈希值/桶索引//size_t hash_i = hashFunc(kv.first) % _tables.size();size_t hash_i = hashFunc(kot(data)) % _tables.size();//4.1:头插第一步:newNode->_next = _tables[hash_i];//4.2:头插第二步:_tables[hash_i] = newNode;//5.更新新表中有效元素的数量++_n;////6.插入成功返回true即可//return true;//6.返回插入成功的信息return { Iterator(newNode, this), true }; //新节点迭代器和插入成功标志//注意:迭代器由两部分组成:1.当前节点的指针 2.当前哈希表的指针}};
}

Myunordered_set.h

#pragma once#include "HashTable.h"namespace Myunordered_set
{/*------------任务:定义哈希表中节点的三种状态的“枚举”------------*/enum State{EXIST,  //存在状态EMPTY,  //空状态DELETE  //删除状态};/*------------任务:定义哈希表存储的数据结构的“结构体模板”------------*/template <class K, class V>struct HashData{//1.存储键值对类型的数据//2.记录存储的节点的状态pair<K, V> _kv;State _state = EMPTY; //节点的状态默认为空};/*------------任务:实现一个基于哈希表的无序集合------------*/template<class K, class Hash = HashFunc<K>>class unordered_set{private:/*--------------------成员函数--------------------*///1.实现:“从键值对中取键的仿函数”(当然这里的键就是本身)struct SetKeyOfT{const K& operator()(const K& key){return key;}};/*--------------------成员变量--------------------*///注意:unoredered_set容器的底层是“使用链地址法实现的哈希表”hash_bucket::HashTable<K, const K, SetKeyOfT, Hash> _ht;public:/*--------------------类型别名--------------------*///1.重定义哈希表底层的“普通迭代器”hash_bucket::HashTable<K,const K,SetKeyOfT,Hash>::Iterator ---> iteratortypedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::Iterator iterator;//2.重定义哈希表底层的“常量迭代器”hash_bucket::HashTable<K,const K,SetKeyOfT,Hash>::ConstIterator ---> const_iteratortypedef typename hash_bucket::HashTable<K, const K, SetKeyOfT, Hash>::ConstIterator const_iterator;/*--------------------成员函数--------------------*///1.实现:“获取普通迭代器的起始位置”iterator begin(){return _ht.Begin();}//2.实现:“获取普通迭代器的终止位置”iterator end(){return _ht.End();}//3.实现:“获取常量迭代器的起始位置”const_iterator begin()const{return _ht.Begin();}//4.实现:“获取常量迭代器的终止位置”const_iterator end()const{return _ht.End();}//5.实现:“查找操作”iterator find(const K& key){return _ht.Find(key);}//6.实现:“删除操作”bool erase(const K& key){return _ht.Erase(key);}//7.实现:“插入操作”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:/*--------------------成员函数--------------------*///1.实现:“从键值对中取键的仿函数”(用于哈希表操作)struct MapKeyOfT{const K& operator()(const pair<K, V>& kv){return kv.first;}};/*--------------------成员变量--------------------*///注意:unoredered_set容器的底层是“使用链地址法实现的哈希表”hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;public:/*--------------------类型别名--------------------*///1.重定义哈希表底层的“普通迭代器”hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::Iterator ---> iteratortypedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::Iterator iterator;//2.重定义哈希表底层的“常量迭代器”hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::ConstIterator ---> const_iteratortypedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash>::ConstIterator const_iterator;/*--------------------成员函数--------------------*///1.实现:“获取普通迭代器的起始位置”iterator begin(){return _ht.Begin();}//2.实现:“获取普通迭代器的终止位置”iterator end(){return _ht.End();}//3.实现:“获取常量迭代器的起始位置”const_iterator begin()const{return _ht.Begin();}//4.实现:“获取常量迭代器的终止位置”const_iterator end()const{return _ht.End();}//5.实现:“查找操作”iterator find(const K& key){return _ht.Find(key);}//6.实现:“删除操作”bool erase(const K& key){return _ht.Erase(key);}//7.实现:“插入操作”pair<iterator, bool> insert(const pair<K, V>& kv){return _ht.Insert(kv);}//8.实现:“[]运算符的重载”V& operator[](const K& key){//1定义变量接收insert函数的返回值pair<iterator, bool> ret = insert({ key,V() });//2.返回插入或已存在的键对应的值引用return ret.first->second;}/* 注意事项:*      1. 如果键存在,返回对应值的引用*	   2. 如果键不存在,插入新键值对(值为默认构造)并返回引用*/};
}

测试文件:Test.cpp

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <string>#include "Myunordered_set.h"
#include "Myunordered_map.h"
using namespace std;/*--------------------------测试:unordered_set容器的基本功能--------------------------*/
void test01()
{cout << "=== 测试 unordered_set 基本功能 ===" << endl;//1. 初始化与插入Myunordered_set::unordered_set<int> s;s.insert(3);s.insert(1);s.insert(4);s.insert(1);  // 重复插入,应被忽略s.insert(2);s.insert(5);//2. 迭代器遍历(无序)cout << "遍历元素: ";for (auto it = s.begin(); it != s.end(); ++it){cout << *it << " ";}cout << endl;//3. 查找操作int key = 3;auto find_ret = s.find(key);if (find_ret != s.end()){cout << "找到元素: " << *find_ret << endl;}else{cout << "未找到元素: " << key << endl;}//4. 删除操作key = 4;bool erase_ret = s.erase(key);if (erase_ret){cout << "删除元素 " << key << " 成功" << endl;}else{cout << "删除元素 " << key << " 失败" << endl;}//5. 验证删除结果cout << "删除后遍历: ";for (auto val : s)  // 范围for循环(依赖begin/end){cout << val << " ";}cout << endl << endl;
}/*--------------------------测试:unordered_set 对“字符串类型”的支持--------------------------*/
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 (const auto& 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 (const auto& str : str_set){cout << str << " ";}cout << endl << endl;
}/*--------------------------测试:unordered_map 的基本功能--------------------------*/
void test03()
{cout << "=== 测试 unordered_map 基本功能 ===" << endl;//1. 初始化与插入Myunordered_map::unordered_map<string, int> m;m.insert({ "apple", 10 });m.insert({ "banana", 20 });m.insert({ "cherry", 30 });m.insert({ "apple", 15 });  // 重复插入,应被忽略//2. 使用[]运算符(插入+修改)m["date"] = 40;       // 插入新键值对m["banana"] = 25;     // 修改已有值//3. 迭代器遍历(键值对)cout << "遍历键值对: " << endl;for (auto it = m.begin(); it != m.end(); ++it){cout << it->first << ": " << it->second << endl;}//4. 查找操作string key = "cherry";auto it = m.find(key);if (it != m.end()){cout << "找到 " << key << ": " << it->second << endl;}else{cout << "未找到 " << key << endl;}//5. 删除操作key = "banana";bool erase_ret = m.erase(key);if (erase_ret){cout << "删除 " << key << " 成功" << endl;}else{cout << "删除 " << key << " 失败" << endl;}//6. 验证删除结果cout << "删除后 " << key << " 是否存在: "<< (m.find(key) != m.end() ? "是" : "否") << endl << endl;
}/*--------------------------测试:Myunordered_map 的[]运算符特殊场景--------------------------*/
void test04()
{cout << "=== 测试 unordered_map []运算符 ===" << endl;Myunordered_map::unordered_map<int, string> m;//1. 插入新键(值为默认构造)m[1];  // 插入{1, ""}//2. 插入并修改m[2] = "two";//3. 修改已有值m[2] = "second";//4. 插入新键值对m[3] = "three";//5.遍历验证for (auto& kv : m){cout << kv.first << ": " << kv.second << endl;}cout << endl;
}/*--------------------------测试:测试边界情况(空容器、删除不存在元素等)--------------------------*/
void test05()
{cout << "=== 测试边界情况 ===" << endl;//1.测试空setMyunordered_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;//2.测试空mapMyunordered_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;
}

运行结果:

在这里插入图片描述

------------代码解释------------

片段一:仿函数的设计

setmap相比,unordered_setunordered_map的模拟实现类结构要更复杂一些,但整体大框架和思路是完全类似的。


由于HashTable是泛型实现,无法确定模板参数T具体是K(像unordered_set的元素类型 ),还是pair<K, V>(像unordered_map的键值对类型 )。

在插入操作(insert)时,需要用K对象转换为整数取模,以及进行K的相等性比较。

  • pairvalue本就不参与取模计算
  • 而且默认比较逻辑是对keyvalue一起比较是否相等,但我们实际只需要比较K对象

所以:在unordered_setunordered_map这一层,我们分别实现SetKeyOfTMapKeyOfT这两个仿函数,传递给HashTableKeyOfT

之后HashTable就能通过KeyOfT仿函数,从T类型对象里提取出K对象,再完成转换为整数取模以及K相等性比较的操作

/*-------------------------- Myunordered_set.h --------------------------*/// Myunordered_set.h
namespace Myunordered_set
{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:pair<iterator, bool> insert(const K& key){return _ht.Insert(key);}};
}/*-------------------------- Myunordered_map.h --------------------------*/// Myunordered_map.h
namespace bit
{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:pair<iterator, bool> insert(const pair<K, V>& kv){return _ht.Insert(kv);}};
}/*-------------------------- HashTable.h --------------------------*/// HashTable.h
template<class K>
struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};
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{private:vector<HashNode<T>*> _tables;size_t _n;typedef HashNode<T> Node;public:HashTable(){_tables.resize(_stl_next_prime(_tables.size()), nullptr);}~HashTable(){//1.遍历哈希表的每个桶(注:_tables 是存储桶头节点指针的容器)for (size_t i = 0; i < _tables.size(); ++i){//2.获取当前桶的头节点指针,从第一个桶开始清理Node* current = _tables[i];//3.遍历当前桶对应的链表,逐个释放节点内存while (current){//3.1:提前保存当前节点的下一个节点指针 ---> 防止删除当前节点后丢失链表连接Node* next = current->_next;//3.2:释放当前节点的内存(避免内存泄漏)delete current;//3.3:移动到下一个节点,继续清理current = next;}//4.将当前桶的头节点指针置空,确保析构后桶不会指向无效内存_tables[i] = nullptr;}}pair<Iterator, bool> Insert(const T& data) //注意:封装unordered_map 时候会添加operator[]操作,所以这里的Insert函数的返回类型从bool ---> pair<Iterator bool>{/*----------------第一步:查重判断----------------*///1.定义提取当前节点的键的仿函数KeyOfT kot;//2.使用Find函数判断键是否已经存在Iterator it = Find(kot(data));if (it != End()){//return false; // 当键kv.first已经存在时,插入失败return { it,false }; //若存在,直接返回已有迭代器和插入失败标志}/*----------------第二步:扩容操作----------------*///1.进行扩容判断:负载因子(元素数/桶数)等于1时触发扩容if (_n == _tables.size()) //目的:避免哈希冲突过多导致链表过长,影响性能{//2.创建新数组,容量为大于当前size的最小质数(减少哈希冲突)//vector<Node*> newVector(_stl_next_prime(_tables.size() + 1));vector<Node*> newVector(_tables.size() * 2);//3.使用for循环遍历旧表中的所有桶 ---> 将旧表中的所有节点重新插入到新表中for (size_t i = 0; i < _tables.size(); i++){//4.定义一个指针指向当前节点的指针Node* current = _tables[i];//5.遍历链表 ---> 将遍历的到的每个节点节点进行插入while (current){//6.定义一个指针指向当前节点的下一个节点的指针 ---> 暂存下一节点避免链表断裂Node* next = current->_next;//7.定义将键转化为size_t类型的仿函数Hash hashFunc;//8.重新计算“遍历到节点”在新表中的桶索引 ---> 因表长变化,哈希值需重新取模//size_t hash_i = hashFunc(current->_kv.first) % newVector.size();size_t hash_i = hashFunc(kot(current->_data)) % newVector.size();//9.使用头插入法将当前节点插入新表对应桶的头部//第一步:新插入的节点的下一个节点 ---> 该节点的桶索引位置的链表头节点current->_next = newVector[hash_i];//第二步:将新插入的节点的指针赋值给其对应的桶newVector[hash_i] = current;//第三步:指向当前节点的current指针向后移动一位current = next;}//10.清空旧表的当前桶_tables[i] = nullptr;  //注意:当前桶在旧表中的哈希值是i,在新表中的哈希值是hash_i}//11.交换新旧哈希表:旧表.swap(新表)_tables.swap(newVector);   //注意:当前表指向新表,旧表由newht接管(离开作用域时自动释放)}private:vector<Node*> _tables;  // 指针数组 size_t _n = 0;          // 表中存储有效数据个数 }};
}

片段二:迭代器的设计

第一点unordered_setunordered_map的 iterator 的实现框架,和 list 的 iterator 思路一致:

  • 用一个类型封装 “结点指针”,再通过重载运算符,让迭代器能像指针一样完成访问操作
  • 需要注意的是,哈希表的迭代器是单向迭代器(只能向后遍历,无法反向)

第二点unordered_setunordered_map的 iterator 的容器特性的满足:

unordered_set 迭代器的 “不可修改性”

  • unordered_set 的迭代器不支持修改 key,只需把 unordered_set 的第二个模板参数改为 const K 即可,对应哈希表声明为:

    HashTable<K, const K, SetKeyOfT, Hash> _ht;
    

unordered_map 迭代器的 “部分可修改性”

  • unordered_map 的迭代器不允许修改 key,但允许修改 value 。只需把 unordered_map 的键值对中 “第一个参数(key)” 设为 const K 即可,对应哈希表声明为:

    HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
    

总结:这样一来,迭代器遍历、修改的规则就和容器特性(unordered_set/unordered_map 的 “键是否可改” 需求 )匹配上了,既保证逻辑合理,也符合 C++ 对容器迭代器的设计习惯。

//前置声明,因为 HTIterator 中要用到 HashTable 
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
{/*------------------成员变量------------------*///1.指向当前节点的指针//2.指向哈希表的指针 ---> 用于遍历的桶HashNode<T>* _node;const HashTable<K, T, KeyOfT, Hash>* _ht; // 注意:迭代器这里需要使用到哈希表,所以在定义迭代器之前我们先进行了哈希表的前置声明/*------------------类型别名------------------*///1.重命名“哈希表节点”的类型:HashNode<T> ---> Nodetypedef HashNode<T> Node;//2.重命名“哈希表的迭代器”的类型:HTIterator<K,T,Ref,Ptr,SetKeyOfT,Hash> ---> Selftypedef HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;//3.重命名“哈希表”的类型:HashTable<K,T,KeOfT,Hash> ---> HTtypedef HashTable<K, T, KeyOfT, Hash> HT;/*------------------成员函数------------------*///1.实现:“迭代器的构造函数”HTIterator(Node* node, const HT* ht):_node(node), _ht(ht){}//2.实现:“*运算符的重载” ---> 返回数据的引用(本身)Ref operator*(){return _node->_data;}//3.实现:“->运算符的重载”---> 返回数据的指针(地址)Ptr operator->(){return &_node->_data;}//4.实现:“!=运算符的重载” ---> 用于判断两个迭代器是否指向不同节点bool operator!=(const Self& ht){return _node != ht._node;}//5.实现:“==运算符的重载” ---> 用于判断两个迭代器是否指向相同节点bool operator==(const Self& ht){return _node == ht._node;}//6.实现:“前置++运算符的重载”---> 用于遍历哈希表Self& operator++(){//情况1:当前链表中的还有后序节点 ---> 访问下个节点if (_node->_next){_node = _node->_next;}//情况2:当前链表中的所有节点都已经遍历完了 --->  寻找下一个非空桶else{/*----------------第一步:定义仿函数----------------*///1.定义提取当前节点的键的仿函数KeyOfT kot;//2.定义将键转化为size_t类型的仿函数Hash hashFunc;/*----------------第二步:寻找空桶----------------*///1.计算当前键对应桶索引size_t hash_i = hashFunc(kot(_node->_data)) % _ht->_tables.size();  //注意:第一步一定要先计算桶索引,因为元素可能分布在不同的桶中//2.从当前桶的下一个位置开始线性搜索空桶的位置++hash_i;  //注意这一步哟,如果你不添加这一不步的话,程序会死循环,别问我是怎么知道的哈哈哈//3.使用while循环遍历后序的桶 ---> 直到找到非空桶或结束while (hash_i < _ht->_tables.size()) //未找到:遍历到最后一桶结束{//3.1:获取当桶的头节点_node = _ht->_tables[hash_i];//3.2:情况1:当前桶是非空桶 ---> 停止搜索if (_node){break; //找到了:找到非空桶}//3.3:情况2:当前桶是空桶 ---> 继续检查下一桶else{++hash_i;}}//4.处理未找到空桶的情况 ---> 将迭代器置为end()的状态if (hash_i == _ht->_tables.size()){_node = nullptr;}/*----------------第三步: 返回自身的引用----------------*/return *this;}}
};

片段三:operator++的设计

迭代器内部会维护一个 “指向结点的指针”operator++ 实现时,要分两种情况:

  • 如果当前桶还有后续结点直接让指针指向下一个结点即可
  • 如果当前桶已经遍历完毕就需要 “寻找下一个桶”

这里的关键设计是:迭代器中除了存 “结点指针”,还会存 “哈希表对象的指针”


这样,当当前桶遍历完时:

  • 可通过哈希表对象,结合 key 值计算当前桶位置
  • 然后往后找 “第一个不为空的桶”,就能拿到下一批结点

在这里插入图片描述

//6.实现:“前置++运算符的重载”---> 用于遍历哈希表
Self& operator++()
{//情况1:当前链表中的还有后序节点 ---> 访问下个节点if (_node->_next){_node = _node->_next;}//情况2:当前链表中的所有节点都已经遍历完了 --->  寻找下一个非空桶else{/*----------------第一步:定义仿函数----------------*///1.定义提取当前节点的键的仿函数KeyOfT kot;//2.定义将键转化为size_t类型的仿函数Hash hashFunc;/*----------------第二步:寻找空桶----------------*///1.计算当前键对应桶索引size_t hash_i = hashFunc(kot(_node->_data)) % _ht->_tables.size();  //注意:第一步一定要先计算桶索引,因为元素可能分布在不同的桶中//2.从当前桶的下一个位置开始线性搜索空桶的位置++hash_i;  //注意这一步哟,如果你不添加这一不步的话,程序会死循环,别问我是怎么知道的哈哈哈//3.使用while循环遍历后序的桶 ---> 直到找到非空桶或结束while (hash_i < _ht->_tables.size()) //未找到:遍历到最后一桶结束{//3.1:获取当桶的头节点_node = _ht->_tables[hash_i];//3.2:情况1:当前桶是非空桶 ---> 停止搜索if (_node){break; //找到了:找到非空桶}//3.3:情况2:当前桶是空桶 ---> 继续检查下一桶else{++hash_i;}}//4.处理未找到空桶的情况 ---> 将迭代器置为end()的状态if (hash_i == _ht->_tables.size()){_node = nullptr;}/*----------------第三步: 返回自身的引用----------------*/return *this;}
}

片段四:begin()和end()的设计

begin()end() 的设计:

  • begin():返回 “第一个非空桶的第一个结点指针” 构造的迭代器
  • end():返回一个代表 “遍历结束” 的空迭代器(可用空指针等方式表示)
//1.实现:“获取普通迭代器的起始位置”---> 找到第一个非空桶的第一个节点
Iterator Begin()
{//1.若哈希表中没有任何有效数据,直接返回结束迭代器if (_n == 0){return End();}//2.使用for循环遍历哈希桶数组寻找第一个非空桶for (size_t i = 0; i < _tables.size(); i++){//2.1:获取第i的桶的头节点Node* current = _tables[i];//2.2:若当前桶的头节点不为空,说明该桶有数据if (current){return Iterator(current, this); //注意:返回指向该桶第一个节点的迭代器,同时传入当前哈希表指针(供迭代器内部使用)}}
}//2.实现:“获取普通迭代器的终止位置”---> 用nullptr表示
Iterator End()
{return Iterator(nullptr, this);
}

片段五:map支持[]运算符的重载

要让 unordered_map 支持 [] 运算符,关键在于调整 insert 相关的返回值逻辑 。

  • 具体来说,需要修改 HashTable 中的 Insert 函数,使其返回值类型变为 pair<Iterator, bool> ,也就是把 Insert 函数定义改为

    pair<Iterator, bool> Insert(const T& data)
    
  • 这样的修改,能为 unordered_map 实现 [] 运算符提供必要的返回值支撑,让插入操作的结果可以被合理利用

在这里插入图片描述

http://www.dtcms.com/a/390428.html

相关文章:

  • LLM中如何添加special_token,并且尽可能保持原模型的训练效果
  • [x-cmd] 使用系统包管理器安装 x-cmd
  • 亮数据MCP结合Dify:构建自动化视频数据抓取与智能分析工作流的深度实践
  • 革新交互体验,开启未来智慧生活 —— OPE.AI 多语言多模态AI产品
  • 生活琐记(2)
  • 一文读懂HTTP 1.1/2.0/3.0:从原理到应用的通俗解析
  • JavaStream用法全解析
  • 在日常开发中实现异常处理和空值处理的最佳实践
  • openstack port binding failure (by quqi99)
  • leetcode 3484. 设计电子表格 中等
  • Docker+cpolar 实战:打造灵活可控的远程办公系统
  • uniApp开发XR-Frame微信小程序 | 设置透明贴图
  • M3 Ultra版Mac Studio无法正常升级到macOS 26.0 苹果已经在调查
  • 老的ios项目在新的mac M1上编译运行遇到的问题及整理
  • Java 大视界 -- Java 大数据机器学习模型在元宇宙虚拟场景智能交互中的关键技术
  • 2025年目标检测还有什么方向好发论文?
  • 离线openHarmonySdk鸿蒙系统动态库的封装
  • 从零实现鸿蒙智能设备数据采集:权限、传感器、云端上传全流程实战
  • 智慧医院IBMS中央集成系统解决方案:构建医疗安全优先的智慧运营中枢​
  • ✅ Python房源数据采集+分析+预测平台 requests爬虫+sklearn回归 大数据实战项目(建议收藏)机器学习(附源码)
  • 结婚证 OCR 识别:政务服务提速的 “关键一环”
  • Git企业开发--多人协作
  • 【论文阅读 | IF 2025 | IF-USOD:用于水下显著目标检测的多模态信息融合交互式特征增强架构】
  • 【14/20】安全强化:HTTPS 和率限制在 Express 中的应用,实现防护机制
  • C#调用钉钉API实现安全企业内部通知推送
  • MyBatis与MyBatis-Plus区别
  • 数据血缘探秘:用SQL串联不同数据源的脉络
  • 多线程程序性能优化:缓存命中率与伪共享深度剖析
  • Spring Boot支持哪些类型的自定义配置文件?
  • uniapp:scss变量使用方法