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

【C++】封装哈希桶实现unordered_map和unordered_set

封装红黑树实现map和set容器

  • 封装哈希桶实现unordered_map和unordered_set
  • github地址
  • 前言
  • 一、改造哈希桶
    • 1. 改造后实现代码整体框架
      • 哈希函数的模板以及为string的特化
      • 哈希桶的结点定义
      • 哈希桶架构设计
    • 2. 改造细节分析
      • 哈希桶泛型设计思想解析
      • unordered_set & unordered_map 实例化 哈希表 时的差异
      • 为什么哈希桶需要两个模板参数(Key & Value)?
  • 二、KeyOfT 修改哈希桶 insert 中的 取模逻辑
    • 哈希桶架构设计
    • SetKeyOfT
    • MapKeyOfT
    • 传入不同的仿函数实例化哈希桶分析
    • KeyOfT 对哈希桶中 insert 取模的修改
  • 三、迭代器与相关重载实现
    • 1. 自定义哈希的iterator类
      • 由 普通迭代器构造 const 迭代器
    • 2. operator*
    • 3. operator->
    • 4. operator!=
    • 5. operator==
    • 6. operator++
  • 四、哈希桶的iterator以及const_itreator的封装
    • 1. 封装iterator与const_iterator
    • 2. begin 与 const_begin
    • 3. end 与 const_end
    • 4. 迭代器中哈希桶的指针为什么定义为const T*
  • 五、实现Key不支持修改与容器的迭代器
    • 1. unordered_set 借助 const 迭代器实现 Key 不支持修改
    • 2. unordered_map 实现 Key 不支持修改
    • 3. 封装 unordered_map 的迭代器
      • begin与end函数
  • 六、改造insert实现operator[]
    • 1. operator[] 的介绍
    • 2. operator[]和insert行为的哲学
    • 3.哈希桶中 insert 函数的修改
    • 4. unordered_map中 operator[] 的实现
    • 4. unordered_set 中 insert 的实现
      • 实现 insert
      • 由普通迭代器构造 const 迭代器 详解
  • 七、完整代码实现
    • m_unordered_set
    • m_unordered_map
    • 改造后的哈希桶
  • 八、结语

封装哈希桶实现unordered_map和unordered_set

github地址

有梦想的电信狗

前言

上文链接1:封装红黑树实现map和set

上文链接2:哈希表的实现

封装哈希桶实现 unordered_mapunordered_set封装红黑树实现map和set的思路完全一致


在前两篇文章《封装红黑树实现 map 和 set》与《哈希表的实现》中,我们分别从有序关联容器的平衡二叉树结构哈希表的底层原理两方面进行了完整剖析。
本篇文章将继续延续这一思路,带你基于自定义哈希桶(Hash Bucket)封装出 STL 中的 unordered_mapunordered_set 容器

与基于红黑树的有序容器不同,无序容器以哈希表为核心,通过高效的哈希映射实现常数级时间复杂度的查找、插入与删除操作。
我们将通过泛型设计、Key 萃取器(KeyOfT)、仿函数特化与迭代器封装等方式,一步步复刻出完整的 unordered_map / unordered_set 实现。

文章将围绕以下几个关键问题展开:

  1. 如何通过模板参数泛化哈希桶结构以同时支持 unordered_mapunordered_set
  2. 为什么需要设计 KeyOfT 仿函数,它解决了什么核心问题?
  3. 如何实现自定义迭代器以支持 begin / end++ 运算?
  4. unordered_map 如何通过改造 insert 实现 operator[]
  5. 如何通过类型修饰与封装实现 “key 不可修改” 的约束?

通过本文的完整实现与讲解,你将从底层彻底理解 STL 无序关联容器的设计思想与工程实现,体会泛型编程在容器封装中的威力。


一、改造哈希桶

1. 改造后实现代码整体框架

哈希函数的模板以及为string的特化

// 使用仿函数控制 string 和 其他整型的取模
template<class K>
struct DefaultHashFunc
{size_t operator()(const K& key){return static_cast<size_t> (key);}
};template <>
struct DefaultHashFunc<string>
{size_t operator()(const string& str){// BKDRsize_t hash = 0;for (auto ch : str) {hash *= 131;hash += ch;}return hash;}
};

哈希桶的结点定义

  • 哈希桶结点为模版实现
// 由于 unordered_set 和 unordered_map 底层存储的数据不一样, unordered_set 存储 key,unordered_map 存储 pair<key, value> (key-value)
// 所以哈希桶结点存储的数据类型不是写死的, 而是写成一个模板参数 T
// 哈希桶结点代码模板被实例化出两份,一份 _data 存储 K 类型,一份 _data 存储 pair<K, V> 类型
template<class T>
struct HashNode
{T _data;struct HashNode<T>* _next;HashNode(const T& data):_data(data),_next(nullptr){ }
};

搜索结构常用于存储键值,方便查找关键字,这里我们使用T _data来存储我们的实际数据

  • 在 unordered_set 中 T 为 Key
  • 在 unordered_map 中 T 为 pair<K, V>

struct HashNode<K, V>* _next;:单链表设计,存储指向下一个结点的指针

  • 默认构造函数HashNode(const pair<K, V>& kv)
    • _next指针初始化为nullptr
    • 使用形参data初始化类内的_data成员

哈希桶架构设计

// unordered_set -> hash_bucket::HashTable<K, K> _ht;
// unordered_map  -> hash_bucket::HashTable<K, std::pair<K, V>> _ht;
// template<class K, class T, class KeyOfT, class HashFunc = DefaultHashFunc<K>>
template<class K, class T, class KeyOfT, class HashFunc>
class HashTable
{typedef struct HashNode<T> Node;template<class K, class T, class Ptr, class Ref, class KeyOfT, class HashFunc>friend struct HTIterator;private:vector<Node*> _table;	// 需要写析构函数size_t _n = 0;public:// ... 迭代器和相关成员函数   
}
  • 迭代器需要访问表中的私有数据,因此声明迭代器为哈希表的友元

    •   template<class K, class T, class Ptr, class Ref, class KeyOfT, class HashFunc>friend struct HTIterator;
      
  • 节点类型重定义typedef struct HashNode<T> Node;


2. 改造细节分析

哈希桶泛型设计思想解析

我们和封装红黑树实现 mapset 一样, 哈希桶使用巧妙的泛型思想实现

  • 不直接写死“仅支持key 搜索”或“仅支持key/value 搜索"

  • 而是通过哈希桶第二个模板参数 Value 灵活控制哈希桶节点中实际存储的数据类型这样一份哈希桶代码,既可以实现 key 搜索场景的 unordered_set,也可以实现 key/value 搜索场景的 unordered_map。

    • unordered_set 实例化 哈希桶 时,第二个模板参数给的是 key,

    • unordered_map 实例化 哈希桶 时,第二个模板参数给的是 pair。


unordered_set & unordered_map 实例化 哈希表 时的差异

  • unordered_set 实例化时的场景

    • unordered_set 实例化 HashTable 时,给第二个模板参数传入纯 key 类型
    • 哈希表节点直接存储 key,自然适配“仅按 key 搜索、不重复存储”的需求
  • unordered_map 实例化时的场景

    • unordered_map 实例化 HashTable 时,给第二个模板参数传入键值对类型pair<const Key,T>
    • 哈希桶节点存储完整的键值对pair,从而支持“按 key 关联 value ”的搜索逻辑

为什么哈希桶需要两个模板参数(Key & Value)?

既然 hash_table 第二个模板参数 Value已经控制了哈希表节点中存储的数据类型,为什么还要传第一个模板参数 Key 呢?

尤其是 unordered_set,两个模板参数均为K,这是为什么呢?

核心原因在于 find/erase 等操作的需求

  • 对 unordered_set 和 unordered_map 来说find /erase函数的参数是 Key 类型(按key 查找、删除),而非完整的节点数据(Value 类型)

  • unordered_set而言

    • KeyValue 类型相同(节点存key,操作也用 key),两个模板参数看似冗余,但是这样做主要是为了和unordered_map容器保持统一的接口
  • unordered_map 而言:

    • Key(操作入参类型)和 Value(节点存储的键值对类型)完全不同——unordered_map 插入的是 pair对象,但查找删除只用 Key

因此Key 模板参数的意义是为 find/erase 等函数提供形参类型让一份哈希桶模板代码通过不同的实例化,能统一支撑 unordered_set(Key 与Value 同类型)和unordered_map(Key 与 Value 不同类型)的存储场景。


二、KeyOfT 修改哈希桶 insert 中的 取模逻辑

由于 哈希桶 采用泛型设计无法直接判断模板参数 T 具体是单纯的键类型K,还是键值对类型pair<K,V>

  • 这会导致一个问题:在 insert 的逻辑里计算 hashi 的值,在对“Key 进行取模”时,默认规则无法满足需求
    • 默认规则unordered_set 使用Key进行取模,unordered_set使用std::pair进行取模,
  • unordered_setKey可以完成预期取模操作,而std::pair默认不支持取模,不符合我们仅对Key进行取模的逻辑

为解决这个问题,我们在 unordered_map 和 unordered_set 这两个容器层,分别实现了仿函数 MapKeyofT 和 SetKeyofT ,并将它们传递给哈希桶的KeyOfT模板参数


这样,哈希桶内部就能通过上层容器KeyOfT 仿函数通过 KeyOfT 仿函数取出 T 类型对象中的 key,再进行取模

  • 先从 T 类型对象中提取出 key,再对这个 key 取模,从而实现仅对Key进行取模的逻辑

哈希桶架构设计

  • 模板参数设置为template<class K, class T, class KeyOfT, class HashFunc>,方便通过仿函数KeyOfT取出Key值进行取模计算hashi
// unordered_set -> hash_bucket::HashTable<K, K> _ht;
// unordered_map -> hash_bucket::HashTable<K, std::pair<K, V>> _ht;
// template<class K, class T, class KeyOfT, class HashFunc = DefaultHashFunc<K>>
template<class K, class T, class KeyOfT, class HashFunc>
class HashTable
{typedef struct HashNode<T> Node;// 声明友元template<class K, class T, class Ptr, class Ref, class KeyOfT, class HashFunc>friend struct HTIterator;private:vector<Node*> _table;	// 需要写析构函数size_t _n = 0;public:// ... 迭代器和相关成员函数
};
  • 哈希桶的的迭代器的实现中,迭代器需要访问到哈希桶的私有成员,因此将迭代器声明为哈希桶的友元

SetKeyOfT

// unordered_set.h
template<class K>
class unordered_set
{
private:struct SetKeyOfT{const K& operator()(const K& key) const{return key;}};hash_bucket::HashTable<K, K, SetKeyOfT> _ht;public:// ... 迭代器和相关成员函数
}

MapKeyOfT

// m_unordered_map.h
template<class K, class V>
class unordered_map
{struct MapKeyOfT{const K& operator()(const pair<K, V>& kv) const{return kv.first;}};
private:hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT> _ht;public:// ... 迭代器和相关成员函数
};

传入不同的仿函数实例化哈希桶分析

在这里插入图片描述

使用 萃取器 KeyOfT 取出 Key 的设计相当巧妙

  • unordered_setunordered_map 的设计,_data 分别是 keypair
  • 如果是 unordered_set 中的Key, 可以借助哈希函数直接取模,如果是unordered_set,标准库中的 pair<K, V> 借助哈希函数后依然不能支持仅对Key取模

因此设计了仿函数萃取器 KeyOfT ,用于取出 T 中的 Key 来进行取模

  • 如果是 unordered_set 返回 K,如果是 unordered_set 中的 pair<K, V>取出 K 进行取模

KeyOfT 对哈希桶中 insert 取模的修改

需要修改的地方如下

  • 哈希桶在插入节点时计算hashi的逻辑需要修改
    • 之前哈希桶计算桶的位置时,进行的是套上哈希函数后直接取模
    • 现在需要使用仿函数KeyOfT,提取出类型中的Key再进行取模
  • 例如size_t hashi = hf(curNode->_data) % newSize;修改为size_t hashi = hf(kot(curNode->_data)) % newSize;,仅对关键字Key取模,需要再套上一层仿函数对象
    • kot为定义出的仿函数对象
pair<iterator, bool> Insert(const T& data)
{KeyOfT kot;iterator it = Find(kot(data));if (it != end())return std::make_pair(it, false);HashFunc hf;// 扩容逻辑// 控制负载因子 为 1 时扩容if (static_cast<double> (_n) / static_cast<double> (_table.size()) >= 1.0){size_t newSize = GetNextPrime(_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* curNode = _table[i];while (curNode){Node* curNext = curNode->_next;//size_t hashi = i % newSize;	// 自己写的时候写错的点size_t hashi = hf(kot(curNode->_data)) % newSize;curNode->_next = newTable[hashi];newTable[hashi] = curNode;//curNode = curNode->_next;curNode = curNext;}_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 std::make_pair(iterator(newNode, this), true);
}

三、迭代器与相关重载实现

这里的 iterator 的实现思路与 封装红黑树实现 map 和 set ** 的思路完全一致**:

  • 用一个类封装 “结点指针”
  • 再通过重载运算符,让迭代器能像指针一样完成访问和移动行为(如:*it++itit-> 等行为)

1. 自定义哈希的iterator类

// 提前声明哈希表  告诉编译器, HashTable 类型已有定义, 方便迭代器中定义哈希表的指针
template<class K, class T, class KeyOfT, class HashFunc = DefaultHashFunc<K>>
class HashTable;// 实现迭代器
template<class K, class T, class Ptr, class Ref, class KeyOfT, class HashFunc>
struct HTIterator
{typedef struct HashNode<T> Node;typedef struct HTIterator<K, T, Ptr, Ref, KeyOfT, HashFunc> Self;// 始终为 普通迭代器, 可用于 普通迭代器 构造 const 迭代器typedef struct HTIterator<K, T, T*, T&, KeyOfT, HashFunc> Iterator;Node* _node;	// 当前结点的指针// 保存哈希表的指针 方便操作 ,因为  当前桶走完了,需要走到下一个桶const HashTable<K, T, KeyOfT, HashFunc>* _pht;	  // 当前哈希表的指针HTIterator(Node* node, const HashTable<K, T, KeyOfT, HashFunc>* pht):_node(node),_pht(pht){ }// 由 普通 iterator 构造 const iteratorHTIterator(const Iterator& it)	:_node(it._node), _pht(it._pht){ }
};

哈希桶的迭代器中,实现operator++时:前桶走完了,需要走到下一个桶,因此需要保存当前哈希表的指针方便访问下一个桶

const HashTable<K, T, KeyOfT, HashFunc>* _pht;	  // 当前哈希表的指针
  • 关于哈希桶的指针为什么需要定义为const T*在const迭代器部分的实现处解释

构造函数

  • 初始化结点的指针和哈希表的指针
HTIterator(Node* node, const HashTable<K, T, KeyOfT, HashFunc>* pht):_node(node),_pht(pht)
{ }

由 普通迭代器构造 const 迭代器

typedef struct HTIterator<K, T, Ptr, Ref, KeyOfT, HashFunc> Self;// 始终为 普通迭代器, 可用于 普通迭代器 构造 const 迭代器
typedef struct HTIterator<K, T, T*, T&, KeyOfT, HashFunc> Iterator;// 由 普通 iterator 构造 const iterator
HTIterator(const Iterator& it)	:_node(it._node), _pht(it._pht)
{ }
  • 注意这里:typedef struct HTIterator<K, T, T*, T&, KeyOfT, HashFunc> Iterator;中的模板参数给的是<K, T, T*, T&, KeyOfT, HashFunc>,那么类型Iterator始终为普通迭代器
  • 形参const Iterator& it为普通迭代器的常引用,既可以接受普通迭代器,也可以接收 const 迭代器,该构造函数完成的功能有两个:
    • 由 普通迭代器构造 const 迭代器:当it接收到的实参是 普通迭代器 时,完成由普通迭代器构造 const 迭代器
    • 由 const 迭代器 的拷贝构造:当it接收到的实参是 const 迭代器 时,完成 const 迭代器 的拷贝构造

2. operator*

Ref operator*() const
{if (_node == nullptr)assert(false);return _node->_data;
}
  • 防止访问空结点:访问空结点时assert(false)
  • *it需要访问到结点中的数据,引用返回,因此返回结点中的数据return _node->_data

3. operator->

Ptr operator->() const
{if (_node == nullptr)assert(false);return &(_node->_data);
}
  • 防止访问空结点:访问空结点时assert(false)
  • it->通过指针访问数据的行为,因此返回结点中的地址return &(_node->_data)

4. operator!=

bool operator!=(const Self& s) const
{return _node != s._node;
}

5. operator==

bool operator==(const Self& s) const
{return _node == s._node;
}

6. operator++

  • 前置++
// 单向迭代器,不支持--
Self& operator++()
{// 当前桶没完,就找当前桶的下一个结点位置if (_node->_next){_node = _node->_next;return *this;}// 当前桶结束了,就去下一个桶找else{KeyOfT kot;HashFunc hf;// _table 是 哈希表的 private 成员,迭代器类无法直接访问 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;}// 循环内没有返回,代表走到表尾没找到下一个非空桶// 返回 nullptr 表示 迭代器的 end()_node = nullptr;return *this;}
}

**operator++**实现的是找下一个结点,思路分为两个分支

  • 当前桶中还有下一个结点_node指针移向下一个结点,再返回当前迭代器对象即可

    •   if (_node->_next){_node = _node->_next;return *this;}
      
  • 当前迭代器是当前桶中的最后一个结点:需要去下一个非空桶中找下一个结点

    • 计算当前桶的hashi值: size_t hashi = hf((kot(_node->_data))) % _pht->_table.size();

    • 从下一个桶开始,查找下一个不为空的桶:找到后,_node向后移动,返回当前迭代器对象*this

      •   ++hashi;while (hashi < _pht->_table.size()){// 不为空,就是找到了if (_pht->_table[hashi]){_node = _pht->_table[hashi];return *this;}else++hashi;}
        
      • 从下一个桶开始,查找下一个不为空的桶的过程中,需要访问哈希桶的size,而 _table 是 哈希桶的 private 成员,迭代器类无法直接访问,因此**需要将迭代器类声明为哈希桶的友元**

    • 找不到时返回空

      •   // 循环内没有返回,代表走到表尾没找到下一个非空桶// 返回 nullptr 表示 迭代器的 end()_node = nullptr;return *this;
        

  • 后置++:**复用前置++**实现即可
// 后置 ++
Self operator++(int) 
{Self tmp(*this);++(*this);return tmp;
}

四、哈希桶的iterator以及const_itreator的封装

1. 封装iterator与const_iterator

在哈希桶的层面实现迭代器

template<class K, class T, class KeyOfT, class HashFunc>
class HashTable
{typedef struct HashNode<T> Node;// 友元声明template<class K, class T, class Ptr, class Ref, class KeyOfT, class HashFunc>friend struct HTIterator;private:vector<Node*> _table;	// 需要写析构函数size_t _n = 0;public:// 迭代器typedef struct HTIterator<K, T, T*, T&, KeyOfT, HashFunc> iterator;typedef struct HTIterator<K, T, const T*, const T&, KeyOfT, HashFunc> const_iterator;// ...   
}

在实现迭代器时,通过传不同的模板参数,实例化出不同的迭代器类型,即可控制是普通迭代器还是 const 版本的迭代器

  • typedef struct HTIterator<K, T, T*, T&, KeyOfT, HashFunc> iterator传入普通 T 类型,实例化出普通迭代器
  • typedef struct HTIterator<K, T, const T*, const T&, KeyOfT, HashFunc> const_iterator传入const T类型,实例化出 const迭代器

2. begin 与 const_begin

实现如下

iterator begin()
{for (size_t i = 0; i < _table.size(); ++i){Node* curNode = _table[i];if (curNode)return iterator(_table[i], this);}// 没找到,返回空return iterator(nullptr, this);
}
const_iterator begin() const
{for (size_t i = 0; i < _table.size(); ++i){Node* curNode = _table[i];if (curNode)return iterator(_table[i], this);}// 没找到,返回空return const_iterator(nullptr, this);
}
  • begin()返回第一个非空桶中的第一个节点遍历哈希表,找第一个非空桶中的第一个节点
  • 最终返回一个迭代器对象,用当前结点的指针和当前哈希表的指针this构造迭代器对象返回
    • 普通对象调用普通begin(),返回iterator对象
    • const对象调用const begin(),返回const_iterator对象
  • 找不到时,nullptr和当前哈希表的指针this构造迭代器对象返回return iterator(nullptr, this)

3. end 与 const_end

iterator end()
{return iterator(nullptr, this);
}const_iterator end() const
{return const_iterator(nullptr, this);
}
  • end()返回最后一个位置的下一个位置,这里我们用空结点来表示
  • 最终返回一个迭代器对象,nullptr和当前哈希表的指针this构造空迭代器对象返回
    • 普通对象调用普通end(),返回iterator对象
    • const对象调用const end(),返回const_iterator对象

4. 迭代器中哈希桶的指针为什么定义为const T*

迭代器中哈希桶的指针为什么定义为const T*需要结合哈希桶中设计的const_begin和const_end接口说起

// 哈希桶中的 const_begin 和 const_end 接口
const_iterator begin() const
{for (size_t i = 0; i < _table.size(); ++i){Node* curNode = _table[i];if (curNode)return iterator(_table[i], this);}// 没找到,返回空return const_iterator(nullptr, this);
}const_iterator end() const
{return const_iterator(nullptr, this);
}
// 迭代器中的 哈希表指针
const HashTable<K, T, KeyOfT, HashFunc>* _pht;	  // 当前哈希表的指针

在这里插入图片描述


五、实现Key不支持修改与容器的迭代器

  • unordered_map 和 unordered_set 的迭代器的实现,本质是对哈希桶迭代器的适当封装,我们对其进行封装即可
    • unordered_map 和 unordered_set 的普通迭代器,本质是对哈希桶中 普通迭代器 的适当封装
    • unordered_map 和 unordered_set 的 const 迭代器,本质是对哈希桶中 const 迭代器 的适当封装

1. unordered_set 借助 const 迭代器实现 Key 不支持修改

template<class K>
class unordered_set
{
public:typedef typename hash_bucket::HashTable<K, K, SetKeyOfT>::const_iterator iterator;typedef typename hash_bucket::HashTable<K, K, SetKeyOfT>::const_iterator const_iterator;const_iterator begin() const{return _ht.begin();}const_iterator end() const{return _ht.end();}
};

unordered_set 的普通迭代器和const迭代器都是 const_iterator,因此只需提供 const 版本的begin和end函数即可

  • const版本的begin和end函数,普通对象和const对象都可以调用
typedef typename hash_bucket::HashTable<K, K, SetKeyOfT>::const_iterator iterator;
typedef typename hash_bucket::HashTable<K, K, SetKeyOfT>::const_iterator const_iterator;
// 实现以下版本等同,因为 iterator 与 const_iterator 为同一个类型
iterator begin() const { return _ht.begin(); }
iterator end() const { return _ht.end(); }

2. unordered_map 实现 Key 不支持修改

template<class K, class V>
class unordered_map
{
private:hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT> _ht;		// 将 pair 中的 K 设置为 const K 类型
public:// ...
};
  • unordered_map 实现的是 key 不能修改, value 能修改,我们可以pair<key, value> 的存储层解决这个问题,存储时,直接存储key为常量的pair<const K, V>
  • 存储的哈希桶实现为:hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT> _ht;即可实现unordered_mapkey 不能修改, value 能修改

3. 封装 unordered_map 的迭代器

  • unordered_map 和 unordered_set 的普通迭代器,本质是对哈希桶中 普通迭代器 的适当封装
  • unordered_map 和 unordered_set 的 const 迭代器,本质是对哈希桶中 const 迭代器 的适当封装
template<class K, class V>
class unordered_map
{
private:hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT> _ht;
public:// ... // key 不能修改, value 能修改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;// ... begin 和 end 等函数
};
  • 实现unordered_mapkey 不能修改, value 能修改是在pair存储层增加了const来解决这个问题,因此beginend常规实现即可

    • 普通 unordered_map 对象,调用哈希桶的普通迭代器
    • const unordered_map 对象,调用哈希桶的 const 迭代器
  • unordered_map封装哈希桶的迭代器

    • 将哈希桶中的普通迭代器封装为 unordered_map的普通迭代器
      • typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT>::iterator iterator;
    • 将哈希桶中的 const迭代器封装为 unordered_map的const迭代器
      • typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT>::const_iterator const_iterator;

begin与end函数

  • unordered_mapKey 不能修改是在 pair 的存储层实现的,因此需要分别实现 begin 和 end 函数的普通版本const对象版本
iterator begin()				// 普通 unordered_map 对象,调用树的普通迭代器
{return _ht.begin();
}
iterator end()
{return _ht.end();
}const_iterator begin() const	// const unordered_map 对象,调用树的普通迭代器
{return _ht.begin();
}
const_iterator end() const
{return _ht.end();
}

六、改造insert实现operator[]

1. operator[] 的介绍

在这里插入图片描述

unordered_map中,operator[] 可以实现插入 + 修改的功能。既然有插入,那么要用到 insert 接口。在 STL 库中,insert 的接口的返回值是一个 pair 类型

  • 如果插入成功,返回 <插入的该数据对应的迭代器, true>
  • 如果插入失败,返回 <已经存在的该数据对应的迭代器, false>

2. operator[]和insert行为的哲学

unordered_mapoperator[]的行为等效如下:

mapped_type& operator[] (const key_type& k);	// 参考文档中的声明
pair<iterator,bool> insert (const value_type& val);		// 参考文档中 insert 的声明// operator[] 的行为等效如下
(*((this->insert(make_pair(mapped_type()))).first))
(*((this->insert()).first))		// 将 make_pair 函数去掉后如下
// 对以上代码做拆解,可得如下结果
(*((insert()).first))	// 这里本质是对 insert 返回的 pair 中的 first 进行解引用// 由insert的返回值为 pair ,将insert替换为pair,可以得到 以下结果
(* ( (pair<iterator,bool>).first) )	

insert之所以这么设计,就是为了实现 operator[]时可以复用insert

  • 因此我们需要将哈希桶中 insert 的返回值修改为返回 pair

3.哈希桶中 insert 函数的修改

  • 由于 pairreturn 时构造,因此仅需修改return语句中构造 pair 的逻辑
pair<iterator, bool> Insert(const T& data)
{KeyOfT kot;iterator it = Find(kot(data));if (it != end())return std::make_pair(it, false);HashFunc hf;// 扩容逻辑// 控制负载因子 为 1 时扩容if (static_cast<double> (_n) / static_cast<double> (_table.size()) >= 1.0){size_t newSize = GetNextPrime(_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* curNode = _table[i];while (curNode){Node* curNext = curNode->_next;//size_t hashi = i % newSize;	// 自己写的时候写错的点size_t hashi = hf(kot(curNode->_data)) % newSize;curNode->_next = newTable[hashi];newTable[hashi] = curNode;//curNode = curNode->_next;curNode = curNext;}_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 std::make_pair(iterator(newNode, this), true);//return pair<iterator, bool>(newNode, true);
}

4. unordered_map中 operator[] 的实现

  • unordered_map中的 insert 实现
pair<iterator, bool> insert(const pair<K, V>& _kv)
{return _ht.Insert(_kv);
}
  • unordered_map 中的 operator[] 实现
V& operator[](const K& key)
{pair<iterator, bool> ret = insert(std::make_pair(key, V()));return (ret.first)->second;
}

operator[]的实现:

  • 先根据 KeyValue 插入pair,再返回pairValue的引用

4. unordered_set 中 insert 的实现

实现 insert

  • 由于 unordered_set 中 只有 const_iterator,因此修改了哈希桶的 insert 函数后,需要将 unordered_set 中 insert 函数的返回值和哈希桶中 insert 的返回值做对应
//pair<iterator, bool> insert(const K& key)
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);//return _ht.Insert(key); // 返回的是 pair<iterator, bool>,这里的 iterator 是 哈希表中的 普通 iterator// set 中的 pair<iterator, bool> 本质是 pair<const_iterator, bool>// 两个 pair 实例化成了 不同的类型,不能互相转换
}

在这里插入图片描述


由普通迭代器构造 const 迭代器 详解

typedef struct HTIterator<K, T, Ptr, Ref, KeyOfT, HashFunc> Self;// 始终为 普通迭代器, 可用于 普通迭代器 构造 const 迭代器
typedef struct HTIterator<K, T, T*, T&, KeyOfT, HashFunc> Iterator;// 由 普通 iterator 构造 const iterator
HTIterator(const Iterator& it)	// 该函数 可以不写:_node(it._node), _pht(it._pht)
{ }
  • typedef struct HTIterator<K, T, T*, T&, KeyOfT, HashFunc> Iterator:不论HTIterator被实例化成什么类型,Iterator始终为普通迭代器类型。函数HTIterator(const Iterator& it)形参类型普通迭代器的 const 引用
    • HTIterator被实例化成普通迭代器时,此时该函数HTIterator(const Iterator& it)充当HTIterator类的拷贝构造函数
    • HTIterator被实例化成const迭代器时,此时该函数HTIterator(const Iterator& it))充当HTIterator类的构造函数可以由普通迭代器构造 const 迭代器

七、完整代码实现

m_unordered_set

#include "HashTable.h"namespace m_unordered_set
{template<class K>class unordered_set{private:struct SetKeyOfT{const K& operator()(const K& key) const{return key;}};hash_bucket::HashTable<K, K, SetKeyOfT> _ht;public:typedef typename hash_bucket::HashTable<K, K, SetKeyOfT>::const_iterator iterator;typedef typename hash_bucket::HashTable<K, K, SetKeyOfT>::const_iterator const_iterator;const_iterator begin() const{return _ht.begin();}const_iterator end() const{return _ht.end();}//pair<iterator, bool> insert(const K& key)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);// 返回的是 pair<iterator, bool>,这里的 iterator 是 哈希表中的 普通 iterator//return _ht.Insert(key);		// set 中的 pair<iterator, bool> 本质是 pair<const_iterator, bool>// 两个 pair 实例化成了 不同的类型,不能互相转换}//pair<iterator, bool> insert(const K& key)//{//	// 这里树的 insert 返回的 iterator 是树里 的 普通iterator//	// 但是 set 中 insert 的返回值 pair 中的迭代器 是 const_iterator//	// 解决方法,先接收 树里的 普通迭代器,再用 普通迭代器构造 const_迭代器 返回//	pair<typename RBTree<K, K, SetKeyOfT>::iterator, bool> retPair = _tree.insert(key);//	// 下面用 普通迭代器去构造 const 迭代器 并 返回//	return pair<const_iterator, bool>(retPair.first, retPair.second);//}};
}

m_unordered_map

#pragma once
#include "HashTable.h"namespace m_unordered_map
{template<class K, class V>class unordered_map{struct MapKeyOfT{const K& operator()(const pair<K, V>& kv) const{return kv.first;}};private:hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT> _ht;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 = insert(std::make_pair(key, V()));return (ret.first)->second;}};
}

改造后的哈希桶

namespace hash_bucket
{// 使用仿函数控制 string 和 其他整型的取模template<class K>struct DefaultHashFunc{size_t operator()(const K& key){return static_cast<size_t> (key);}};//// 方式一: 专门为 string 写一个 哈希函数,使用第一个 char 控制//struct StringHashFunc//{//	size_t operator()(const string& str)//	{//		return static_cast<size_t> (str[0]);//	}//};// 为 string 特化一个版本 哈希函数template <>struct DefaultHashFunc<string>{size_t operator()(const string& str){// BKDRsize_t hash = 0;for (auto ch : str) {hash *= 131;hash += ch;}return hash;}};//template<class K, class V>template<class T>struct HashNode{T _data;struct HashNode<T>* _next;HashNode(const T& data):_data(data),_next(nullptr){ }};// 提前声明哈希表  告诉编译器, HashTable 类型已有定义//template<class K, class T, class KeyOfT, class HashFunc>template<class K, class T, class KeyOfT, class HashFunc = DefaultHashFunc<K>>class HashTable;// 1. 改造哈希表  KeyOfT// 2. 封装   MapKeyOfT  SetKeyOfT// 3. 普通迭代器     operator++   operator...// 4. const 迭代器		通过模板参数控制// 5. 实现 key 不能修改的问题// 5. insert 返回值        由普通迭代器构造 const 迭代器// 实现迭代器// 为了方便操作,存了一个哈希表的指针template<class K, class T, class Ptr, class Ref, class KeyOfT, class HashFunc>struct HTIterator{typedef struct HashNode<T> Node;typedef struct HTIterator<K, T, Ptr, Ref, KeyOfT, HashFunc> Self;// 始终为 普通迭代器, 可用于 普通迭代器 构造 const 迭代器typedef struct HTIterator<K, T, T*, T&, KeyOfT, HashFunc> Iterator;Node* _node;	// 当前结点的指针//HashTable<K, T, KeyOfT, HashFunc>* _pht;// 需要哈希表的指针  是因为  当前桶走完了,需要走到下一个桶const HashTable<K, T, KeyOfT, HashFunc>* _pht;	  // 当前哈希表的指针HTIterator(Node* node, const HashTable<K, T, KeyOfT, HashFunc>* pht):_node(node),_pht(pht){ }// 由 普通 iterator 构造 const iteratorHTIterator(const Iterator& it)	// 该函数 可以不写:_node(it._node), _pht(it._pht){ }//HTIterator(Node* node, HashTable<K, T, KeyOfT, HashFunc>* pht)	// 该函数 可以不写//	:_node(node)//	, _pht(pht)//{ }Ref operator*() const{if (_node == nullptr)assert(false);return _node->_data;}Ptr operator->() const{if (_node == nullptr)assert(false);return &(_node->_data);}bool operator!=(const Self& s) const{return _node != s._node;}bool operator==(const Self& s) const{return _node == s._node;}// 单向迭代器,不支持--Self& operator++(){// 当前桶没完,就找当前桶的位置if (_node->_next){_node = _node->_next;return *this;}// 当前桶结束了,就去下一个桶找else{KeyOfT kot;HashFunc hf;// _table 是 哈希表的 private 成员,迭代器类无法直接访问 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;}// 循环内没有返回,代表走到表尾没找到下一个非空桶// 返回 nullptr 表示 迭代器的 end()_node = nullptr;return *this;}}// 后置 ++Self operator++(int) {Self tmp(*this);++(*this);return tmp;}};// set -> hash_bucket::HashTable<K, K> _ht;// map -> hash_bucket::HashTable<K, std::pair<K, V>> _ht;//template<class K, class T, class KeyOfT, class HashFunc = DefaultHashFunc<K>>template<class K, class T, class KeyOfT, class HashFunc>class HashTable{//typedef struct HashNode<K, V> Node;typedef struct HashNode<T> Node;template<class K, class T, class Ptr, class Ref, class KeyOfT, class HashFunc>friend struct HTIterator;private:vector<Node*> _table;	// 需要写析构函数size_t _n = 0;public:typedef struct HTIterator<K, T, T*, T&, KeyOfT, HashFunc> iterator;typedef struct HTIterator<K, T, const T*, const T&, KeyOfT, HashFunc> const_iterator;iterator begin(){for (size_t i = 0; i < _table.size(); ++i){Node* curNode = _table[i];if (curNode)return iterator(_table[i], 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* curNode = _table[i];if (curNode)return iterator(_table[i], this);}// 没找到,返回空return const_iterator(nullptr, this);}const_iterator end() const{return const_iterator(nullptr, this);}HashTable(){_table.resize(11, nullptr);}// 需要手动析构桶中的节点~HashTable(){for (size_t i = 0; i < _table.size(); i++){Node* curNode = _table[i];while (curNode){Node* curNext = curNode->_next;delete curNode;curNode = curNext;}_table[i] = nullptr;}}inline size_t GetNextPrime(size_t prime){const int PRIMECOUNT = 28;static const size_t primeList[PRIMECOUNT] ={53ul, 97ul, 193ul, 389ul, 769ul, 1543ul, 3079ul, 6151ul, 12289ul, 24593ul, 49157ul, 98317ul, 196613ul, 393241ul, 786433ul, 1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul, 50331653ul, 100663319ul, 201326611ul, 402653189ul,805306457ul, 1610612741ul, 3221225473ul, 4294967291ul};size_t i = 0;for (; i < PRIMECOUNT; ++i){if (primeList[i] > prime)return primeList[i];}return primeList[i];}pair<iterator, bool> Insert(const T& data){KeyOfT kot;iterator it = Find(kot(data));if (it != end())return std::make_pair(it, false);HashFunc hf;// 扩容逻辑// 控制负载因子 为 1 时扩容if (static_cast<double> (_n) / static_cast<double> (_table.size()) >= 1.0){size_t newSize = GetNextPrime(_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* curNode = _table[i];while (curNode){Node* curNext = curNode->_next;//size_t hashi = i % newSize;	// 自己写的时候写错的点size_t hashi = hf(kot(curNode->_data)) % newSize;curNode->_next = newTable[hashi];newTable[hashi] = curNode;//curNode = curNode->_next;curNode = curNext;}_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 std::make_pair(iterator(newNode, this), true);//return pair<iterator, bool>(newNode, true);}iterator Find(const K& key) const{KeyOfT kot;HashFunc hf;size_t hashi = hf(key) % _table.size();Node* curNode = _table[hashi];while (curNode){if (kot(curNode->_data) == key)return iterator(curNode, this);curNode = curNode->_next;}return iterator(nullptr, this);}// 单链表的删除需要更改前一个结点的 next 指针,不适合复用 findbool Erase(const K& key){KeyOfT kot;HashFunc hf;size_t hashi = hf(key) % _table.size();Node* curPrev = nullptr;Node* curNode = _table[hashi];while (curNode){if (kot(curNode->_data) == key){if (curPrev == nullptr) // 头删_table[hashi] = curNode->_next;	else  // 非头删curPrev->_next = curNode->_next;delete curNode;--_n;return true;}curPrev = curNode;curNode = curNode->_next;}return false;}void Print(){for (size_t i = 0; i < _table.size(); ++i){printf("[%d]->", i);Node* curNode = _table[i];while (curNode){cout << curNode->_data << ":" << curNode->_data << "->";curNode = curNode->_next;}printf("NULL\n");}cout << endl;}};
}

八、结语

至此,我们完成了 封装哈希桶实现 unordered_mapunordered_set 的全过程。
从哈希函数、节点结构、模板参数设计到迭代器与接口封装,每一个环节都体现了 STL 设计中“泛型 + 仿函数 + 迭代器”三者的协同思想。

回顾整个系列:

  • map / set —— 基于红黑树的有序容器,通过中序遍历保证顺序;
  • unordered_map / unordered_set —— 基于哈希表的无序容器,通过哈希分桶实现高效查找。

通过对两种容器的完整封装,我们不只是复现了 STL,更重要的是掌握了其底层抽象思想与通用设计模式
这些知识将在你编写任何通用数据结构、网络缓存、符号表或数据库索引模块时,发挥巨大价值。


以上就是本文的所有内容了,如果觉得文章对你有帮助,欢迎 点赞⭐收藏 支持!如有疑问或建议,请在评论区留言交流,我们一起进步

分享到此结束啦
一键三连,好运连连!

你的每一次互动,都是对作者最大的鼓励!


征程尚未结束,让我们在广阔的世界里继续前行! 🚀

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

相关文章:

  • 多语言网站建设应注意哪些事项做沙盘实训在哪个网站做
  • 沈阳专业制作网站东莞家具网站建设
  • 制造业营销外贸网站建设手机网站建设 技术规范
  • C# var 关键字详解:从入门到精通
  • 使用 SQLAlchemy 连接数据库:从基础到最佳实践
  • 如何使用Profiler进行内存分析?
  • 12306网站开发携程网站建设计划管理与进度控制
  • 淮南电商网站建设价格新校区建设网站管理规定
  • 进入新岗位的第一课——潜龙勿用
  • DeepSeek辅助编写转换DuckDB explain_analyze_json 格式执行计划到postgresql_plan 的程序
  • 旅游网站网页设计图片网络营销和网络销售的区别
  • STM32H743-ARM例程41-FMC_INDEP
  • 网站怎么申请百度小程序室内设计师网上培训班
  • 【Java 并发编程】线程创建 6 种方式:Thread/Runnable/Callable 核心类解析+线程池使用说明
  • 第四课:时序逻辑进阶 - 有限状态机(FSM)设计
  • Unicode全字符集加解密工具 - 强大的编码转换GUI应用
  • 网站管理和维护设计师学编程能自己做网站吗
  • PyInstaller 工具使用文档及打包教程
  • 怎么建商业网站外国广告公司网站
  • USB Gadget 技术
  • 常州小型网站建设北京电商网站开发公司哪家好
  • 1108秋招随记
  • 做自己视频教程的网站wordpress去除谷歌
  • 咋把网站制作成软件建设网站需要注意什么手续
  • 线程4.2
  • SOAR:利用状态空间模型和可编程梯度进行航空影像中小目标物体检测的进展
  • 开一个网站需要多少钱网站开发工作量评估
  • [SPSS] SPSS数据的保存
  • Verilog中+:和 -:
  • 清理空壳网站做网站的程序员工资大约月薪