【C++】哈希表封装 unordered_map 和 unordered_set 的实现过程

【C++】哈希表封装 unordered_map 和 unordered_set 的实现过程
在这里插入图片描述
C++语法相关知识点可以通过点击以下链接进行学习一起加油!
命名空间缺省参数与函数重载C++相关特性类和对象-上篇类和对象-中篇
类和对象-下篇日期类C/C++内存管理模板初阶String使用
String模拟实现Vector使用及其模拟实现List使用及其模拟实现容器适配器Stack与QueuePriority Queue与仿函数
模板进阶-模板特化面向对象三大特性-继承机制面向对象三大特性-多态机制STL 树形结构容器二叉搜索树
AVL树红黑树红黑树封装map/set哈希-开篇闭散列-模拟实现哈希
哈希桶-模拟实现哈希
本篇将讲述如何通过哈希表封装 unordered_mapunordered_set 容器。在开始本章内容之前,建议先阅读红黑树封装 mapset一文,以便更好地理解编译器如何区分 unordered_mapunordered_set 并调用底层哈希表。
请添加图片描述


Alt


🌈个人主页:是店小二呀
🌈C语言专栏:C语言
🌈C++专栏: C++
🌈初阶数据结构专栏: 初阶数据结构
🌈高阶数据结构专栏: 高阶数据结构
🌈Linux专栏: Linux

🌈喜欢的诗句:无人扶我青云志 我自踏雪至山巅

请添加图片描述


文章目录

这里哈希表将采用哈希桶的方式解决哈希冲突且实现哈希表结构,那么我们需要对哈希表节点类型进行处理,同时设计编译器去识别unordered_map/set的逻辑。

一、节点类型

如果我们想使用哈希表封装unordered_map/set容器,就需要考虑泛型编程,将节点类型统一成T类型

template<classT>structHashNode{ T _data; HashNode<T>* _next;HashNode(const T& data):_data(data),_next(nullptr){}};

二、区分unordered_map/set容器

编译器是无非确定你需要啥容器**,这里我们可以通过外部输入数据,使得编译器明确你需要啥容器,该容器都有一套属于返回K类型的仿函数**,只要unordered_set是否会多余,这里可以参考红黑树封装map/set文章,这里是为了跟map构成模板,陪太子读书。

在这里插入图片描述

既然我们希望可以通过手动输入数据,得到预计效果,那么关于HashFunc函数也是期望从外部调用,这里可以写到外面来。(图片写错了,这里看下面的)

template<class K,class Hash = HashFunc > and template<class K,class T,class Hash = HashFunc>

三、相关文件修改后

3.1 HashTable.h

#pragmaonce#include<iostream>usingnamespace std;#include<string>#include<vector>template<classK>structHashFunc{ size_t operator()(const K& key){return(size_t)key;}};template<>structHashFunc<string>{ size_t operator()(const string& key){ size_t hash =0;for(auto ch : key){ hash *=131; hash += ch;}return hash;}};namespace hash_bucket {template<classT>structHashNode{ T _data; HashNode<T>* _next;HashNode(const T& data):_data(data),_next(nullptr){}};template<classK,classT,classKeyOfT,classHash>classHashTable{public:typedef HashNode<T> Node;HashTable(){ _tables.resize(10,nullptr);}~HashTable(){for(size_t i =0; i < _tables.size(); i++){ Node* cur = _tables[i];while(cur){ Node* next = cur->_next;delete cur; cur = next;}}}boolInsert(const T& data){ Hash hs; KeyOfT kot;if(Find(kot(data)){returnfalse;}if(_n == _tables.size()){ vector<Node*>NewTable(n *10,nullptr);for(size_t i =0; i < _tables.size(); i++){ Node* cur = _tables[i];while(cur){ Node* next = cur->_next;// 头插新表的位置 size_t hashi =hs(kot(cur->_data))% NewTable.size(); cur->_next = NewTable[hashi]; NewTable[hashi]= cur; cur = cur->_next;} _tables[i]=nullptr;} _tables.swap(NewTable);} size_t hashi =hs(kot(data))% _tables.size();//进行头插操作 Node* newnode =newNode(data); _tables[hashi]->_next = newnode; _tables[hashi]= newnode;++_n;returntrue;} Node*Find(const K& key){ Hash hs; KeyOfT kot; size_t hashi =hs(key)% _tables.size(); Node* cur = _tables[hashi];while(cur){if(kot(cur->_data)== key){return&kot(cur->_data);}else{ cur = cur->_next;}}returnnullptr;}boolErase(const K& key){ Hash hs; size_t hashi =hs(key)% _tables.size(); Node* cur = _tables[hashi]; Node* prev =nullptr;while(cur){if(kot(cur->_data)== key){if(prev ==nullptr){ _tables[hashi]= cur->_next;}else{ prev->_next = cur->_next;}delete cur;returntrue;}else{ prev = cur; cur = cur->_next;}}returnfalse;}private: vector<Node*> _tables; size_t _n =0;};}
这里关于HashFunc()需要放在哈希命名空间外部,因为在其他头文件需要使用到该HashFunc()

3.2 unoderded_set.h

namespace unoderded_set {template<classK,classHash= HashFunc<K>>classunoderded_set{structSetKeyOfT{const K&operator()(const K& key){return key;}};private: hash_bucket::HashTable<K,const K, SetKeyOfT, Hash> _ht;};}

3.3 unoderded_map.h

namespace unoderded_map {template<classK,classV,classHash= HashFunc<K>>classunoderded_map{structSetKeyOfT{const K&operator()(const pair<K,V>& key){return key.first;}};public:private: hash_bucket::HashTable<K,const pair<K,V>, MapKeyOfT, Hash> _ht;};}

四、实现迭代器

实现哈希表中迭代器跟之前其他容器实现迭代器需要考虑的差不多,主要是对于operator++和operator–需要考虑不同容器的结构进行行为重定义。同时需要设计为模板template<class T,class Ptr, class Ref>去匹配const类型和非const类型调用同一个迭代器。
typedef __HTIterator<T*, T&> iterator;typedef __HTIterator<const T*,const T&> const_iterator;
这里得到当前_node的位置,是浅拷贝。这不希望改变实参,而是希望通过该实参位置进行操作,得到预期数据,在下一次进行操作时还是之前位置。

4.1 迭代器常见功能

template<classT,classPtr,classRef>struct_HTIterator{//得到一个节点typedef HashNode<T> Node;typedef _HTIterator Self; Node* _node;//这里希望是浅拷贝_HTIterator(const Node* node):_node(node){} Ptr operator->(){return&_node->_data;} Ref operator*(){return _node->_data;}booloperator!=(const Self& s){return _node != s._node;}};

4.2 operator++实现

大体思路】:在第一个不为空位置,向下遍历++直到_node-> _next ==nullptr说明该位置迭代器达到悬挂着哈希桶最后一个不为空的桶,那么需要通过当前节点数值,进行除留余数法得到当前表位置下标,以该下标为基准重新在表中找到下一个不为空的位置,直到i == tables.size()说明遍历完成。

4.3 编译器扫描顺序问题

目前关于迭代器实现某方面功能需要借助HashTable类的类型及其成员,但是问题在于HashTable需要将迭代器设为"武器",将迭代器类功能具体实现放在HashTabl类上方,编译器扫描是上到下扫描,这就导致了这两个类总有一方无法得到另外一份的东西。

在这里插入图片描述

4.3.1 【第一个办法】:声明及其友元

我们可以在迭代器类中声明HashTable类作为类型定义一个哈希表指针,同时在HashTable类型设置迭代器友元,使之迭代器可以通过定义的哈希表指针访问到哈希表。

template<classK,classT,classPtr,classRef,classKeyOfT,classHash>struct_HTIterator{//前置声明template<classK,classT,classKeyOfT,classHash>classHashTable;}template<classK,classT,classKeyOfT,classHash>classHashTable{public:template<classK,classT,classPtr,classRef,classKeyOfT,classHash>friendstruct_HTIterator;}

前置声明

前置声明仅仅告诉编译器有一个 HashTable 类,但由于没有给出该类的完整定义和实现,编译器并不知道如何去使用它。也就是说,你不能在代码中使用 HashTable 类的功能,比如创建对象、调用其成员函数、访问数据成员等,除非你提供了类的完整定义。

4.3.2 【第二办法】:内部类

template<classK,classT,classKeyOfT,classHash>classHashTable{typedef HashNode<T> Node;public:// 友元声明/*template<class K, class T, class KeyOfT, class Hash> friend struct __HTIterator;*/// 内部类template<classPtr,classRef>struct__HTIterator{typedef HashNode<T> Node;typedef __HTIterator Self;}}
这里就采用第一种方法(选择哪个方法都是差不多的)

4.4 operator++内部逻辑

Self&operator++(){ KeyOfT kot; Hash hs;if( _node->_next ){ _node = _node->_next;}else{//访问该成员 size_t i =hs(kot(_node->_data))% _pht->_tables.size(); i++;for(; i < _pht->_tables.size(); i++){if(_pht->_tables[i]){break;}}if(i == _pht->tables.size()){ _node =nullptr;}else{ _node = _pht->_tables[i];}}}
由于在库中unoderded_map和unoderded_set只有单向迭代器,就没有operator–。如果有兴趣可以将单链表设计为双向链表,然后根据operator++逻辑,设计出一个operator–接口

4.5 获得begin和end迭代器位置

在这里插入图片描述
iterator begin(){for(size_t i =0; i < _tables.size(); i++){ Node* cur = _tables[i];if(cur){returniterator(cur,this);}}} iterator end(){returniterator(nullptr,this);} const_iterator begin()const{for(size_t i =0; i < _tables.size(); i++){ Node* cur = _tables[i];if(cur){returniterator(cur,this);}}} const_iterator end()const{returniterator(nullptr,this);}
这里就是通过哈希表begin和end接口,构造相关需求的迭代器。这里很好体现了每个不同对象的迭代器指向表是自己对象的哈希表。

4.6 迭代器调用流程图

在这里插入图片描述
unoderded_map及其unoderded_set迭代器配置,由于unoderded_map支持修改元素,那么没有必要写const系列。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

五、unoderded_map/set相关接口实现

5.1 unoderded_map相关接口

namespace unoderded_map {template<classK,classV,classHash= HashFunc<K>>classunoderded_map{structMapKeyOfT{const K&operator()(const pair<K, V>& key){return key.first;}};typedeftypenamehash_bucket::HashTable<K,const pair<K, V>, MapKeyOfT, Hash>::iterator iterator; iterator begin(){return _ht.begin();} iterator end(){return _ht.end();} V&operator[](const K& key){ pair<iterator,bool> ret =insert(make_pair(key,V()));return ret.first->second;} pair<iterator,bool>insert(const pair<K, V>& kv){return _ht.Insert(kv);}private: hash_bucket::HashTable<K,const pair<K, V>, MapKeyOfT, Hash> _ht;};}

unoderded_map增添接口】:V& operator[](const K& key)pair<iterator, bool> insert(const pair<K, V>& kv)

5.2 unoderded_set相关接口

#pragmaonce#include"HashTable.h"namespace unoderded_set {template<classK,classHash= HashFunc<K>>classunoderded_set{structSetKeyOfT{const K&operator()(const K& key){return key;}};typedeftypenamehash_bucket::HashTable<K,const K, SetKeyOfT,Hash>::iterator iterator;typedeftypenamehash_bucket::HashTable<K,const K, SetKeyOfT, Hash>::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 K& key){return _ht.Insert(key);} iterator find(const K& key){return _ht.Find(key);}boolerase(const K& key){return _ht.Erase(key);}private: hash_bucket::HashTable<K,const K, SetKeyOfT, Hash> _ht;};}

unoderded_set增添接口 pair<iterator, bool> insert(const K& key) iterator find(const K& key)及其bool erase(const K& key)

5.3 HashTable相关接口调整

iterator Find(const K& key){ Hash hs; KeyOfT kot; size_t hashi =hs(key)% _tables.size(); Node* cur = _tables[hashi];while(cur){if(kot(cur->_data)== key){returniterator(cur,this);}else{ cur = cur->_next;}}returnend();} pair<iterator,bool>Insert(const T& data){ Hash hs; KeyOfT kot; iterator it =Find(kot(data));if(it !=end())returnmake_pair(it,false);if(_n == _tables.size()){ vector<Node*>NewTable(_n *10,nullptr);for(size_t i =0; i < _tables.size(); i++){ Node* cur = _tables[i];while(cur){ Node* next = cur->_next;// 头插新表的位置 size_t hashi =hs(kot(cur->_data))% NewTable.size(); cur->_next = NewTable[hashi]; NewTable[hashi]= cur; cur = cur->_next;} _tables[i]=nullptr;} _tables.swap(NewTable);} size_t hashi =hs(kot(data))% _tables.size();//进行头插操作 Node* newnode =newNode(data); _tables[hashi]->_next = newnode; _tables[hashi]= newnode;++_n;returnmake_pair(iterator(newnode,this),true);}
unodered_map当中operator[]可以看作insert和find融合版本,不需要单独设计Find(),但是内部还是有调用Find()逻辑

HashTable.h

#pragmaonce#include<iostream>usingnamespace std;#include<string>#include<vector>template<classK>structHashFunc{ size_t operator()(const K& key){return(size_t)key;}};template<>structHashFunc<string>{ size_t operator()(const string& key){ size_t hash =0;for(auto ch : key){ hash *=131; hash += ch;}return hash;}};namespace hash_bucket {template<classT>structHashNode{ T _data; HashNode<T>* _next;HashNode(const T& data):_data(data),_next(nullptr){}};template<classK,classT,classPtr,classRef,classKeyOfT,classHash>struct_HTIterator{//前置声明template<classK,classT,classKeyOfT,classHash>classHashTable;//得到一个节点typedef HashNode<T> Node;typedef _HTIterator Self; Node* _node;const HashTable* _pht;//这里希望是浅拷贝//这里对象-->指向一个哈希表-->cur及其this。_HTIterator(const Node* node,const HashTable* pht):_node(node),_pht(pht){} Ptr operator->(){return&_node->_data;} Ref operator*(){return _node->_data;}booloperator!=(const Self& s){return _node != s._node;} Self&operator++(){ KeyOfT kot; Hash hs;if( _node->_next ){ _node = _node->_next;}else{//访问该成员 size_t i =hs(kot(_node->_data))% _pht->_tables.size(); i++;for(; i < _pht->_tables.size(); i++){if(_pht->_tables[i]){break;}}if(i == _pht->tables.size()){ _node =nullptr;}else{ _node = _pht->_tables[i];}}}};template<classK,classT,classKeyOfT,classHash>classHashTable{public:template<classK,classT,classPtr,classRef,classKeyOfT,classHash>friendstruct_HTIterator;typedef _HTIterator< K, T, T*, T&, KeyOfT, Hash> iterator;typedef _HTIterator<K, T,const T*,const T&, KeyOfT, Hash> const_iterator;typedef HashNode<T> Node;HashTable(){ _tables.resize(10,nullptr);}~HashTable(){for(size_t i =0; i < _tables.size(); i++){ Node* cur = _tables[i];while(cur){ Node* next = cur->_next;delete cur; cur = next;}}} iterator Find(const K& key){ Hash hs; KeyOfT kot; size_t hashi =hs(key)% _tables.size(); Node* cur = _tables[hashi];while(cur){if(kot(cur->_data)== key){returniterator(cur,this);}else{ cur = cur->_next;}}returnend();} pair<iterator,bool>Insert(const T& data){ Hash hs; KeyOfT kot; iterator it =Find(kot(data));if(it !=end())returnmake_pair(it,false);if(_n == _tables.size()){ vector<Node*>NewTable(_n *10,nullptr);for(size_t i =0; i < _tables.size(); i++){ Node* cur = _tables[i];while(cur){ Node* next = cur->_next;// 头插新表的位置 size_t hashi =hs(kot(cur->_data))% NewTable.size(); cur->_next = NewTable[hashi]; NewTable[hashi]= cur; cur = cur->_next;} _tables[i]=nullptr;} _tables.swap(NewTable);} size_t hashi =hs(kot(data))% _tables.size();//进行头插操作 Node* newnode =newNode(data); _tables[hashi]->_next = newnode; _tables[hashi]= newnode;++_n;returnmake_pair(iterator(newnode,this),true);} iterator begin(){for(size_t i =0; i < _tables.size(); i++){ Node* cur = _tables[i];if(cur){returniterator(cur,this);}}} iterator end(){returniterator(nullptr,this);} const_iterator begin()const{for(size_t i =0; i < _tables.size(); i++){ Node* cur = _tables[i];if(cur){returniterator(cur,this);}}} const_iterator end()const{returniterator(nullptr,this);}boolErase(const K& key){ Hash hs; size_t hashi =hs(key)% _tables.size(); Node* cur = _tables[hashi]; Node* prev =nullptr;while(cur){if(kot(cur->_data)== key){if(prev ==nullptr){ _tables[hashi]= cur->_next;}else{ prev->_next = cur->_next;}delete cur;returntrue;}else{ prev = cur; cur = cur->_next;}}returnfalse;}private: vector<Node*> _tables; size_t _n =0;};};

在这里插入图片描述

以上就是本篇文章的所有内容,在此感谢大家的观看!这里是店小二呀C++笔记,希望对你在学习C++语言旅途中有所帮助!

Read more

优选算法——二分查找

👇作者其它专栏 《数据结构与算法》《算法》《C++起始之路》 二分查找相关题解 1.二分查找 算法思路: a.定义left,right指针,分别指向数组的左右区间。 b.找到待查找区间的中间点mid,找到后分三种情况讨论:         i.arr[mid]==target说明正好找到,返回mid的值;         ii.arr[mid]>target说明[mid,right]这段区间都是大于target的,因此舍去右边区间,在左边[left,mid-1]的区间继续查找,即让right=mid-1,然后重复b过程;         iii.arr[mid]<target说明[left,mid]这段区间的值都是小于target的,因此舍去左边区间,在右边区间[mid+1,right]

By Ne0inhk
深入理解 C++ 哈希:从概念到实战应用

深入理解 C++ 哈希:从概念到实战应用

🔥个人主页:Cx330🌸 ❄️个人专栏:《C语言》《LeetCode刷题集》《数据结构-初阶》《C++知识分享》 《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔 🌟心向往之行必能至 🎥Cx330🌸的简介: 目录 前言: 一、哈希的概念 1.1 直接定址法 1.2 哈希冲突 1.3 负载因子 1.4 将关键字转为整数 二、哈希函数 2.1 除法散列法 / 除留余数法 2.2 乘法散列法(了解) 2.3 全域散列法(了解)  2.4 其他方法(了解)  三、处理哈希冲突(

By Ne0inhk
【优选算法必刷100题】第031~32题(前缀和算法):连续数组、矩阵区域和

【优选算法必刷100题】第031~32题(前缀和算法):连续数组、矩阵区域和

🔥艾莉丝努力练剑:个人主页 ❄专栏传送门:《C语言》、《数据结构与算法》、C/C++干货分享&学习过程记录、Linux操作系统编程详解、笔试/面试常见算法:从基础到进阶 ⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平 🎬艾莉丝的简介: 🎬艾莉丝的算法专栏简介: 目录 031  连续数组 1.1  解法一:暴力解法 1.2  解法二:前缀和在哈希表中 1.3  算法实现 1.3.1  C++实现 1.3.2  Java实现 1.4  博主手记 032  矩阵区域和 2.1

By Ne0inhk
贪心算法篇——万千抉择中的唯一考量,最优解追寻的跬步累积(1)

贪心算法篇——万千抉择中的唯一考量,最优解追寻的跬步累积(1)

文章目录 * 引言:在选择的海洋中 * 贪心算法的哲学:局部最优,全球最优 * 贪心算法的经典应用 * 贪心算法的局限与挑战 * 结语:智者的选择,最优的未来 引言:在选择的海洋中 在人生的旅途上,每个人都要面临无数的选择。每一个选择,都是一次抉择;每一次抉择,都是命运的交汇点。数学与计算机科学的世界里,贪心算法正是对这种“选择”的一种深刻体现。在一系列的选择面前,贪心算法如同一位睿智的旅行者,始终秉持着最优的哲学:每一次决策都应基于局部最优,以期在最后抵达全局最优的境地。 贪心算法(Greedy Algorithm),正如其名所示,是一种每次都选择当前看起来最优解的算法。这种算法策略简单却充满智慧,常常能够解决很多看似复杂的问题。它通过一种局部的、贪婪的方式,一步步走向最终解。然而,正如智慧的旅行者需要对道路有所预见一样,贪心算法也有其适用的范围,只有在满足某些条件时,它才能发挥出最优解的魅力。 在这篇报告中,我们将深入探讨贪心算法的基本理念、适用范围、经典应用,并通过具体的代码示例,揭开这一算法的神秘面纱。 贪心算法的哲学:

By Ne0inhk