【C++进阶】用哈希表封装unordered_set和unordered_map

本文介绍了如何改造C++的哈希表结构,解决k/v参数冲突,以及对unordered_set和unordered_map的插入、查找和删除操作进行优化。同时讨论了迭代器的实现,包括正向迭代器、const迭代器和operator[]的扩展。此外,还涉及了素数表在哈希表优化中的应用。
摘要由CSDN通过智能技术生成

在这里插入图片描述

👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨


一、改造哈希表

1.1 解决 k/v 参数冲突问题

在模拟实现mapset时,我们知道set<K,K>模型的红黑树,map<K,pair>模型的红黑树,而真正决定树里存储什么,是由第二个模板参数决定的,这也就是为什么mapset可以共用一颗树。

unordered系列容器也是如此,其中unordered_set<K,K>模型的哈希表,unordered_map<K,pair>模型的哈希表

【Unordered_set.h】

在这里插入图片描述

【Unordered_map.h】

在这里插入图片描述

由于unordered系列的底层使用的也是同一个哈希表,因此,真正决定表里存储什么,也是依靠第二个模板参数决定。

【OpenHashTable.h】

在这里插入图片描述

1.2 插入操作的改造

首先我们来分析插入的模板参数应该是什么?对于unordered_set就是key;对于unordered_map则是pair。那么参数类型应该用第二个模板参数V接收。

在这里插入图片描述

但这里就遇到了一个尴尬的问题:对于unordered_map来说,kv.first就是key;而对于set来说,kv就是key

因此,解决方法和map/set一样:写一个仿函数。首先在unordered_mapunordered_set里面写两个内部类,这个内部类重载运算符()。对于unordered_map返回pairfirst,对于unordered_set返回它本身,也就是key

【Unordered_set.h】

在这里插入图片描述

【Unordered_map.h】

在这里插入图片描述

这下就可以对哈希表进行改造了

【OpenHashTable.h】

在这里插入图片描述

1.3 查找操作的改造

在这里插入图片描述

1.4 删除操作的改造

在这里插入图片描述

二、迭代器

2.1 封装正向迭代器

哈希表的正向迭代器和链表类似,实际上就是对哈希结点指针进行了封装

在这里插入图片描述

但是由于在实现++运算符重载时,可能在当前坑位就有非空结点,那么_node = _node->_next来遍历下一个非空结点,也有可能下一个就是空结点了,因此就需要在哈希表中去寻找下一个非空坑位,因此每一个正向迭代器中还都应该存储哈希表的地址(指针) 来帮助我们判断表中的下一个坑位是否为空

在这里插入图片描述

然后再在迭代器类__HTIterator中实现operator!=operator*operator->,那么遍历操作就已经完成一半了

在这里插入图片描述

接着在类HashTable中封装begin()end()

在这里插入图片描述

最后分别在Unordered_map.hUnordered_set.h中调用begin()end()

【Unordered_map.h】

在这里插入图片描述

【Unordered_set.h】

在这里插入图片描述

当运行时发现编译不通过!!

在这里插入图片描述

这里其实存在相互依赖的问题,也就是说这里存在相互使用

在这里插入图片描述

什么意思呢?假设是unordered_set使用哈希表迭代器,那么迭代器的定义要在哈希表之前,因为代码会向上搜索;而又因为迭代器类要用到哈希表,而代码向上搜索时没发现哈希表的定义,因此在迭代器类中typedef HashTable<K, V, GetKey, HashFunc> HashPtr其实是未定义的。

解决方式:在迭代器类前声明哈希表即可但是要注意声明格式:参数列表 + class类名

在这里插入图片描述

接下来运行看看结果:

在这里插入图片描述

可是又报错了,原因是在__HTIterator中,访问了类HashTable的私有成员_table(类外不允许访问私有成员)
在这里插入图片描述

解决方法:友元声明。迭代器是哈希表的友元

在这里插入图片描述

【测试结果】

在这里插入图片描述

这里我们对unordered_map遍历使用范围for,因为范围for的底层就是迭代器

在这里插入图片描述

2.2 const迭代器

但是以上代码还是不完美,不管是unordered_setunordered_map,都可以修改key,一旦修改了,那么就意味着这表已经乱了。

回想一下当时模拟实现map/set

在这里插入图片描述

因此,unordered系列也可以模仿以上操作:对迭代器类增加两个模板参数来控制迭代器的operator*operator->的返回值

首先修改迭代器类在哈希表的声明,并增加const版本的beginend接口供unordered系列容器调用:

在这里插入图片描述

当然了,咱们自定义类型的迭代器也得修改

在这里插入图片描述

【Unordered_Set.h】

在这里插入图片描述

先来简单测试一下:

在这里插入图片描述

接下来再来测试const迭代器遍历

在这里插入图片描述

通过排查,是哈希表封装的beginend出问题了

在这里插入图片描述

原因如下:

在这里插入图片描述

解决办法很简单,由于迭代器不会修改哈希表的内容,因此对哈希表指针用const修饰

在这里插入图片描述

【Unordered_Map.h】

在这里插入图片描述

2.3 unordered_map 新增 operator[]

在这里插入图片描述

operator[]原理是:通过调用insert来查找键值key来返回实值value的引用

  • 如果键值key已经在表里,那么就返回表里面key所在结点的迭代器
  • 如果键值key不在表里,那么就返回新插入key所在结点的迭代器

所以还得将insert的返回值再进一步完善

在这里插入图片描述

【OpenHashTable.h】

首先由于insert操作中有用到Find函数,因此要Find函数的返回值改为迭代器

在这里插入图片描述

接下来可以修改insert的返回值了

在这里插入图片描述

接着再重新修改unordered_mapunordered_set封装insert的返回值

【Unordered_map.h】

在这里插入图片描述

【Unordered_set.h】

在这里插入图片描述

当编译的时候发现还是不通过

在这里插入图片描述

这是当时我们封装mapset一样的问题:_ht是一个普通对象,去调用哈希表的insert,那么返回的是普通的迭代器;而在unordered_set中,迭代器都是const迭代器。

就这么说吧,以上代码在编译器眼中其实是这样的:

在这里插入图片描述

又由于pair是一个类模板,类模板实例化不同的模板参数就是不同的类型。例如定义vector<int> vivector<char> vc,将vc赋值给vi成立吗?显然不可能!

在这里插入图片描述

那么如何解决呢?可以想想不同类型的内置类型是如何相互转化的?

在这里插入图片描述
因此,可以对迭代器类写一个拷贝构造函数:将普通迭代器构造成const迭代器

这种list容器其实也有一样的玩法:

在这里插入图片描述

如上代码,普通对象l调用begin返回普通迭代器,然后再赋值普通迭代器合情合理,但是也可以赋值给const迭代器,按常理普通迭代器赋值给const迭代器是不允许的!如果允许将普通迭代器赋值给常量迭代器,那么就可能导致通过常量迭代器意外地修改数据,违反了常量迭代器的设计初衷。

因此,我们可以简单看看list的底层:

在这里插入图片描述

也就是说:list底层有一个拷贝构造函数,支持普通迭代器转化为const迭代器。

理论部分解释完了,那么现在就对__HTIterator类添加一个拷贝构造函数

【OpenHashTable.h】

在这里插入图片描述

  • 如果__HTIterator实例化成普通迭代器,重命名的iterator永远是一个普通迭代器,那么这个拷贝构造函数其实就是一个赋值。
  • 如果__HTIterator实例化成const迭代器,重命名的iterator永远是一个普通迭代器,那么iterator对象就会隐式转化为__HTIterator类型,也就实现了普通迭代器转化为const迭代器

自此,unordered_set最终insert接口如下:

在这里插入图片描述

解释:第一行_ht调用哈希表中的insert返回的是普通迭代器;第二行再通过pair的构造,对于firstsecond成员变量都会走初始化列表初始化,而这里的first恰好是一个自定义类型,那么自定义类型在初始化列表初始化必须调用它的构造函数,因此就可以实现普通迭代器转化为const迭代器。

最后就可以在unordered_map封装operator[]

在这里插入图片描述

三、优化:素数表

  • 使用除留余数法时,哈希表的大小最好是素数,这样能够减少哈希冲突产生的次数

SGI STL 中,哈希表 在扩容时就使用了这一技巧

在这里插入图片描述

简单来说,就是当我们扩容后,按照下一个素数值大小进行扩容

这些素数都是近似 2倍的大小关系,在确保不会频繁扩容的同时,尽可能减少哈希冲突

在这里插入图片描述

同样的,需要对插入函数进行改造

在这里插入图片描述

四、源码

Gitee仓库链接:点击跳转

  • 29
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
unordered_set和unordered_mapC++11中新增加的两个关联式容器,它们的区别主要体现在以下几个方面: 1. 底层实现:unordered_set和unordered_map的底层都是哈希表,而setmap的底层是红黑树。哈希表是一种根据键值直接进行访问的数据结构,而红黑树是一种自平衡的二叉搜索树。 2. 排序:unordered_set是不可排序的,而set是有序的。unordered_map是无序的,而map是有序的。这是因为哈希表是根据键值的哈希值进行存储和访问的,没有固定的顺序。 3. 迭代器:unordered_set和unordered_map使用的是单向迭代器,而setmap使用的是双向迭代器。单向迭代器只能从前往后遍历容器中的元素,而双向迭代器可以从前往后和从后往前遍历。 4. 效率:由于底层实现的不同,unordered_set和unordered_map的插入、查找和删除操作的时间复杂度都是O(1),而setmap的时间复杂度是O(logN)。因此,unordered_set和unordered_map相对于setmap来说,在大部分情况下具有更高的效率。 综上所述,unordered_set和unordered_mapsetmap在底层实现、排序、迭代器和效率上存在一些区别。选择使用哪个容器取决于具体的需求和性能要求。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [C++ 哈希表及unordered_set + unordered_map容器](https://blog.csdn.net/qq_60750110/article/details/126746419)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [算法(42)-数组等值切割-前缀累加和-哈希表Map-set版-C++](https://download.csdn.net/download/weixin_38710566/14039060)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值