首页 > 其他分享 >封装实现unordered_map和set

封装实现unordered_map和set

时间:2023-11-20 16:06:15浏览次数:40  
标签:map set cur hashi key 哈希 size unordered con

什么是哈希思想

首先哈希是一个关联式容器,各个数据之间是具有关系的,和vector那些序列式容器不一样。

首先unordered_map中的迭代器是一个单向的迭代器。

其次在unorderede_map和set中是无序的(因为底层不是红黑树,而是哈希了)不再进行排序了。

用法和set/map一样(除了不能使用--之外)。

然后下面是对于map和unorder_ed map在性能上面的差异。

总结:在存在很多个重复值的时候unorder_ed系列是更加具有优势的,本质就是unorder_ed在查找时的效率更加优秀一些,当查找到某些值的时候,就不会插入重复,而哈希表因为查找较为优秀所以会块一些,在其它的情况下两者的效率是差不多的。哈希的定义也就是下面的这个:

封装实现unordered_map和set_链表

下面我们回忆一下下面的这道题目:

封装实现unordered_map和set_链表_02

这道题目的第一步肯定是统计次数,但是怎么统计的呢?

我们那时候的使用方法就是建立一个25个空间的数组,然后做一个相对的映射,让a映射到数组的0位置,b在1位置,然后遇到a让a位置处的值++。

封装实现unordered_map和set_数据_03

这里的统计次数也就体现了一个哈希,这里就是你统计的那个值和所处的位置建立了一个关联关系,通过值直接就能在数组中找到它储存的位置,但是在数组中储存的是这个值出现的次数。

这里就是储存的位置和值建立一个关联关系,这样你给我一个值我就能知道它的储存位置,找到这个位置,我就能判断这个值存不存在,在那个位置上在存一个val,就能够实现通过一个值找到另外一个值的kv模型。

这就是哈希(散列)。

闭散列哈希表的实现(不完整)

但是如果一个值一定和自己的位置一一绑定,如果遇到下面的情况就会造成空间浪费,有的值很小,但是有的值非常大,这里的这个方法就是直接定址法(要么这个值就是这个位置,要么这个值减去一个值,或者加上一个值就是这个值的位置)。

封装实现unordered_map和set_数组_04

这个方法在遇到下面的这种数据的时候,就会出现严重的空间浪费。

为了解决这个问题,有人就提出了下面的方法不管你的值存在多少我都只开有限的空间数。

那么如何保证所有的值都能映射到空间中呢?我们可以让每一个值模一下空间的大小:

封装实现unordered_map和set_数组_05

但是就拿上图举例如果是3和33举例子,3模完是一个3,33模完也是3。

这个问题也就是

封装实现unordered_map和set_链表_06

那么要怎么解决这个问题呢?

我们上面学习的那个题目因为题目已经规定了只是小写字母,所以我们设置25个空间就能够让每一个字母都一一映射到属于自己的位置。因为小写字母的数量有限所以使用直接定址法是不会出现冲突的。

那么如何解决这个冲突呢?

这里有两种方法解决

第一种:

封装实现unordered_map和set_数据_07

封装实现unordered_map和set_链表_08

首先当我们已经将3和4放到上面的位置之后,如果遇到了33,在模了一个10之后找到hashi为3之后,首先会检测3这个位置,如果发现3这个位置已经存在值了,那么这里就会去后面的位置中寻找到一个没有值的位置放置。

那么要怎么寻找新的位置呢?这里也存在两个方法寻找新的方法。

封装实现unordered_map和set_链表_09

总结就是线性探测就是从当前位置一个一个往后找,而二次探测则是按照i^2往后找(例如第一次跳2,第二次跳4,第三次跳16,当遇到了边界之后,又会跳回来和循环队列一样不会导致越界的情况发生)。

下面我们来详细的讲解一下线性探测:

首先会不会发生无法找到新位置的情况呢?

这个情况是不可能发生的,因为在后序我们会控制一个叫做负载因子的东西,这个东西是储存的值的个数和空间的比率:

封装实现unordered_map和set_数组_10

这个负载因子会保持在一定的比率之下,让其一直存在多余的空间。但是哈希还有一个缺点就是如果遇到下面的情况就会造成一大片的哈希冲突。例如下面这样:

封装实现unordered_map和set_链表_11

首先插入一个3的位置,但是在插入完成之后,又插入了一个33,根据线性探测的规定就将4的位置用来储存

了,但是后面又插入了一个4,这个4只能去占据5的位置,由此也就造成了一片的哈希冲突。这也正是线性探测的问题,也正因此才会存在二次探测,二次探测因为不是连续的占用空间,就不会出现上面的这种情况。

然后在模拟实现的时候我们还会遇到一个问题,我们怎么知道当前数组所算出来的这个位置是空,还是存在值呢?

就算我能够判断但是如果遇到下面的这个情况呢?

封装实现unordered_map和set_链表_12

这里假设我将33给删除了,然后并没有记录初始值,然后我去find(34).这里当我到4这个位置,然后发现4这个位置是一个空,我就会停止寻找,这里会停止寻找的原因就在于我要寻找34这个值,那么根据模出来的位置就在4这个位置,但是因为4这个位置是空(之前存有值,但是被我们删除了,只有那个值没有被删除了,根据哈希的规则才会往后面寻找),就会停止寻找,也不能使用全部遍历的方法,不然寻找效率太低了。

所以为了防止这种情况,我们必须给每一个位置设定两个状态,第一种状态,这个位置本来就没有值,还有一个状态就是本来有值,但是被删除了。

然后下面还存在问题就是在模拟实现的位置了,当我们在模拟实现的时候。

当遇到了一个值要被放到vector中时要模的是vector的容量还是vector的size呢?

这里需要模的是size原因如下:

封装实现unordered_map和set_数组_13

假设我算出来的值是在18这个位置,size在15能够直接放值吗?很明显是不能的。

在vector中[]会直接断言小于size报错的。

所以我们在给vector扩容的时候,可以直接使用resize,保证容量和size是一样的。

还有对于负载因子在多少的时候扩容是有要求的

封装实现unordered_map和set_链表_14

所以一般负载因子是控制在70%左右最好.

哈希表在扩容的时候也是有要求的,我们在扩容的时候,能不能直接吧原表扩容到2倍,然后就不管了。

就以下面作为假设:

封装实现unordered_map和set_数据_15

假设此时负载因子到了,能不呢直接将上面的空间扩容到20呢?

肯定不能假设在扩容成20之后,我们在去find(34)还找的到吗?此时34模的不再是10而是20自然就找不到34了,所以哈希表的扩容并不是直接扩大空间的。

哈希表创建空间是按照下面的步骤做的:

封装实现unordered_map和set_数组_16

初步实现

(没有增加扩容的规则)

#include<iostream>
using namespace std;
#include<vector>
enum status
{
       EXIST,// 代表当前的节点是存在值的
       DELETE,// 代表当前的节点之前是存在值的但是现在这个值被删除了
       NU//代表当前节点不存在值
};
template<class K,class V>
struct HashNode
{
       HashNode()
              :_status(NU)
       {}
       int _status;
       pair<K, V> _kv;// 储存的值
};//为了解决线性哈希表的哈希冲突问题,我们必须自己建立哈希表中的节点
template<class K,class V>// 依旧是K,V模式的哈希表
class HashTable
{
       HashTable()
       {
              con.resize(5);//提供一些初始化的空间,要使用resize保证容量和size是一致的,用于计算hashi是不会出错
       }
       bool insert(pair<K,V>& kv)
       {
              // 下面就是需要在vector中寻找一个值
              int hashi = kv.first % con.size();
              while (con[hashi]._status != NU)// 如果当前寻找到的这个空间不是NULL,那就使用线性探测(一个一个往后寻找的方法)
              {
                      hashi++;
                      hashi %= con.resize();
              }//到这里代表的就是con[hashi]的状态是一个空了,可以插入值了
              con[hashi]->_kv = kv;
              con[hashi]->_status = EXIST;//修改状态,但是在这里我们发现了一个问题,如果需要扩容呢?我们此时并没有解决需要扩容的问题
       }
private:
       vector<HashNode<K, V>> con;//一个普通的哈希table中只会含有一个vector数组去实现
};

总结以上的知识:

封装实现unordered_map和set_数组_17

增加扩容

在上面的代码中我们使用的方法就是线性探测。

那么如何将数组中的空间做到扩容呢?首先直接使用让这个数组resize的·方法肯定是不可行的因为resize之后,每一个数的映射关系肯定要做出变化,因为当前值的位置在之前可能是除以10得到的,但是现在resize之后这个值应该改变位置因为它现在的位置应该是除以20,才能得到的。

所以直接使用resize增加空间不可行这里的方法是首先创建一个新的HashTable,修改这个新的HashTable的值为20(假设原来的值为10),然后将当前数组中的值全部insert插入到这个新的HashTable中,最后当插入完成之后,使用swap交换两个HashTable中的vector。

这里有一个需要注意的点就是当重新开辟空间之后之前发生冲突的值,就可能不会发生冲突了,例如4和14,在空间为10的时候,会发生冲突,但是在空间为20的时候,就不会发生冲突。还有一个点如果我的key是一个负数呢?那么这个pair会如何插入呢?这里虽然key是一个负数,但是在我们计算hashi的时候,key模上的con.size()(这个函数的返回值是一个无符号的整型),所以这里会将key当作一个无符号的整型,所以最后得到的还是一个正数,可以映射到vector中,所以即使是负数的key也不需要单独的处理。

但是到这里还是不够完善,想象一个场景那就是如果我往这个哈希表中插入一个已经存在的值呢?此时依旧会将这个新的值插入到HashTable中,但是这是不符合规则的,所以这里我们需要再完善一个find函数,如果我们在插入之前就发现当前的key已经在HashTable中存在了,就不需要插入了。


至于Erase因为在这里是一个伪删除,我们只需要先找到然后修改节点的状态即可:

下面是代码:

HashNode<K,V>* Find(const K& key)
{
        int hashi = key % con.size();
        while (con[hashi]._status != NU)
        {
               if (con[hashi]._status != DELETE && con[hashi]._kv.first  == key)//这里需要判断是否处于删除状态也是重要的,因为
                      //在这个HashTable实现的时候,删除其实是一个伪删除,这里只是将要删除的那个元素状态修改成了DE,然后让_n--
                      //所以在这里我们假设一个状态,首先我在哈希表中插入了一个pair<3,3>,然后删除了pair<3,3>,如果在查找的这里我没有判断当前这个节点的状态是否是删除,那么这里
                      //就会出现我虽然删除了pair<3,3>但是任然可以找到pair<3,3>的情况,所以这里需要判断
               {
                      return &con[hashi];
               }//在这里代表找到了需要寻找的值
               hashi++;//代表没有找到继续往后直到状态为NU,才会停止寻找
               hashi %= con.size();//防止越界
        }//当运行到这里代表在hash表中没有找到这个值
        return nullptr;//返回一个空指针即可
}
bool Erase(const K& key)
{
        //对于删除就很简单了
        //因为是一种伪删除,所以直接修改状态,然后让_n--即可
        HashNode<K, V>* ret = Find(key);
        if (ret)
        {
               ret->_status = DELETE;
               --_n;
               return true;
        }
        //在这里代表没有找到这个值自然就不能够删除
        return false;
}
bool insert(pair<K, V> kv)
{
        //增加了需要扩容的机制所以在这里就需要判断一下是否需要扩容
        //这里的规则就是当负载因子达到70%左右时需要扩容
        if (Find(kv.first))//这里如果Finde返回的非空,代表在vector中已经存在了key,那么直接返回false
        {
               return false;// 代表这个值存在于vector中,不能插入
        }//到这里代表在原数组中没有key,代表可以插入
        if (_n * 10 / con.size() == 7)
        {
               //需要扩容
               // 这里不能直接让con.resize(con.size()*2),因为会让原来数据的映射关系发生变化(之前数据的映射关系除的是10),但是这里新的映射关系需要除以20
               //但是,如果直接是修改con的resize的话,之前所有已经插入的数据,映射关系都会出错
               HashTable<K, V> new_table;//创建一个新的hash_table
               new_table.con.resize(con.size() * 2);//修改这个新哈希表的大小
               for (auto e : con)
               {
                      new_table.insert(e._kv);
               }//将原来的值插入到新的这个哈希表中
               con.swap(new_table.con);
        }
        // 下面就是需要在vector中寻找一个值
        int hashi = kv.first % con.size();
        while (con[hashi]._status != NU&&con[hashi]._status!=DELETE)//  如果当前寻找到的这个空间不是NULL,那就使用线性探测(一个一个往后寻找的方法)
        {
               hashi++;
               hashi %= con.size();
        }//到这里代表的就是con[hashi]的状态是一个空了,可以插入值了
        con[hashi]._kv = kv;
        con[hashi]._status = EXIST;//修改状态,但是在这里我们发现了一个问题,如果需要扩容呢?我们此时并没有解决需要扩容的问题
        //为了解决这个问题,我们增加了一个叫做负载因子的变量,每插入一个值那就让负载因子++
        ++_n;
        return true;
}

这里也能直到在节点的状态中删除和空是不等价的,空用于在寻找时判断结束。上面的线性哈希表其实还存在一些问题。

封装实现unordered_map和set_数据_18

例如如果我想完成一个能够计数的线性哈希表呢?

首先能够计数,也就意味这pair<string,int>但是这里存在一个问题就是,如何将string给转化成int呢?这里我们的放法就是将string中每一个字符的ascll2码相加然后模上一个size来作为hashi。但是这么做是存在一些冲突的情况的。例如一个字符串abc和字符串acb两者算出来的hashi是一样的,但是这两个字符串很明显不是同一个字符串。为了尽量的减少这种情况,于是就存在下面的这种计算字符串的hashi的方法。

封装实现unordered_map和set_数据_19

虽然这里依旧是拿每一个字符的asc2码相加但是对于每一个字符都做了一个处理,那就是让每一个字符的asc2码都乘上了一个31/131(至于为什么是这个数字我暂时没有理解)

仿函数的增加

这里我们就可以再将仿函数加上了。

下面是代码:

struct stringFunc
{
    size_t operator()(const string& key)
    {
        size_t hashi = 0;
        for (auto e : key)
        {
           hashi = hashi * 31 + e;//让每一次计算出的hashi都乘上一个31/131
        }
        return hashi;//将string中的每一个字符都加起来最后返回字符之和。
    }
};


使用这种方法之后,打印一下不同字符串所对应的hashi:

封装实现unordered_map和set_数组_20

可以看到此时的abc和acb对应的hashi就不一样了。此时就能尽可能的减少这种冲突了。这种方法也就是BCDR方法,那么假设我们在我们的HashTable中储存的键值对是一个Person类呢?如果是一个Person类,我们就需要在Person类中寻找每一个对象都独一无二的属性作为key,例如身份证号。或者你可以使用姓名+年龄+班级来作为一个Person的key,总而言之一句话需要尽可能的做到让key是不会冲突的。当然即使冲突了也没有关系,只是我们选要做到尽可能的避免冲突,即使冲突了下面也会自己解决冲突。

解决不同类型计算hashi的问题

但是我们现在实现的这个HashTable和库中的还是具有冲突的。因为当我们在使用库中的unorder_ed map/undered_ed set的时候即使我们的键值对是string,int,在不传递hashstring这个仿函数的时候,还是可以使用的。那么在库中是怎么做到的呢?

这里解决的方法就是使用特化:

template<class K>
       struct HashFunc
       {
              size_t operator()(const K& key)
              {
                      return (size_t)key;//对于能够直接使用key转化的那就直接返回key即可
              }
       };
template<>
       struct HashFunc<string>//当K为string的时候,编译器会特化实现这一个仿函数
       {
              size_t operator()(const string& key)
              {
                      size_t hashi = 0;
                      for (auto e : key)
                      {
                             hashi = hashi * 31 + e;//让每一次计算出的hashi都乘上一个31/131
                      }
                      cout << key << ":" << hashi << endl;// 在这里我打印一下这些值的hashi
                      return hashi;//将string中的每一个字符都加起来最后返回字符之和。
              }
       };

那么如果在这里重载一个operator()是否可行呢?答案自然是不可行:原因在于如果你是在struct HashFunc中重载了operator(),如果你传递的key是一个int那不会出现任何的问题,因为此时在HashFunc对象中实例化形成的两个operator(),是能够构成函数重载的(函数参数不同)但是如果你传递的key是一个string呢?此时在HashFunc对象中实例化形成了两个operator()并且两个operator()的函数参数和返回值一摸一样,就会直接报错。如果使用的是特化的话,如果你的key非string,那么就会去上面的那个特化,如果你的key是一个string那么就会去自己实现一个string的operator。但是上面的这种哈希表(闭散列的方法)实际上运用的并不多,运用的多的还是下面的这种哈希表(开散列的方法)

开散列哈希表的实现

实现思想

此时这个哈希表的增删查改就已经基本完成了,Find函数也时完成了修改的操作,但是这个哈希表的缺点1就是可能会造成哈希冲突成一片。而冲突的越多,查找的效率也就越低,,虽然二次探测能够减少冲突的发生,但是当数据达到一个量级时冲突时一定还会发生的。所以也就有了下面的哈希桶的存在(拉链法):

那么什么是哈希桶(拉链法)呢?

依旧是一个数组然后一推数据:

不过这次的这个数组中存的是一个指针,即这是一个指针数组:

封装实现unordered_map和set_数组_21

当遇到key为1的时候,会放到这个指针指向的链表中去,然后如果是4,14,24,44,那就找到4这个桶所在的位置,然后使用链表的方式不断的往下链接数据。

这样就解决了哈希冲突的问题,当遇到一个新的值要插入的时候,也只需要找到这个值所在的桶然后直接头插即可(不使用尾插因为找尾会浪费一些时间)。而在java中对于哈希桶其实还有一个规则,当在哈希桶中某一个桶的长度如何大于了8,那么就会将当前的这个链表结构换成红黑树的结构(我们1今天实现的这个哈希桶并没有这个规则)。下面我们来看一个哈希桶的时间复杂度:首先我们要知道的是哈希表的平均时间复杂度为o(1),那么是怎么计算的呢?

首先在哈希桶里面负载因子是控制在1的,那么平均下来在每一个桶的下面都会存在一个节点,虽然在实际情况中可能不会这么平均,但是大概率会存在某些桶上没有数据,但是,某些桶上是存在多个数据的,那么在寻找特定数的时候,也就基本能做到在O(1)的时间杂度完成查找。如果某些极端的情况下(某个桶的长度过长)就会做下面的改变:

封装实现unordered_map和set_链表_22

这样也能保证哈希表的效率,但是这种单个桶很长的情况,是很难出现的(除非是故意设置的数据)

那么我们要如何实现这个哈希桶呢?

首先肯定是要使用一个指针数组,但是对于链表我们是使用c++中已经存在的链表,还是自己模拟实现一个链表呢?

两个方法都行:

封装实现unordered_map和set_数组_23

以上就是你如果要使用c++中的链表,那么可以这么写,怎么写当len>8的时候还能做到使用迭代器区间初始化红黑树,然后将list清除即可。所以你使用c++中的list也是可以使用哈希桶的,但是这里我选择了vector<Node*>原因在于我们下面要模拟实现迭代器,如果你是使用c++中的list,那么模拟实现迭代器就会很麻烦。(如果你要使用list也应该使用 forward_list,因为list是一个双向的链表,forward_list才是一个单向的链表)

基于以上的原因所以我选择vector<Node*>。

这里选择自己模拟实现list还有一个原因就是这里使用list插入数据时只需要考虑一个头插就行了。

那么下面我们就来实现一个简单版的insertr(不包含取key,等等额外的操作)我们在完成了简单版之后,再往上迭代新功能。

在这里我们也能够理解为什么很多时候,单链表的头节点在很多时候时没有意义的,例如这里,此时一个链表在桶下面挂着有创建头节点的必要么?还有一个点就在于,这就是为什么在之前学习单链表的时候,我们说过单链表一般是作为其它结构的子结构存在的这句话。

插入节点的思路也很简单,找到hashi然后创建节点让节点的next指向这个桶下面的链表头,在让这个桶指向这个新的节点。

封装实现unordered_map和set_数据_24

在这里我们也能够知道了为什么unorder_ed的迭代器是单向的(只能++),而不是双向的。

在插入这里还需要考虑一个问题那就是扩容,首先哈希桶是可以不扩容的,但是如果你1w个数据但是桶只有10个,如果这样计算下去每一个桶中至少都会存在1000个数据,那么查找的效率就会变得非常的慢,所以这里我们的选择是要扩容,那么负载因子到什么时候才扩容呢?还有扩容要怎么取扩容呢?首先在这里一般是当负载因子等于size的时候才需要扩容,在vs中的stl库中也是怎么做的当负载等于1的时候,才会去扩容,那么这里扩容还是和上面的方法一样吗?即首先创建一个新的哈希桶扩展空间后逐个插入数据吗?

封装实现unordered_map和set_数据_25

下图中蓝色的那些节点都是重新new出来的,在将上面的数据都拷贝到下面之后,我们需要释放上面的数据,那么我们是直接释放上面的桶就行了吗?肯定是不行的所以对于哈希桶的析构我们必须自己动手(因为默认的析构只会释放指针数组的空间不会释放下面桶的空间)。使用上图中的insert的方法的坏处就在于我们每一次都是重新开辟了节点的空间,这就显得很多余,最好的方法就是我们将上面桶中的节点修改链接到下面的桶中。上图中的那个思路正确性是没有问题的但是,还是具有优化空间的。

下面我们首先来完成第一步:


创建一个哈希模板:

template<class K,class V>
struct HashNode
{
       HashNode* next;
       pair<K, V> _kv;
};//哈希桶下的节点
template<class K,class V>
class HashTable
{
public:
       typedef HashNode<K,V> Node;
private:
       vector<Node*> _con;//哈希桶
       size_t _n = 0;//负载因子
};


因为这里我们需要创建HashNode节点所以HashNode这个节点也需要写构造函数

HashNode(const pair<K,V>& kv)
                      :next(nullptr)
                      ,_kv(kv)
              {}//构造函数

然后下面是插入函数:

bool insert(pair<K,V>& kv)
{
    if (Find(kv.first))//首先使用Find函数,这里我还没有实现
    {
        return false;//如果在哈希用中已经存在过这个节点了,返回false
    }
    if (_n == _con.size())
    {
        //这里需要扩容创建空间
    }
    int hashi = kv.first % _con.size();//首先获取一个hashi
    Node* newnode = new Node(kv);//先创建一个新的节点
    newnode->next = _con[hashi];
    _con[hashi] = newnode;//完成头插
    _n++;//用于计算负载因为的_n++
    return true;
}//现在就来写插入函数

如何扩容

那么下面我们想一下如何做到扩容空间。

这里使用的方法是依旧创建一个vector<Node*>,然后遍历旧的HashTable中的节点,然后使用旧的HashTable中的节点链接到新的HashTable中。

最后当一个桶中的节点全部转移之后,需要让旧桶赋值为nullptr。

所以下面就是比较完善的insert函数(增加了扩容空间)

bool insert(const V& kv)
{
    HashFunc geth;//hashfunc这个仿函数的功能为获取hashii
    getkey getk;//这个仿函数的功能为获取对象中的key值
    if (Find(getk(kv)))//首先使用Find函数,这里我还没有实现
    {
        return false;//如果在哈希用中已经存在过这个节点了,返回false
    }
    if (_n == _con.size())
    {
        //如何扩容呢?
        //首先创建一个vector<Node*>的数组
        vector<Node*> newvector;
        size_t newsize = _con.size() * 2;
        newvector.resize(newsize);
        //这里需要扩容创建空间
        //这里扩容的规则为遍历原哈希桶,然后将这个哈希桶中的节点拿出来,然后放到新的哈希桶中
        //这里并不是创建一个新的哈希节点而是将上面的节点直接拿出来
        for (int i = 0; i < _con.size(); i++)
        {
            Node* cur = _con[i];
            while (cur)
            {
                //记录当前桶的下一个节点
                Node* Nnext = cur->next;
                //计算当前节点新桶所在的位置
                size_t hashi = geth(getk(cur->_data)) % newsize;
                cur->next = newvector[hashi];
                newvector[hashi] = cur;
                cur = Nnext;
            }//当这个循环结束的时候还有一个重要的步骤
            _con[i] = nullptr;//原哈希桶的值转移完成之后,直接赋值为空
        }
        //完成交换之后
        _con.swap(newvector);//最后交换这个新哈希表的_con
    }
    int hashi = geth(getk(kv)) % _con.size();//首先获取一个hashi
    Node* newnode = new Node(kv);//先创建一个新的节点
    newnode->next = _con[hashi];
    _con[hashi] = newnode;//完成头插
    _n++;//用于计算负载因为的_n++
    return true;
}//现在就来写插入函数

Find和erase函数

那么现在Find函数也很好写了:

Node* Find(const K& key)
{
    //下面我们就来完善Find函数
    int hashi = key % _con.size();
    Node* cur = _con[hashi];
    while (cur)
    {
        if (cur->_kv.first == key)
        {
            return cur;
        }
            cur = cur->next;
        }
        //整个桶都完成之后直接返回nullptr
    return nullptr;
}

下面就是erase函数:对于erase函数和上面的闭散列erase函数能够复用Find函数然后去删除,但是这里我们不能使用Find函数找到Node* 节点,然后直接删除,因为不要忘了这里是单链表,所以这里我们依旧是要找到桶的位置,记录前驱节点然后去删除key节点。

下面是代码:

bool Erase(const K& key)
{
    int hashi = key % _con.size();
    Node* prev = nullptr;
    Node* cur = _con[hashi];
    while (cur)
    {
        if (cur->_kv.first == key)
        {
            if (prev == nullptr)//如果前驱节点为空,那么代表这是一个头节点的删除
            {
                _con[hashi] = cur->next;//先让cur的next成为_con[hashi]的值,然后去删除cur
                delete cur;
            }
            else//代表此时的前驱节点不为空
            {
                prev->next = cur->next;//将cur后面的节点链接到prev的后面
                delete cur;
            }
            return true;//代表删除成功了
        }
        cur = cur->next;//在当前的桶中寻找需要删除的节点
    }
    return false;//删除失败
}

在完成了上面的代码之后,我们去解决下一个问题,对于字符串和普通的key来计算hashi的仿函数。

解决的思路和上面闭散列的哈希表是一样的

封装实现unordered_map和set_数组_26

然后我们需要使用这个仿函数去将kv.first给替换成为size_t。

在将上面的代码都修改之后,我们去检查一下上面写的代码跑一下:

string b[] = { "苹果","西瓜","樱桃","西瓜","苹果","樱桃" };
Hash_Bucket::HashTable<string, int> dict;
for (auto e : b)
{
    if(dict.Find(e))
    {
        dict.Find(e)->_kv.second++;//当前水果存在
    }
    else
    {
        dict.insert(make_pair(e, 1));//如果当前的水果没有
    }
}

这里因为我没有写打印函数所以这里我就用调式窗口查看一下:

封装实现unordered_map和set_数组_27

这里说明我们现在写的代码是正确的。

封装实现unordered_map和set

那么下面我们就来封装一下unordered_map和undered_set。

对于unordered_map和undered_set的封装我们首先要知道的是两者在给hashtable传参数的时候,对于的key和value都是一起传递给hashtable的第二个参数的。

例如下面是unordered_map的封装:

template<class K,class V>
class unordered_map
{
public:
       struct mapofkey
       {
              K operator()(const pair<K,V>& v)
              {
                      return v.first;//返回first
              }
       };//需要注意这里的仿函数的作用是从一个对象中获得key的值,而不是获得key的类型
       bool insert(const pair<K, V>& k)
       {
              return _con.insert(k);
       }
private:
       Hash_Bucket::HashTable<K, pair<K, V>, mapofkey> _con;//从这1里将K,和V以及获取mapoofkey的方法放过去
       // 可以看到map的kev和value都放在了hashtable的第二个参数中,所以为了能够从pair从获得K,搭配了对应的仿函数
};

下面是unordered_set的封装

template<class K>
class unordered_set
{
public:
       struct setofkey
       {
              const K& operator()(const K& key)
              {
                      return key;//对于set而言key返回的就是key
              }
       };
private:
       Hash_Bucket::HashTable<K,K,setofkey> _con;//这里其实就应该将转换hashi的那个仿函数放到这里传过去
//因为这里我的HashTable是放在了命名空间Hash_Bucket中的所以这里需要首先指定命名空间
};//这里既然要封装unordered_set那么就回到之前使用红黑树封装set的时候,我们需要考虑如何获得一个set对象中的key
//方法依旧是使用仿函数
//这里因为对于set而言是没有使用kv模型的所以set的key也就是value

在完成上面的封装之后我们还需要改变一下Hash_Table中的函数。

首先就是对于Node的声明

封装实现unordered_map和set_数组_28

因为此时无论是对map还是set而言都是将(keyvalue)放到了一个参数中,所以这里Node也就只需要是一个T类型即可,如果是set那么这个T就是一个普通的key,如果这是map那么这里就是一个pair<K,V>。

然后就是对于HashTable的模板参数

封装实现unordered_map和set_数组_29

这里增加了一个新的模板参数,这个模板参数就是由unordered_map和undered_set传递过来的mapofkey和setofkey,用于获取一个对象中key的值。

然后就是insert对于之前使用_data的地方都要再进行一层解封装。

bool insert(const V& kv)
{
    HashFunc geth;//hashfunc这个仿函数的功能为获取hashii
    getkey getk;//这个仿函数的功能为获取对象中的key值
(Find(getk(kv))))//首先使用Find函数,这里我还没有实现
    {
        return false;//如果在哈希用中已经存在过这个节点了,返回false
    }
    if (_n == _con.size())
    {
        //如何扩容呢?
        //首先创建一个vector<Node*>的数组
        vector<Node*> newvector;
        size_t newsize = _con.size() * 2;
        newvector.resize(newsize);
        //这里需要扩容创建空间
        //这里扩容的规则为遍历原哈希桶,然后将这个哈希桶中的节点拿出来,然后放到新的哈希桶中
        //这里并不是创建一个新的哈希节点而是将上面的节点直接拿出来
        for (int i = 0; i < _con.size(); i++)
        {
            Node* cur = _con[i];
            while (cur)
            {
                //记录当前桶的下一个节点
                Node* Nnext = cur->next;
                //计算当前节点新桶所在的位置
size_t hashi = geth(getk(cur->_data)) % newsize;
                cur->next = newvector[hashi];
                newvector[hashi] = cur;
                cur = Nnext;
            }//当这个循环结束的时候还有一个重要的步骤
            _con[i] = nullptr;//原哈希桶的值转移完成之后,直接赋值为空
        }
        //完成交换之后
        _con.swap(newvector);//最后交换这个新哈希表的_con
    }
geth(getk(kv))
    // 这里的KV就是一个data,因为我们不知道这个data是否是一个pair所以使用一个getkey仿函数获得key的值,再使用key的值去计算一个hashi
    Node* newnode = new Node(kv);//先创建一个新的节点
    newnode->next = _con[hashi];
    _con[hashi] = newnode;//完成头插
    _n++;//用于计算负载因为的_n++
    return true;
}

然后是Find和erase函数

Node* Find(const K& key)
{
    HashFunc geth;
    getkey getk;
    //下面我们就来完善Find函数
geth(key)
    Node* cur = _con[hashi];
    while (cur)
    {
        if (getk(cur->_data) == key)
        {
            return cur;
        }
        cur = cur->next;
    }
    //整个桶都完成之后直接返回nullptr
    return nullptr;
}
bool Erase(const K& key)
{
    HashFunc geth;
    getkey getk;
    int hashi = geth(key) % _con.size();
    Node* prev = nullptr;
    Node* cur = _con[hashi];
    while (cur)
    {
        if (getk(cur->_data) == key)
        {
            if (prev == nullptr)//如果前驱节点为空,那么代表这是一个头节点的删除
            {
                _con[hashi] = cur->next;//先让cur的next成为_con[hashi]的值,然后去删除cur
                delete cur;
            }
            else//代表此时的前驱节点不为空
            {
                prev->next = cur->next;//将cur后面的节点链接到prev的后面
                delete cur;
            }
            return true;//代表删除成功了
        }
        cur = cur->next;//在当前的桶中寻找需要删除的节点
    }
    return false;//删除失败
}

再做改变的地方我都使用了黑色做出了标注。

因为在封装map的时候,我额外多封装了一个insert,下面我们就来测试一下map的封装是否存在问题

封装实现unordered_map和set_数组_30

运行调试没有出错。下面我们再封装一下set的insert再测试一下。

template<class K>
class unordered_set
{
public:
    struct setofkey
    {
        const K& operator()(const K& key)
        {
            return key;//对于set而言key返回的就是key
        }
    };
    bool insert(const K& k)
    {
        return _con.insert(k);
    }
private:
    Hash_Bucket::HashTable<K, K, setofkey> _con;//这里其实就应该将转换hashi的那个仿函数放到这里传过去
};

运行调式的结果:

封装实现unordered_map和set_链表_31

依旧是没有错误。

我们下面再将其它的接口一起封装进去。

下面是map的封装:

bool find(const K& key)
{
    Hash_Bucket::HashNode<K,V>* ret = _con.Find(key);
    if (ret)
    {
        return true;
    }
    else
    {
        return false;
    }
}//这里是简单版的find函数如果ret为空代表没有找到返回false
// 找到则返回true
bool erase(const K key)
{
    return _con.Erase(key);
}

下面是set的封装:

bool find(const K& key)
{
    Hash_Bucket::HashNode<K>* ret = _con.Find(key);
    if (ret)
    {
        return true;
    }
    else
    {
        return false;
    }
}//这里是简单版的find函数如果ret为空代表没有找到返回false
// 找到则返回true
bool erase(const K key)
{
    return _con.Erase(key);
}

下面简单测试一下这些接口的封装

封装实现unordered_map和set_数据_32

运行没有出错。

那么下面我们就要来到最难的一步了,那就是完成迭代器的封装,要完成迭代器,首先肯定是需要完成哈希表的迭代器。

迭代器的封装和实现

下面我们思考一下在这个迭代器中需要什么成员,首先节点的指针肯定是必须的。除此之外我们还需要那一张表来帮助寻找每一个桶的下一个桶。那么下面就来写:

基本的迭代器代码

template<class K, class V, class getkey, class HashFunc = Hash<K>>
struct __iterator
{
    typedef HashNode<V> Node;
    typedef __iterator<K, V, getkey, HashFunc> Self;
    Node* _node; //首先既然是迭代器我们肯定要包含的是一个节点的指针,迭代器的作用是能够找到下一个指针,所以我们还需要一个记录桶位置的表,
        //这里有两种实现方法,第一种就是将HashTable中的vector给传递过来,还有一个方法就是将hashTable中的vector传递过来,这里选择的是将HashTable传递过来
        //其实这里传递HashNodce中的vector实现更为简单,但是为了复习再模板中的友元类所以这里选择是将HashTable传递过来
    HashTable<K, V, getkey, HashFunc>* _pid; // 为了得到那张表这里选择的是将HashTable传递过来
    size_t _hashw;// 记录当前迭代器所在桶的位置
    __iterator(Node* root,HashTable<K, V, getkey, HashFunc>* pid,size_t hashw)
        :_node(root)
        ,_pid(pid)
        ,_hashw(hashw)
    {}//完成构造函数
    Self operator++()
    {
        //那么如何取得下一个节点的迭代器呢?
        Node* next = _node->next;
        if (next)//如果next不是空代表next就是下一个迭代器需要的节点
        {
            _node = next;
        }
        else//next为空,代表当前的这个桶已经被我们遍历完成了,要去下一个桶中遍历节点
        {
            //现在的问题就在于如何获得下一个桶的坐标这就是为什么我们要将HashTable传递过来,但是光有表还是不行的这里我们还需要知道当前的这个桶所在的位置
            //获取位置的方法存在两个第一个现场算
            //getkey getk;
            //HashFunc geth;
            //size_t hashi = geth(getk(_node->_data)) % (_pid->_con.size());
            //这是一种获得所在桶位置的方法
            //下一种方法就是在创建迭代器的时候就将当前桶所在的下标传递过来
            //这里我选择的是第二种方法
            _hashw++;
            while (_hashw<_pid->_con.size())//往后寻找所有的桶中下一个节点的位置
            {
                if (_pid->_con[_hashw])//这个桶中的数据不为空
                {
                    _node = _pid->_con[_hashw];
                    break;
                }
                _hashw++;
            }
            if (_hashw == _pid->_con.size())//如过所有的桶都已经寻找过了,代表在这个哈希表中已经不存在数据了让_node赋值为空
            {
                _node = nullptr;
            }
        }
        return *this;
    }
    bool operator!=(const Self& b)
    {
        return _node != b._node;
    }
    V& operator*()
    {
        return _node->_data;
    }
};


解决相互依赖

但是如果只是和上面这样写是存在一个问题的,这个迭代器的实现肯定是放在了Node结构体的下面,但是在HashTable结构体的上方,这时如果我在迭代器中需要使用HashTable就会出现一个相互依赖的问题。此时我的HashTable的构造需要一个迭代器,所以必须要将迭代器放在HashTable的前面,但是要完成迭代器又需要HashTable这个结构,因为编译器在编译的时候,只会从迭代器处往上找,也就导致了编译器不知道这个HashTable是什么。这里即使你将HashTable放到迭代器的前方,那么迭代器的问题是解决了,但是HashTable需要迭代器,但是编译器在往上找的时候没有找到迭代器,又会出现错误。

所以这里为了解决这个问题:我们需要做一个声明在迭代器实现的上方做一个HashTable的声明用于告诉编译器HashTable这个结构是存在的。

封装实现unordered_map和set_链表_33

那么现在就解决了相互依赖的问题,下面我们来将迭代器给封装到HashTable中

封装实现unordered_map和set_数据_34

下面在将迭代器封装到map和set中。

封装到map中:

封装实现unordered_map和set_链表_35

封装到set中:

封装实现unordered_map和set_数据_36

下面我们来测试一下:

map和set中能否使用范围for

然后就会看到下面的报错:

封装实现unordered_map和set_数据_37

这个错误就在于HashTable中的迭代器:

封装实现unordered_map和set_链表_38

可以看到_con是一个私有成员,但是在迭代器的operator++()中却直接访问了_con这个私有成员。

封装实现unordered_map和set_数组_39

所以这里需要将迭代器作为HashTable的友元类。

封装实现unordered_map和set_数据_40

下面我们就来测试一下迭代器能否使用:

封装实现unordered_map和set_数据_41

可以看到没有问题。

在这里我们也需要修改一下HashFunc这个函数传递的位置,因为我们现在是在map和set中封装了哈希表,所以这里我们需要将获取Hashi的那两个仿函数传递的位置u修改一下。

步骤就是在HashTable中将HashFunc的那两个仿函数放到set和map中然后删除HashTable中的默认模板参数。在map和set中传递过去:

封装实现unordered_map和set_链表_42

封装实现unordered_map和set_数据_43

这里我就只显示map的修改了,set的修改是一样的。

做了上面的修改之后我们来完成最后的const迭代器。

const迭代器实现

首先就是在普通迭代器的模板参数中增加两个参数:

封装实现unordered_map和set_数组_44

然后修改一下operator*和operator->

封装实现unordered_map和set_数据_45

然后修改一下迭代器在HashTable中的声明。生成出两种类型的迭代器

封装实现unordered_map和set_数组_46

然后要为const迭代器专门写一套begin和end函数。

封装实现unordered_map和set_数组_47

然后下面就是封装到set中因为set是完全不支持修改数据的所以在set中普通迭代器就是const迭代器。

下面是map的封装:

封装实现unordered_map和set_数据_48

下面是set的封装

封装实现unordered_map和set_数组_49

但是在完成了上面的修改之后还是会抱一个错误

封装实现unordered_map和set_数据_50

原因如下:

封装实现unordered_map和set_数组_51

在end里面此时的this指针是被const修饰的但是在迭代器的那个构造函数中pht只是一个普通的HashTable这里就发生了权限的放大自然会报错。

解决方法:

封装实现unordered_map和set_数据_52

在迭代器的HasshTable成员前面增加一个const,同时让构造函数的pid前面也增加一个const,这里不需要在写一个非const版本的构造函数,因为非const修饰的HashTable指针也能放在const修饰啊的HashTable指针上面。

这样我们就解决了set的key可以被修改的问题了。

封装实现unordered_map和set_链表_53

然后下面是对于map的key不可被修改但是value可以被修改。

只需要在传参的时候这么传即可:

封装实现unordered_map和set_数据_54

封装实现unordered_map和set_数据_55

这样在下面的模板生成的时候key就是一个const key无论是在普通还是const迭代器这个key都不可被修改。

封装实现unordered_map和set_数据_56

下面我们在来为map完成最后一个函数也就是[]的重载。

实现map中的[]重载

要完成[]的重载那么我们就需要修改一下之前写的一些函数:

首先是Find函数

封装实现unordered_map和set_数据_57

然后就是在map和set中封装的find函数修改:

封装实现unordered_map和set_数据_58

两个容器封装的修改都是这样。

然后就是对于HashTable中insert函数的修改

pair<iterator,bool> insert(const V& kv)
{
    HashFunc geth;//hashfunc这个仿函数的功能为获取hashii
    getkey getk;//这个仿函数的功能为获取对象中的key值
    iterator it = Find(getk(kv));
    if (it)
    {
        //it不是空代表找到了这个值
        return make_pair(it,false);//那么插入自然就是失败了
    }
    if (_n == _con.size())
    {
        //如何扩容呢?
        //首先创建一个vector<Node*>的数组
        vector<Node*> newvector;
        size_t newsize = _con.size() * 2;
        newvector.resize(newsize);
        //这里需要扩容创建空间
        //这里扩容的规则为遍历原哈希桶,然后将这个哈希桶中的节点拿出来,然后放到新的哈希桶中
        //这里并不是创建一个新的哈希节点而是将上面的节点直接拿出来
        for (int i = 0; i < _con.size(); i++)
        {
            Node* cur = _con[i];
            while (cur)
            {
                //记录当前桶的下一个节点
                Node* Nnext = cur->next;
                //计算当前节点新桶所在的位置
                size_t hashi = geth(getk(cur->_data)) % newsize;
                cur->next = newvector[hashi];
                newvector[hashi] = cur;
                cur = Nnext;
            }//当这个循环结束的时候还有一个重要的步骤
            _con[i] = nullptr;//原哈希桶的值转移完成之后,直接赋值为空
        }
        //完成交换之后
        _con.swap(newvector);//最后交换这个新哈希表的_con
    }
    size_t hashi = geth(getk(kv)) % _con.size();//首先获取一个hashi
    Node* newnode = new Node(kv);//先创建一个新的节点
    newnode->next = _con[hashi];
    _con[hashi] = newnode;//完成头插
    _n++;//用于计算负载因为的_n++
    return make_pair(iterator(newnode,this,hashi),true);//插入成功了那么就返回这个新插入指针的迭代器。
}

然后重新封装一下map和set中的insert函数。

map的封装:

封装实现unordered_map和set_链表_59

对于set的封装就比较麻烦了,因为set里面的迭代器都是const迭代器但是insert返回的是一个普通的迭代器,这中间就存在了迭代器的转化问题,这里我是这解决的:

封装实现unordered_map和set_链表_60

map不需要任何的修改因为在map中普通迭代器和const迭代器是分开的。

最后让我们来完成map中[]的重载:

封装实现unordered_map和set_数组_61

最后让我们使用记录水果数量的代码来测试一下:

封装实现unordered_map和set_数组_62

运行截图:

封装实现unordered_map和set_数组_63

完整代码

下面是各个头文件的完整的代码:

#include<vector>
//namespace LHY {
//	enum status
//	{
//		EXIST,// 代表当前的节点是存在值的
//		DELETE,// 代表当前的节点之前是存在值的但是现在这个值被删除了
//		NU//代表当前节点不存在值
//	};
//	template<class K,class V>
//	struct HashNode
//	{
//		HashNode()
//			:_status(NU)
//		{}
//		int _status;
//		pair<K, V> _kv;// 储存的值
//	};//为了解决线性哈希表的哈希冲突问题,我们必须自己建立哈希表中的节点
//	template<class K>
//	struct HashFunc
//	{
//		size_t operator()(const K& key)
//		{
//			return (size_t)key;//对于能够直接使用key转化的那就直接返回key即可
//		}
//	};
//	//struct stringFunc
//	//{
//	//	size_t operator()(const string& key)
//	//	{
//	//		size_t hashi = 0;
//	//		for (auto e : key)
//	//		{
//	//			hashi = hashi * 31 + e;//让每一次计算出的hashi都乘上一个31/131
//	//		}
//	//		cout << key<<":" << hashi << endl;// 在这里我打印一下这些值的hashi
//	//		return hashi;//将string中的每一个字符都加起来最后返回字符之和。
//	//	}
//	//};
//	//为了解决能够不传递stringFunc也能算出string的hashi这里使用的方法是使用特化
//	template<>
//	struct HashFunc<string>//当K为string的时候,编译器会特化实现这一个仿函数
//	{
//		size_t operator()(const string& key)
//		{
//			size_t hashi = 0;
//			for (auto e : key)
//			{
//				hashi = hashi * 31 + e;//让每一次计算出的hashi都乘上一个31/131
//			}
//			cout << key << ":" << hashi << endl;// 在这里我打印一下这些值的hashi
//			return hashi;//将string中的每一个字符都加起来最后返回字符之和。
//		}
//	};
//	template<class K, class V,class Hash = HashFunc<K>>// 依旧是K,V模式的哈希表
//	class HashTable
//	{
//	public:
//		HashTable()
//		{
//			con.resize(10);//提供一些初始化的空间,要使用resize保证容量和size是一致的,用于计算hashi是不会出错
//		}
//		HashNode<K,V>* Find(const K& key)
//		{
//			Hash get;
//			int hashi = get(key) % con.size();
//			while (con[hashi]._status != NU)
//			{
//				if (con[hashi]._status == EXIST && con[hashi]._kv.first == key)//这里需要判断是否处于存在状态也是重要的,因为
//					//在这个HashTable实现的时候,删除其实是一个伪删除,这里只是将要删除的那个元素状态修改成了DE,然后让_n--
//					//所以在这里我们假设一个状态,首先我在哈希表中插入了一个pair<3,3>,然后删除了pair<3,3>,如果在查找的这里我没有判断当前这个节点的状态是否是存在的,那么这里
//					//就会出现我虽然删除了pair<3,3>但是任然可以找到pair<3,3>的情况,所以这里需要判断当前的这个值是否是存在的状态
//				{
//					return &con[hashi];
//				}//在这里代表找到了需要寻找的值
//				hashi++;//代表没有找到继续往后直到状态为NU,才会停止寻找
//				hashi %= con.size();//防止越界
//			}//当运行到这里代表在hash表中没有找到这个值
//			return nullptr;//返回一个空指针即可
//		}
//		bool Erase(const K key)
//		{
//			//对于删除就很简单了
//			//因为是一种伪删除,所以直接修改状态,然后让_n--即可
//			HashNode<K, V>* ret = Find(key);
//			if (ret)
//			{
//				ret->_status = DELETE;
//				--_n;
//				return true;
//			}
//			//在这里代表没有找到这个值自然就不能够删除
//			return false;
//		}
//		bool insert(pair<K, V> kv)
//		{
//			//增加了需要扩容的机制所以在这里就需要判断一下是否需要扩容
//			//这里的规则就是当负载因子达到70%左右时需要扩容
//			Hash get;
//			if (Find(kv.first))//这里如果Finde返回的非空,代表在vector中已经存在了key,那么直接返回false
//			{
//				return false;// 代表这个值存在于vector中,不能插入
//			}//到这里代表在原数组中没有key,代表可以插入
//			if (_n * 10 / con.size() == 7)
//			{
//				//需要扩容
//				// 这里不能直接让con.resize(con.size()*2),因为会让原来数据的映射关系发生变化(之前数据的映射关系除的是10),但是这里新的映射关系需要除以20
//				//但是,如果直接是修改con的resize的话,之前所有已经插入的数据,映射关系都会出错
//				HashTable<K, V,Hash> new_table;//创建一个新的hash_table,但是这里这么写也存在一个问题,那就是如果这里的key是一个
//				size_t newsize = con.size() * 2;
//				new_table.con.resize(newsize);//修改这个新哈希表的大小
//				for (auto e : con)
//				{
//					new_table.insert(e._kv);
//				}//将原来的值插入到新的这个哈希表中
//				con.swap(new_table.con);
//			}
//			// 下面就是需要在vector中寻找一个值
//			int hashi = get(kv.first) % con.size();//这些地方都需要使用仿函数去完成修改
//			while (con[hashi]._status != NU&&con[hashi]._status!=DELETE)// 如果当前寻找到的这个空间不是NULL,那就使用线性探测(一个一个往后寻找的方法)
//			{
//				hashi++;
//				hashi %= con.size();
//			}//到这里代表的就是con[hashi]的状态是一个空了,可以插入值了
//			con[hashi]._kv = kv;
//			con[hashi]._status = EXIST;//修改状态,但是在这里我们发现了一个问题,如果需要扩容呢?我们此时并没有解决需要扩容的问题
//			//为了解决这个问题,我们增加了一个叫做负载因子的变量,每插入一个值那就让负载因子++
//			++_n;
//			return true;
//		}
//		void testinsert()
//		{
//		
//			HashTable<int, int> t;
//			t.insert(make_pair(3, 3));
//			t.Erase(3);
//			t.insert(make_pair(3, 3));
//		}
//		void TestHT2()
//		{
//			string arr[] = { "香蕉", "甜瓜","苹果", "西瓜", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉","abc","acb"};
//			//HashTable<string, int, HashFuncString> ht;
//			HashTable<string, int> ht;//因为我已经在上面实现了对string的特化,所以这里即使我们自己不传也可以完成下面的计数
//			for (auto& e : arr)
//			{
//				//auto ret = ht.Find(e);
//				HashNode<string, int>* ret = ht.Find(e);
//				if (ret)
//				{
//					ret->_kv.second++;
//				}
//				else
//				{
//					ht.insert(make_pair(e, 1));
//				}
//			}
//			int a[] = { 1,4,7,8,5,2,9,6,3 };
//			HashTable<int, int> ht1;//如果这里不是string作为key那么特化实现的就是一个普通的仿函数
//			for (auto e : a)
//			{
//				ht1.insert(make_pair(e, e));
//			}
//			int t;
//			cin >> t;
//		}
//	private:
//		vector<HashNode<K, V>> con;//一个普通的哈希table中只会含有一个vector数组去实现
//		size_t _n = 0;// 用于计算负载因子使用,给一个缺省值
//	};
//}
//下面来实现哈希桶
namespace Hash_Bucket
{
	template<class K, class V, class getkey, class HashFunc>
	class HashTable;//在这里声明HashTable的存在
	template<class T>
	struct HashNode
	{
		HashNode* next;
		T _data;
		HashNode(const T& kv)
			:next(nullptr)
			,_data(kv)
		{}//构造函数
	};//哈希桶下的节点
	template<class K, class V,class Ref,class Ptr, class getkey, class HashFunc>
	struct __iterator
	{
		typedef HashNode<V> Node;
		typedef __iterator<K, V,Ref,Ptr,getkey, HashFunc> Self;
		Node* _node; //首先既然是迭代器我们肯定要包含的是一个节点的指针,迭代器的作用是能够找到下一个指针,所以我们还需要一个记录桶位置的表,
			//这里有两种实现方法,第一种就是将HashTable中的vector给传递过来,还有一个方法就是将hashTable中的vector传递过来,这里选择的是将HashTable传递过来
			//其实这里传递HashNodce中的vector实现更为简单,但是为了复习再模板中的友元类所以这里选择是将HashTable传递过来
		const HashTable<K, V, getkey, HashFunc>* _pid; // 为了得到那张表这里选择的是将HashTable传递过来
		size_t _hashw;// 记录当前迭代器所在桶的位置
		__iterator(Node* root,const HashTable<K, V, getkey, HashFunc>* pid,size_t hashw)
			:_node(root)
			,_pid(pid)
			,_hashw(hashw)
		{}//完成构造函数
		Self operator++()
		{
			//那么如何取得下一个节点的迭代器呢?
			Node* next = _node->next;
			if (next)//如果next不是空代表next就是下一个迭代器需要的节点
			{
				_node = next;
			}
			else//next为空,代表当前的这个桶已经被我们遍历完成了,要去下一个桶中遍历节点
			{
				//现在的问题就在于如何获得下一个桶的坐标这就是为什么我们要将HashTable传递过来,但是光有表还是不行的这里我们还需要知道当前的这个桶所在的位置
				//获取位置的方法存在两个第一个现场算
				//getkey getk;
				//HashFunc geth;
				//size_t hashi = geth(getk(_node->_data)) % (_pid->_con.size());
				//这是一种获得所在桶位置的方法
				//下一种方法就是在创建迭代器的时候就将当前桶所在的下标传递过来
				//这里我选择的是第二种方法
				_hashw++;
				while (_hashw<_pid->_con.size())//往后寻找所有的桶中下一个节点的位置
				{
					if (_pid->_con[_hashw])//这个桶中的数据不为空
					{
						_node = _pid->_con[_hashw];
						break;
					}
					_hashw++;
				}
				if (_hashw == _pid->_con.size())//如过所有的桶都已经寻找过了,代表在这个哈希表中已经不存在数据了让_node赋值为空
				{
					_node = nullptr;
				}
			}
			return *this;
		}
		bool operator!=(const Self& b)
		{
			return _node != b._node;
		}
		Ref operator*()
		{
			return _node->_data;
		}
		Ptr operator->()
		{
			return &(_node->_data);
		}
	};
	template<class K,class V ,class getkey, class HashFunc>
	class HashTable
	{
	template<class K, class V, class Ref,class Ptr,class getkey, class HashFunc>
	friend	struct __iterator;// 模板友元的声明也需要带上模板参数
	public:
		typedef __iterator<K, V,V&,V* ,getkey, HashFunc> iterator;//将迭代器放到HashTable中
		typedef __iterator<K, V,const V&, const V*, getkey, HashFunc> const_iterator;
		const_iterator begin() const
		{
			//这里需要返回第一个桶中的第一个数据,
			for (int i = 0; i < _con.size(); i++)
			{
				Node* cur = _con[i];
				if (cur)
				{
					//cur不为空那么cur就是第一个桶中的第一个指针
					return const_iterator(cur, this, i);//这里因为这个迭代器需要的是节点的指针,一个hashTable的指针,还有当前桶的下标
				}
			}
			//在这里代表的就是当前桶中没有数据
			return end();
		}
		const_iterator end() const
		{
			return const_iterator(nullptr, this, -1);//这里使用-1作为end的桶下标
			// 这里直接传this会出错因为const迭代器需要的是一个const this,而这里的this只是一个
		}
		iterator end()
		{
			return iterator(nullptr,this,-1);//这里使用-1作为end的桶下标
		}
		iterator begin()
		{
			//这里需要返回第一个桶中的第一个数据,
			for (int i = 0; i < _con.size(); i++)
			{
				Node* cur = _con[i];
				if (cur)
				{
					//cur不为空那么cur就是第一个桶中的第一个指针
					return iterator(cur, this, i);//这里因为这个迭代器需要的是节点的指针,一个hashTable的指针,还有当前桶的下标
				}
			}
			//在这里代表的就是当前桶中没有数据
			return end();
		}
		HashTable()
		{
			_con.resize(10);//依旧是给与一些初始化的空间
		}
		~HashTable()
		{
			for (int i = 0; i < _con.size(); i++)
			{
				Node* cur = _con[i];
				while (cur)
				{
					Node* Nnext = cur->next;
					delete cur;
					cur = Nnext;
				}//由此完成析构
				//对于vector我们不需要管可以交给编译器底层自己调用析构
			}
		}//这里需要自己完成一个析构函数
		typedef HashNode<V> Node;
		pair<iterator,bool> insert(const V& kv)
		{
			HashFunc geth;//hashfunc这个仿函数的功能为获取hashii
			getkey getk;//这个仿函数的功能为获取对象中的key值
			iterator it = Find(getk(kv));
			if (it!=end())
			{
				//it不是空代表找到了这个值
				return make_pair(it,false);//那么插入自然就是失败了
			}
			if (_n == _con.size())
			{
				//如何扩容呢?
				//首先创建一个vector<Node*>的数组
				vector<Node*> newvector;
				size_t newsize = _con.size() * 2;
				newvector.resize(newsize);
				//这里需要扩容创建空间
				//这里扩容的规则为遍历原哈希桶,然后将这个哈希桶中的节点拿出来,然后放到新的哈希桶中
				//这里并不是创建一个新的哈希节点而是将上面的节点直接拿出来
				for (int i = 0; i < _con.size(); i++)
				{
					Node* cur = _con[i];
					while (cur)
					{
						//记录当前桶的下一个节点
						Node* Nnext = cur->next;
						//计算当前节点新桶所在的位置
						size_t hashi = geth(getk(cur->_data)) % newsize;
						cur->next = newvector[hashi];
						newvector[hashi] = cur;
						cur = Nnext;
					}//当这个循环结束的时候还有一个重要的步骤
					_con[i] = nullptr;//原哈希桶的值转移完成之后,直接赋值为空
				}
				//完成交换之后
				_con.swap(newvector);//最后交换这个新哈希表的_con
			}
			size_t hashi = geth(getk(kv)) % _con.size();//首先获取一个hashi
			Node* newnode = new Node(kv);//先创建一个新的节点
			newnode->next = _con[hashi];
			_con[hashi] = newnode;//完成头插
			_n++;//用于计算负载因为的_n++
			return make_pair(iterator(newnode,this,hashi),true);//插入成功了那么就返回这个新插入指针的迭代器。
		}//现在就来写插入函数
		iterator Find(const K& key)
		{
			HashFunc geth;
			getkey getk;
			//下面我们就来完善Find函数
			int hashi = geth(key) % _con.size();
			Node* cur = _con[hashi];
			while (cur)
			{
				if (getk(cur->_data) == key)
				{
					return iterator(cur,this,hashi);// 找到了这个值就将这个值的迭代器返回去
				}
				cur = cur->next;
			}
			//整个桶都完成之后直接返回nullptr
			return end();//找不到直接返回end()
		}
		bool Erase(const K& key)
		{
			HashFunc geth;
			getkey getk;
			int hashi = geth(key) % _con.size();
			Node* prev = nullptr;
			Node* cur = _con[hashi];
			while (cur)
			{
				if (getk(cur->_data) == key)
				{
					if (prev == nullptr)//如果前驱节点为空,那么代表这是一个头节点的删除
					{
						_con[hashi] = cur->next;//先让cur的next成为_con[hashi]的值,然后去删除cur
						delete cur;
					}
					else//代表此时的前驱节点不为空
					{
						prev->next = cur->next;//将cur后面的节点链接到prev的后面
						delete cur;
					}
					return true;//代表删除成功了
				}
				cur = cur->next;//在当前的桶中寻找需要删除的节点
			}
			return false;//删除失败
		}
	private:
		vector<Node*> _con;//哈希桶
		size_t _n = 0;//负载因子
	};
}

上面的代码包含了哈希桶(开散列的方式)和哈希表的闭散列方式(被注释的代码)。

然后下面就是map和set的封装了:

namespace LHY {
	template<class K, class V>
	class unordered_map
	{
	public:
		template<class K>
		struct Hash
		{
			size_t operator()(const K& key)
			{
				return (size_t)key;
			}
		};
		template<>
		struct Hash<string>
		{
			size_t operator()(const string& key)
			{
				size_t tmp = 0;
				for (auto e : key)
				{
					tmp += e;
					tmp *= 31;
				}
				return tmp;
			}
		};
		struct mapofkey
		{
			K operator()(const pair<K, V>& v)
			{
				return v.first;//返回first
			}
		};
		typedef typename Hash_Bucket::HashTable<K, pair<const K, V>,mapofkey, Hash<K>>::const_iterator const_iterator;
		typedef typename Hash_Bucket::HashTable<K, pair<const K, V>, mapofkey, Hash<K>>::iterator iterator;
		// 这里因为是从HashTable中取一个类型所以需要增加一个typename。
		const_iterator begin() const
		{
			return _con.begin();
		}
		const_iterator end() const
		{
			return _con.end();
		}
		iterator begin()
		{
			return _con.begin();
		}
		iterator end()
		{
			return _con.end();
		}
		pair<iterator,bool> insert(const pair<K, V>& k)//要完成[]的重载就需要修改一下insert函数
		{
			return _con.insert(k);
		}
		V& operator[](const K& key)
		{
			pair<iterator, bool> ret = insert(make_pair(key, V()));//这里构造一个pair这个pair的key使用传过去的key,value使用默认的value
			return ret.first->second;
		}
		iterator find(const K& key)
		{
			return _con.Find(key);
		}
		bool erase(const K key)
		{
			return _con.Erase(key);
		}
	private:
		Hash_Bucket::HashTable<K, pair<const K, V>, mapofkey, Hash<K>> _con;//从这里将K,和V以及获取Hashi和mapoofkey的方法放过去
	};
}

set的封装:

namespace LHY {
	template<class K>
	class unordered_set
	{
	public:
		template<class K>
		struct Hash
		{
			size_t operator()(const K& key)
			{
				return (size_t)key;
			}
		};
		template<>
		struct Hash<string>
		{
			size_t operator()(const string& key)
			{
				size_t tmp = 0;
				for (auto e : key)
				{
					tmp += e;
					tmp *= 31;
				}
				return tmp;
			}
		};
		struct setofkey
		{
			const K& operator()(const K& key)
			{
				return key;//对于set而言key返回的就是key
			}
		};
		typedef typename Hash_Bucket::HashTable<K,K,setofkey,Hash<K>>::const_iterator iterator;
		typedef typename Hash_Bucket::HashTable<K,K,setofkey,Hash<K>>::const_iterator const_iterator;
		const_iterator begin() const
		{
			return _con.begin();
		}
		const_iterator end() const
		{
			return _con.end();
		}
		pair<const_iterator,bool> insert(const K& k)
		{
			auto ret = _con.insert(k);//首先将insert的返回值得到
			return make_pair(const_iterator(ret.first._node, ret.first._pid, ret.first._hashw), ret.second);
			//这里使用ret中的first构造一个const迭代器,在使用红黑树封装的时候,我们这里的解决方法是pair<Node*,bool>就能够让Node*取构造
			//迭代器,但是这里的迭代器,只使用一个节点的指针是无法构造的所以这里必须使用这种方法去构造一个
			//迭代器
		}
		iterator find(const K& key)
		{
			return _con.Find(key);
		}//这里是简单版的find函数如果ret为空代表没有找到返回false
		// 找到则返回true
		bool erase(const K key)
		{
			return _con.Erase(key);
		}
	private:
		Hash_Bucket::HashTable<K, K, setofkey, Hash<K>> _con;//这里其实就应该将转换hashi的那个仿函数放到这里传过去
	};//这里既然要封装unordered_set那么就回到之前使用红黑树封装set的时候,我们需要考虑如何获得一个set对象中的key
	//方法依旧是使用仿函数
}

希望这篇博客能对你有所帮助,写的不好请见谅,如果发现了任何的错误,欢迎指出。

标签:map,set,cur,hashi,key,哈希,size,unordered,con
From: https://blog.51cto.com/u_15838996/8489383

相关文章

  • timeSetEvent()函数定时器的使用
    1、定时器函数的使用微软公司在其多媒体Windows中提供了精确定时器的底层API支持,利用多媒体定时器可以很精确地读出系统的当前时间,并且能在非常精确的时间间隔内完成一个事件、函数或过程的调用。利用多媒体定时器的基本功能,可以通过两种方法实现精确定时。(1)使用timeGetTime()......
  • memset,memcpy,strcpy 有什么区别?
    memsetmemset的作用是在一段内存块中填充某个给定的值。voidmemset(void*ptr,intvalue,size_tnum);将 ptr 指向的内存块从前往后num个字节用value替换。返回值为ptrintmain(){ chararr[]="abc"; memset(arr,'s',2); printf("%s",arr); return0;}//输......
  • java反序列化----CC6利用链学习笔记(HashMap和HashSet)
    目录java反序列化----CC6利用链学习笔记环境配置利用链java反序列化----CC6利用链学习笔记环境配置jdk8(无版本要求)pom.xml中写入<dependency><groupId>commons-collections</groupId><artifactId>commons-collections</artifactId>......
  • es定制 dynamic mapping template(type)
    定制dynamicmappingtemplate(type)PUT/my_index{"mappings":{"my_type":{"dynamic_templates":[{"en":{"match":"*_en","match_mapping_type":"string","mapping&quo......
  • Java中的Set集合之TreeSet
    TreeSet:TreeSet是一个有序集合,它扩展了AbstractSet类并实现了NavigableSet接口。以下是此实现最重要方面的快速摘要:它存储唯一的元素它不保留元素的插入顺序它按升序对元素进行排序它不是线程安全的在该实现中,对象根据其自然顺序以升序排序和存储。该TreeSet中使用的是一......
  • C# 22H2之后的windows版本使用SetDynamicTimeZoneInformation设置时区失败处理
    使用SetDynamicTimeZoneInformation设置时区返回false,设置失败。使用PowerShell设置Set-TimeZone成功。///<summary>///设置本地时区///参数取值"ChinaStandardTime",即可设置为中国时区///</summary>///<paramname="timeZoneId"></param>///<retur......
  • jdk8 Stream流中将集合转成map,重复key处理,统计最大值,获取某个属性集合等10种最常用方
    jdk8Stream流中将集合转成map,重复key处理,统计最大值,获取某个属性集合等10种最常用方法......
  • qemu-kvm: error: failed to set MSR x38d to x0x 【问题解决】
    问题解决创建报错在下面的issues找到解决办法https://github.com/GNS3/gns3-server/issues/1774可以尝试在VM上禁用MSR,然后检查是否可以启动qemu计算机添加内核模块参数临时修改echoY>/sys/module/kvm/parameters/ignore_msrs或者永久修改cat>/etc/modp......
  • Python、Spark SQL、MapReduce决策树、回归对车祸发生率影响因素可视化分析
    原文链接:https://tecdat.cn/?p=34286原文出处:拓端数据部落公众号分析师:ShichaoZhong项目挑战如何处理庞大的数据集,并对数据进行可视化展示;在后续分析中特征选择是重点之一,要根据事实情况和数据易处理的角度来筛选变量解决方案任务/目标根据已有的车祸数据信息,计算严重车祸......
  • go map删除元素后内存是否会释放
    go底层map是由若干个bmap(桶)构成的,桶只会扩容,不会缩容,所以map中占用的内存不会被释放但是!!!以上只针对值类型的数据结构例如:基本类型intstringslicestruct等如果key为指针变量删除后这个指针变量内存不会释放,但是这个指针指向的对象,引用计数会-1如果引用......