C++ | 哈希 | 基于开散列结构的unordered系列容器模拟实现

news2024/12/26 23:48:28

文章目录

    • unordered_map的封装
      • 所有接口的声明与实现
      • operator[]重载
    • unordered_set的封装

上篇博客模拟实现了哈希的开散列结构,并且将迭代器与泛型进行了封装,至此我们可以将开散列作为底层结构对STL标准容器——unordered_map和unordered_set进行封装。但是在封装之前还需要对我们实现的开散列结构进行改造在这里插入图片描述
在这里插入图片描述
可以看到标准库中的插入接口会返回一个pair对象,因为这两个容器是不允许键值冗余的,所以如果插入成功(容器中没有同样的key值),insert返回的pair对象的first迭代器指向了插入后,该节点在容器中的位置,并且second对象为true。如果插入失败,pair的first迭代器指向了容器中已经存在的节点的位置,second对象为false。下面是修改后的插入接口

template <class K, class T, class KeyOfT, class HashTrans>
pair<typename HashBucket<K, T, KeyOfT, HashTrans>::iterator, bool> HashBucket<K, T, KeyOfT, HashTrans>::Insert(const T& data)
{
	// 仿函数对象的定义
	KeyOfT get_key;
	HashTrans trans;

	iterator it = Find(get_key(data));
	if (it._pnode) // 出现了键值冗余
	{
		return make_pair(it, false);
	}

	if (_bucket.size() == 0 || _n * 10 / _bucket.size() >= 7) // 负载因子的维护,扩容
	{
		// 计算新桶的大小
		size_t new_size = _bucket.size() == 0 ? 10 : 2 * _bucket.size();

		// 新桶的创建与空间开辟
		HashBucket new_bucket;
		new_bucket._bucket.resize(new_size);

		// 遍历旧表完成节点转移
		for (size_t i = 0; i < _bucket.size(); ++i)
		{
			Data* cur = _bucket[i];
			// 当头指针不为空,进入遍历单链表
			while (cur)
			{
				Data* next = cur->_next;
				// 获取新的哈希值
				size_t new_hashi = trans(get_key(cur->_data)) % new_size;
				// 头插操作
				cur->_next = new_bucket._bucket[new_hashi];
				new_bucket._bucket[new_hashi] = cur;

				// 更新遍历旧表的cur
				cur = next;
			}
			_bucket[i] = nullptr; // 好习惯,这里不置空是真的会出问题
		}
		// 交换完成后将新旧表交换
		_bucket.swap(new_bucket._bucket);
	}

	// 获取哈希值
	size_t hashi = trans(get_key(data)) % _bucket.size();
	// 构造节点
	Data* new_node = new Data(data);
	// 将节点头插
	new_node->_next = _bucket[hashi];
	_bucket[hashi] = new_node;
	// 记得负载因子的维护
	++_n;
	return make_pair(iterator(new_node, this), true);
}

unordered_map的封装

我们知道,哈希桶作为作为底层的结构HashBucket,需要接收上层结构传入的模板参数,包括泛型以及支持key值提取(从泛型数据中提取出key值)和哈希转换函数(将key值转换成可以被除模的整形数据),这是HashBucket的模板参数

template <class K, class T, class KeyOfT, class HashTrans>

其中K是key值的类型,因为有些函数需要key值作为参数或者返回值,所以需要把K值的类型单独接收。T是泛型的类型,T可能是int,可能是string,可能是pair对象,总之就是容器需要存储的数据的类型。然后KeyOfT和HashTrans就是key值提取和哈希转换的仿函数接收。对于上层结构
unordered_map和unordered_set,它们的模板参数化不需要接收仿函数以提取key值。因为set容器存储的就是一个key值,所以set需要设计一个仿函数,其接收一个泛型对象,然后再将泛型对象返回(因为对于set,其泛型对象就是key值,不需要什么提取),封装底层结构HashBucket时将仿函数传入。而map容器存储的是一个pair值,first为key对象,second为value对象,所以map容器需要设计一个仿函数,返回pair对象的first值,并将其传入底层容器HashBucket。为什么底层容器HashBucket需要接收key值提取仿函数,因为对于使用它的上层容器来说,它们知道要怎么从存储的数据中提取key值,而作为底层容器,它怎么知道使用者存储数据的key值要怎么提取?所以这里需要接收仿函数。

而哈希转换的仿函数,不论是底层结构,还是上层结构都需要接收。这是为什么?因为哈希映射到数组时需要取模,取模的对象必须是一个整数,但key值可以是任意类型的数据,不论底层还是上层,哪知道更上层的用户需要存储什么类型的数据?只是对一个特殊类型,如string,上层容器可以通过函数特化构建哈希转换,但是数据类型是无限的,容器只能根据一些典型的数据进行模板特化,对于自定义类型的哈希转换就需要我们手动构建并传入。

// 这是unordered_map的模板参数,接收key值与value值
// 将它们封装成pair对象进行存储,用户还可以传入哈希转换函数
// 如果不传入则使用默认的转换函数,这个函数只是将数据强转成size_t
// 如果此时key值并不是整数,程序将出错
template <class K, class V, class HashTrans = DefaultTrans<K>>

所有接口的声明与实现

template <class K>
struct DefaultTrans
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

struct StringTrans
{
	size_t operator()(const string& str)
	{
		size_t ret = 0;
		for (auto c : str)
		{
			ret = ret + 131 * c;
		}
		return ret;
	}
};


template <class K, class V, class HashTrans = DefaultTrans<K>>
class unordered_map
{
public:
	typedef pair<K, V> data_type;

	struct key_of_data
	{
		const K& operator()(const data_type& data)
		{
			return data.first;
		}
	};

	typedef HashBucket<K, data_type, key_of_data, HashTrans> Bucket;
	typedef __HashIterator<K, data_type, key_of_data, HashTrans> map_iterator;
	// 迭代器接口
	map_iterator begin() { return _hash_bucket.begin(); }
	map_iterator end() { return _hash_bucket.end(); }

	// 修改接口
	pair<map_iterator, bool> Insert(const data_type& data) { return _hash_bucket.Insert(data); }
	bool Erase(const K& key) { return _hash_bucket.Erase(key); }

	// 查找接口
	map_iterator Find(const K& key) { return _hash_bucket.Find(key); }

	// []的重载
	V& operator[](const K& key)
	{
		// 创建一个pair对象,其中second为默认值
		data_type data = make_pair(key, V());
		// 将data插入到哈希桶中,接收其返回值
		pair<map_iterator, bool> ret_it = _hash_bucket.Insert(data);
		// 返回插入后的value引用
		return (ret_it.first)->second;
	}
private:
	Bucket _hash_bucket;
};

其中比较重要的是对底层结构HashBucket

typedef pair<K, V> data_type;
typedef HashBucket<K, data_type, key_of_data, HashTrans> Bucket;
typedef __HashIterator<K, data_type, key_of_data, HashTrans> map_iterator;

封装底层结构时,由于HashBucket的第一个模板参数是key值的类型,所以我们把K传入即可,第二个参数是存储的数据类型,由于map存储键值对,所以我们将pair对象封装成data_type,将其传入HashBucket的模板参数。至于最后两个就是key值提取和哈希转换仿函数的传参。将unordered_map和unordered_set对底层结构的封装对比

typedef HashBucket<K, K, key_of_data, HashTrans> Bucket;
typedef __HashIterator<K, K, key_of_data, HashTrans> set_iterator;

因为底层结构HashBucket的第二个模板参数是一个泛型,map传入pair对象,set传入的就是key值对象,所以这就是封装Bucket时,为什么前两个参数都是K的原因

operator[]重载

V& operator[](const K& key)
{
	// 创建一个pair对象,其中second为默认值
	data_type data = make_pair(key, V());
	// 将data插入到哈希桶中,接收其返回值
	pair<map_iterator, bool> ret_it = _hash_bucket.Insert(data);
	// 返回插入后的value引用
	return (ret_it.first)->second;
}

对于map的operator[]重载是一个老生常谈的问题,这里再说明一下。重载函数接收一个key值,因为map存储的是一个键值对,所以可以用传入的key和value的默认值创建一个pair对象,调用insert接口将其插入,并接收其返回的pair对象。pair的first为一个迭代器,指向插入数据(无论是否存在,不存在会先插入再返回,存在会直接返回)所在的位置,因为迭代器指向的数据是一个键值对,函数返回键值对的second对象引用,就是返回键值对的value值引用。综上,operator[]的作用类似于插入,不同的是该重载将key值作为函数参数,用一个默认value值与之配对后插入,但是如果key值之前存在,插入是无法成功的,但其最不同的地方是value引用的返回,也就是用将key值传入后,不论key值之前是否存在,我们都可以修改其value值,因为函数返回的是引用。

unordered_set的封装

template <class K>
struct DefaultTrans
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

template <class K, class HashTrans = DefaultTrans<K>>
class unordered_set
{
	struct key_of_data
	{
		size_t operator()(const K& data)
		{
			return (size_t)data;
		}
	};
public:
	typedef HashBucket<K, K, key_of_data, HashTrans> Bucket;
	typedef __HashIterator<K, K, key_of_data, HashTrans> set_iterator;
	
	// 迭代器接口
	set_iterator begin() { return _hash_bucket.begin(); }
	set_iterator end() { return _hash_bucket.end(); }

	// 修改接口
	pair<set_iterator, bool> Insert(const K& key) { return _hash_bucket.Insert(key); }
	bool Erase(const K& key) { return _hash_bucket.Erase(key); }

	// 查找接口
	set_iterator Find(const K& key) { return _hash_bucket.Find(key); }
private:
	Bucket _hash_bucket;
};

关于这两个上层容器的接口实现,主要都是复用底层容器HashBucket,没有什么技术含量,所以这里不再赘述。真正需要理解的地方是,底层容器对泛型的封装,与其在上层容器中的使用,还有哈希转换与key值提取仿函数,在上层容器与底层容器中是否要使用函数模板接收?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/178055.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用Vue 简化 用户查询/添加功能

使用Vue简化 用户查询/添加功能1. 查询功能1.1 Vue核心对象&#xff1a;1.2 brand.html&#xff1a;1.3 selectAllServlet&#xff08;无变化&#xff09;&#xff1a;2. 添加功能2.1 addBrandhtml&#xff1a;2.2 Vue核心对象&#xff1a;2.3 addServlet&#xff08;无变化&am…

网关zuul源码解析==ZuulServlet

用法&#xff1a; 使用zuul网关&#xff0c;需要引入starter为 <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> 同时在springboot启动类上加注解…

7、代码模板的使用

文章目录7、代码模板的使用7.1 查看Postfix Completion模板&#xff08;后缀补全&#xff09;7.2 查看Live Templates模板&#xff08;实时模板&#xff09;7.3 常用代码模板1 非空判断2 遍历数组和集合3 输出语句4 对象操作5 静态常量声明7.4 自定义代码模板1 自定义Postfix C…

Android入门第60天-MVVM中的Databinding与ListView结合使用

开篇 还记得我们进入Listview、GridView都是以一个layoutadapter组合在一起来实现的是吧&#xff1f;那么还记得我们的Adapter的写法么&#xff1f; 在我们的Adapter里提供了一个bindView方法 。 在调用时我们需要在Activity里把layout里的控件元素一个个传给这个Adapter。 在我…

【Linux】文件权限

本期我们来谈谈Linux上的权限&#xff1a;一、权限的概念在生活中我们处处都会遇到权限。权限是什么呢&#xff1f;下面是对于权限的定义&#xff1a;&#x1f4cc;权限&#xff1a;一件事是否允许被“谁”做&#x1f4cc;简化一下就是&#xff1a;权限人事物属性&#x1f4cb;…

【ROS-Navigation】—— Astar路径规划算法解析

文章目录前言1. 导航的相关启动和配置文件1.1 demo01_gazebo.launch1.2 nav06_path.launch1.3 nav04_amcl.launch1.4 nav05_path.launch1.5 move_base_params.yaml1.6 global_planner_params.yaml2. Astar路径规划算法解析2.1 astar.h2.2 astar.cpp参考文献前言 最近在学习ROS的…

《职场求生攻略》学习笔记 Day8

系列文章目录 这是本周期内系列打卡文章的所有文章的目录 《Go 并发数据结构和算法实践》学习笔记 Day 1《Go 并发数据结构和算法实践》学习笔记 Day 2《说透芯片》学习笔记 Day 3《深入浅出计算机组成原理》学习笔记 Day 4《编程高手必学的内存知识》学习笔记 Day 5NUMA内存知…

JavaEE-多线程进阶

✏️作者&#xff1a;银河罐头 &#x1f4cb;系列专栏&#xff1a;JavaEE &#x1f332;“种一棵树最好的时间是十年前&#xff0c;其次是现在” 目录常见的锁策略乐观锁 vs 悲观锁轻量级锁 vs 重量级锁自旋锁 vs 挂起等待锁互斥锁 vs 读写锁公平锁 vs 非公平锁可重入锁 vs 不可…

恶意代码分析实战 8 恶意代码行为

8.1 Lab 11-01 代码分析 首先使用strings进行分析。 Gina是在 msgina.dll中的。 很多有关资源的函数。 关于注册表的函数。 使用ResourceHacker查看。 发现是一个PE文件。 保存为dll文件。 动态分析 启动Promon。 进入注册表查看。 这个恶意代码向磁盘释放了什么&…

分享140个ASP源码,总有一款适合您

ASP源码 分享140个ASP源码&#xff0c;总有一款适合您 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&#xff0c; 140个ASP源码下载链接&#xff1a;https://pan.baidu.com/s/1vxAMMEI7WYS8SAnfbJKdGQ?pwdsas8 提取码&#x…

【ARIXV2209】Multi-Scale Attention Network for Single Image Super-Resolution

【ARIXV2209】Multi-Scale Attention Network for Single Image Super-Resolution 代码&#xff1a;https://github.com/icandle/MAN 这是来自南开大学的工作&#xff0c;将多尺度机制与大核注意机制结合&#xff0c;用于图像超分辨率。 2022年初&#xff0c;大核卷积火了&a…

GBD学习

GBD gbd官网 GDB, the GNU Project debugger 是一个debug工具 支持多种语言&#xff1a; Ada、Assembly、C、C、D、Fortran、Go、 Objective-C、OpenCL、Modula-2、Pascal、Rust 编译文件 首先使用gcc -g .c文件 -o 可执行文件名 进行编译&#xff0c;再使用gdb 可执行文件名…

恶意代码分析实战 10 数据加密

10.1 Lab13-01 比较恶意代码中的字符串&#xff08;字符串命令的输出&#xff09;与动态分析提供的有用信息&#xff0c;基于这些比较&#xff0c;哪些元素可能被加密&#xff1f; 使用WireShark进行动态分析。 有一串字符看起来像是加密的。 使用Strings分析一下。 发现疑似…

KUKA机器人初次通电配置

安全配置 机器人KSS系统里&#xff0c;会提示选择机器人信息的对话框&#xff0c;选择“机器人”按钮&#xff0c;如下&#xff1a; 承接上一步骤&#xff0c;通过示教器确认所有消息&#xff0c;单击消息提示区域&#xff0c;此时一定会弹出如图 所标示的报警消息&#xff0…

【唐诗学习】一、古诗概述

一、古诗概述 为什么要学习古诗词&#xff1f; 古诗词可以陶冶情操&#xff0c;传承文化诗词&#xff0c;其实就是古代的流行歌曲&#xff0c;它们记录着一个个时代的变迁&#xff0c;是历史的旋律。还有一点很重要&#xff1a;同样是记录历史&#xff0c;史书是国家视角&…

零基础学JavaWeb开发(二十二)之 springmvc入门到精通(2)

3、SpringMVC PostMan工具使用 PostMan简介 Postman 是一款功能超级强大的用于发送 HTTP 请求的 Chrome插件 。做web页面开发和测试的人员会使用到该工具其主要特点 特点&#xff1a; 创建 测试&#xff1a;创建和发送任何的HTTP请求&#xff0c;使用PostMan发送 Get、Post、…

Java IO流之字符流详解

字符流概述 字符流的底层其实就是字节流 字符流 字节流 字符集 字符流特点 输入流&#xff1a;一次读一个字节&#xff0c;遇到中文时&#xff0c;一次读多个字节输出流&#xff1a;底层会把数据按照指定的编码方式进行编码&#xff0c;变成字节再写到文件中 使用场景 用于…

学习Go的全部网站集合

给Golang 入门新手整理了一份全部所需网站集合&#xff0c;主要分为三类。 社区类&#xff1a;这类网站是问答、文档、搜索、资源类网站汇总 镜像和安装包类&#xff1a;有些安装包可能无法下载&#xff0c;此处列出一些可下载Go包和镜像的网站。 开发工具&#xff1a;目前流…

代码随想录--栈与队列章节总结

代码随想录–栈与队列章节总结 1.LeetCode232 用栈实现队列 请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作&#xff08;push、pop、peek、empty&#xff09;&#xff1a; 实现 MyQueue 类&#xff1a; void push(int x) 将元素 x 推到队列的末尾i…

2.4.2 浮点型

1.浮点型基本数据类型介绍 浮点类型用于表示有小数部分的数值。在JAVA种有俩种浮点类型&#xff0c;分别是float和double. 类型字节长度位数取值范围float4字节32约 3.40282347E38Fdouble8字节64约 1.79769313486231570E308 double表示这种类型的数值精度是float类型的俩倍&a…