【C++高阶】深度剖析:从零开始模拟实现 unordered 的奥秘

news2024/11/15 15:34:13

📝个人主页🌹:Eternity._
⏩收录专栏⏪:C++ “ 登神长阶 ”
🤡往期回顾🤡:哈希底层
🌹🌹期待您的关注 🌹🌹

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

❀哈希

  • 📒1. 改造 HashTable
  • 📜2. HashTable的迭代器
    • 🌞迭代器基本设计
    • 🌈operator++()
    • 🌙begin()与end()
    • ⭐迭代器的构造
  • 📚3. Unordered_Set的模拟实现
    • 🧩Unordered_Set的基本设计
    • 🌸Unordered_Set测试
  • 📝4. Unordered_Map的模拟实现
    • 🧩Unordered_Map的基本设计
    • 🌸Unordered_Map测试
  • 📖5. 总结


前言:在C++标准库中,unordered_map和unordered_set作为高效的无序容器,以其基于哈希表的实现方式,为数据的快速查找、插入和删除提供了强有力的支持。这些容器通过哈希函数将元素映射到数组的索引上,从而实现了接近O(1)的平均时间复杂度操作,极大地提升了程序性能。然而,尽管它们的使用极为便捷,了解这些容器背后的工作原理和模拟实现过程,对于深入理解数据结构、算法设计以及优化程序性能都至关重要

本文旨在带领读者踏上一场探索之旅,从理论到实践,逐步揭开unordered_map和unordered_set的神秘面纱。我们将不仅探讨这些容器的基本概念和特性,详细阐述模拟实现的过程,更重要的是,通过模拟实现这两个容器,深入理解其内部机制,包括哈希表的构建、哈希函数的选择、冲突解决策略、动态扩容与再哈希等核心问题

本篇我们采用开散列的方式来模拟实现unordered,帮助读者掌握哈希的构建与使用,如果大家还不太了解哈希,建议先去阅读我的上一篇文章

让我们一起踏上学习的旅程,探索它带来的无尽可能!


📒1. 改造 HashTable

改造HashTable以适配unordered_mapunordered_set容器,主要涉及到如何根据这两种容器的特性来设计和实现HashTable节点的存储以及相应的操作。unordered_mapunordered_set的主要区别在于它们存储的元素类型:map存储键值对(key-value pairs),而set仅存储唯一的键值(通常是键本身作为值)。尽管如此,它们在底层数据结构(如HashTable)的实现上有很多相似之处


改造内容:

  • K:key的类型
  • T:如果是unordered_map,则为pair<K, V>; 如果是unordered_set,则为K
  • KeyOfT:通过T来获取key的一个仿函数类
  • HF: 哈希函数仿函数对象类型,哈希函数使用除留余数法,需要将Key转换为整形数字才能
    取模
// unordered_set 与 unordered_set
// unordered_set -> HashTable<K, K>
// unordered_map -> HashTable<K, pair<K, V>>

HashTable的节点设计:

template<class T>
struct HashNode
{
	HashNode* _next; // 存放数据
	T _data; // 指向节点的下一个元素

	HashNode(const T& data)
		:_data(data) 
		,_next(nullptr)
	{}
};

而在上一篇文章中,我们有介绍了一个关于非整形求关键值的仿函数HashFunc,在模拟实现是可以直接加在模拟实现的类上。

// hash_bucket是一个命名空间
// KeyOfT 和 Hash则是简化特定运算的仿函数
hash_bucket::HashTable<K, K, SetKeyOfT, Hash> _ht;
hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;

适用于unordered的成员函数

代码示例(C++):

// 修改了返回类型
pair<iterator, bool> Insert(const T& data)
{
	Hash hf;
	KeyOfT kot;
	
	// 判断值是否存在
	iterator it = Find(kot(data));
	if (it != end())
	{
		// 插入失败返回已有节点的迭代器和false		
		return make_pair(it, false);
	}

	// 负载因子
	if (_n == _tables.size())
	{
		vector<Node*> newTables;
		newTables.resize(_tables.size() * 2, nullptr);

		// 遍历旧表
		for (size_t i = 0; i < _tables.size(); i++)
		{
			Node* cur = _tables[i];
			while (cur)
			{
				Node* next = cur->_next;

				// 挪动到新表
				size_t hashi = hf(kot(cur->_data)) % newTables.size();
				cur->_next = newTables[hashi];
				newTables[hashi] = cur;

				cur = next;
			}
			_tables[i] = nullptr;
		}
		_tables.swap(newTables);
	}

	size_t hashi = hf(kot(data)) % _tables.size();
	Node* newnode = new Node(data);
	// 头插
	newnode->_next = _tables[hashi];
	_tables[hashi] = newnode;
	++_n;
	
	// 插入成功返回新节点的迭代器和ture		
	return make_pair(iterator(newnode, this, hashi), true);
}

// 返回类型修改成了迭代器
iterator Find(const K& key)
{
	Hash hf;
	KeyOfT kot;

	size_t hashi = hf(key) % _tables.size();
	Node* cur = _tables[hashi];
	while (cur)
	{
		if (kot(cur->_data) == key)
		{
			return iterator(cur, this, hashi);
		}

		cur = cur->_next;
	}

	return end();
}

📜2. HashTable的迭代器

🌞迭代器基本设计

代码示例(C++):

// 为了实现简单,在哈希桶的迭代器类中需要用到hashBucket本身,所以我们要进行一下前置声明,并且我们在 HashTable 中也要设置一个友元(friend)

//前置声明
template<class K, class T, class KeyOfT, class Hash>
class HashTable;

// 通过模板来达到const的迭代器的复用
template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
struct __HTIterator
{
	typedef HashNode<T> Node;
	typedef __HTIterator<K, T, Ref, Ptr, KeyOfT, Hash> Self;
	Node* _node;

	Ref operator*()
	{
		return _node->_data;
	}

	Ptr operator->()
	{
		return &(_node->_data);
	}

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

template<class K, class T, class KeyOfT, class Hash = HashFunc<K>>
class HashTable
{
	typedef HashNode<T> Node;
	
	// 友元
	template<class K, class T, class Ref, class Ptr, class KeyOfT, class Hash>
	friend struct __HTIterator;
	
	...... // 其他待实现的函数
}

🌈operator++()

因为哈希桶在底层是单链表结构,所以哈希桶的迭代器不需要operator–()操作,在operator++()的设计上,我们的问题是在走完这个桶之后,如何找到下一个桶,因此我们需要记录来方便寻找,于是我们引入了两个变量

// HashTable
const HashTable<K, T, KeyOfT, Hash>* _pht;
// 当前桶的位置
size_t _hashi;

代码示例(C++):


const HashTable<K, T, KeyOfT, Hash>* _pht;
size_t _hashi;

Self& operator++()
{
	if (_node->_next)
	{
		// 当前桶没走完,移动到下一个节点
		_node = _node->_next;
	}
	else
	{
		// 初步尝试方法
		// 当前桶走完了,走下一个桶
		/*Hash hf;
		KeyOfT kot;
		size_t hashi = hf(kot(_node->_data)) % _pht._tables.size();*/
		
		// 当前桶走完了,走下一个桶
		++_hashi;
		while (_hashi < _pht->_tables.size()) // 判断当前桶的位置是否合法
		{
			// 判断当前桶是否存在数据
			if (_pht->_tables[_hashi])
			{
				_node = _pht->_tables[_hashi];
				break;
			}
			++_hashi;
		}
		// 走完了
		if (_hashi == _pht->_tables.size())
		{
			_node = nullptr;
		}
	}

	return *this;
}

🌙begin()与end()

关于构建迭代器的begin()end()当我们模拟实现const版本时,又会遇到新的问题,const版本在调用构造时,调不动,因为我最开始实现的构造函数不是const版本,当const版本想要调用构造函数时,这时造成了权限的扩大,因此为了解决这个问题,我们重载了构造函数

代码示例(C++):

// 普通版本
typedef __HTIterator<K, T, T&, T*, KeyOfT, Hash> iterator;
// const 版本
typedef __HTIterator<K, T, const T&, const T*, KeyOfT, Hash> const_iterator;

iterator begin()
{
	for (size_t i = 0; i < _tables.size(); i++)
	{
		if (_tables[i])
		{
			return iterator(_tables[i], this, i);
		}
	}
	return end();
}

iterator end()
{
	return iterator(nullptr, this, -1);
}

const_iterator begin() const
{
	for (size_t i = 0; i < _tables.size(); i++)
	{
		if (_tables[i])
		{
			return const_iterator(_tables[i], this, i);
		}
	}
	return end();
}

// this-> const HashTable<K, T, KeyOfT, Hash>*
const_iterator end() const	
{
	return const_iterator(nullptr, this, -1);
}

⭐迭代器的构造

因为我们引入了两个新的变量,所以此次构造与以往不同

代码示例(C++):

// 非const版本
__HTIterator(Node* node, HashTable<K, T, KeyOfT, Hash>* pht, size_t hashi)
	:_node(node)
	, _pht(pht)
	, _hashi(hashi)
{}

// const版本
__HTIterator(Node* node, const HashTable<K, T, KeyOfT, Hash>* pht, size_t hashi)
	:_node(node)
	, _pht(pht)
	, _hashi(hashi)
{}

📚3. Unordered_Set的模拟实现

🧩Unordered_Set的基本设计

代码示例(C++):

template<class K, class Hash = HashFunc<K>>
class unordered_set
{
	struct SetKeyOfT
	{
		const K& operator()(const K& key)
		{
			return key;
		}
	};

public:
	// 因为 unordered_set的特性K是不能够修改的,
	// 所以我们在 const迭代器和非const迭代器上,都用 const来修饰K来起到不能修改K的特点
	typedef typename hash_bucket::HashTable<K, K, SetKeyOfT, Hash>::const_iterator iterator;
	typedef typename hash_bucket::HashTable<K, K, SetKeyOfT, Hash>::const_iterator const_iterator;

	pair<const_iterator, bool> insert(const K& key)
	{
		auto ret = _ht.Insert(key);
		return pair<const_iterator, bool>(const_iterator(ret.first._node, ret.first._pht, ret.first._hashi),ret.second);
	}
	
	// 因为用到的都是const迭代器,所以非const迭代器我们可以不写
	/*iterator begin()
	{
		return _ht.begin();
	}

	iterator end()
	{
		return _ht.end();
	}*/

	const_iterator begin() const
	{
		return _ht.begin();
	}

	const_iterator end() const
	{
		return _ht.end();
	}

	iterator find(const K& key)
	{
		return _ht.Find(key);
	}

	bool erase(const K& key)
	{
		return _ht.Erase(key);
	}

private:
	hash_bucket::HashTable<K, K, SetKeyOfT, Hash> _ht;
};

🌸Unordered_Set测试

代码示例(C++):

void TestSet()
{
	unordered_set<int> us;
	us.insert(1);
	us.insert(2);
	us.insert(6);
	us.insert(55);
	us.insert(3);

	unordered_set<int>::iterator it = us.begin();
	while (it != us.end())
	{
		// *it += 5;
		cout << *it << " ";
		++it;
	}
	cout << endl;
}

在这里插入图片描述


📝4. Unordered_Map的模拟实现

🧩Unordered_Map的基本设计

代码示例(C++):

template<class K, class V, class Hash = HashFunc<K>>
class unordered_map
{
	struct MapKeyOfT
	{
		const K& operator()(const pair<K, V>& kv)
		{
			return kv.first;
		}
	};
public:
	// 在 unordered_map我们就只需要考虑 kv.first不能修改
	// 但是 kv.first->second是可以修改的,因此我们需要将 K用 const修饰
	typedef typename hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT>::iterator iterator;

	pair<iterator, bool> insert(const pair<K, V>& kv)
	{
 		return _ht.Insert(kv);
	}

	iterator begin()
	{
		return _ht.begin();
	}

	iterator end()
	{
		return _ht.end();
	}
	
	// 重载operator[]
	V& operator[] (const K& key)
	{
		// 当_ht中没有就实现插入
		pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
		return ret.first->second;
	}

	const V& operator[] (const K& key) const
	{
		pair<iterator, bool> ret = _ht.Insert(make_pair(key, V()));
		return ret.first->second;
	}

	iterator find(const K& key)
	{
		return _ht.Find(key);
	}

	bool erase(const K& key)
	{
		return _ht.Erase(key);
	}

private:
		hash_bucket::HashTable<K, pair<const K, V>, MapKeyOfT, Hash> _ht;
};

🌸Unordered_Map测试

代码示例(C++):

void TestMap()
{
	unordered_map<string, string> dict;
	dict.insert(make_pair("sort", "排序"));
	dict.insert(make_pair("left", "左边"));
	dict.insert(make_pair("right", "右边"));

	for (auto& kv : dict)
	{
		// kv.first += 'x';
		kv.second += 'x';
		cout << kv.first << ":" << kv.second << endl;
	}
	cout << endl;
}

在这里插入图片描述


📖5. 总结

在本文的探索之旅中,我们深入剖析了unordered_map与unordered_set的内部机制,并通过模拟实现这两个容器,不仅加深了对哈希表这一重要数据结构的理解,还锻炼了编程能力和问题解决能力

通过模拟实现,我们亲手构建了哈希表,从简单的数组加链表结构,到动态扩容、再哈希等高级特性的实现,每一步都充满了挑战与收获。这个过程中,我们深刻体会到了数据结构设计的精妙之处,也学会了如何在实践中不断优化和调整我们的设计

unordered_mapunordered_set等无序容器将在更多领域发挥重要作用。随着技术的不断发展,我们也期待看到更多高效、稳定、易用的容器实现出现。

同时,我们也希望读者能够保持对新技术、新知识的好奇心和求知欲,不断探索、不断学习、不断进步
在这里插入图片描述
希望本文能够为你提供有益的参考和启示,让我们一起在编程的道路上不断前行!
谢谢大家支持本篇到这里就结束了,祝大家天天开心!

在这里插入图片描述

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

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

相关文章

C++学习笔记04-补充知识点(问题-解答自查版)

前言 以下问题以Q&A形式记录&#xff0c;基本上都是笔者在初学一轮后&#xff0c;掌握不牢或者频繁忘记的点 Q&A的形式有助于学习过程中时刻关注自己的输入与输出关系&#xff0c;也适合做查漏补缺和复盘。 本文对读者可以用作自查&#xff0c;答案在后面&#xff0…

go-kratos 学习笔记(1) 安装

简介&#xff1a; Kratos 一套轻量级 Go 微服务框架&#xff0c;包含大量微服务相关框架及工具。 使用步骤&#xff1a; 安装cli工具 go install github.com/go-kratos/kratos/cmd/kratos/v2latest 创建项目 通过 kratos 命令创建项目模板 # 国内拉取失败可使用gitee源 krat…

C:一些题目

1.分数求和 计算1/1-1/21/3-1/41/5 …… 1/99 - 1/100 的值 #include <stdio.h>int main(){double sum 0.0; // 使用 double 类型来存储结果&#xff0c;以处理可能的小数部分int sign 1; // 符号标志&#xff0c;初始为 1 表示正数for (int i 1; i < 100; i)…

PGSQL学习-基础表结构

1 访问数据库 创建好数据库后&#xff0c;你可以有三种方式访问数据库 运行PostgreSQL的交互式终端程序&#xff0c;它被称为psql&#xff0c; 它允许你交互地输入、编辑和执行SQL命令。 使用一种已有的图形化前端工具&#xff0c;比如pgAdmin或者带ODBC或JDBC支持的办公套件…

Centos7_Minimal安装Cannot find a valid baseurl for repo: base/7/x86_6

问题 运行yum报此问题 就是没网 解决方法 修改网络信息配置文件&#xff0c;打开配置文件&#xff0c;输入命令&#xff1a; vi /etc/sysconfig/network-scripts/ifcfg-网卡名字把ONBOOTno&#xff0c;改为ONBOOTyes 重启网卡 /etc/init.d/network restart 网路通了

opencv 按键开启连续截图,并加载提示图片

背景图小图 键盘监听使用的是pynput 库 保存图片时使用了年月日时分秒命名 原图&#xff1a; from pynput import keyboard import cv2 import time# 键盘监听 def on_press(key):global jieglobal guanif key.char a:jie Trueelif key.char d:jie Falseelif key.char…

【深度学习】LLaMA-Factory 大模型微调工具, 大模型GLM-4-9B Chat ,微调与部署 (2)

资料&#xff1a; https://github.com/hiyouga/LLaMA-Factory/blob/main/README_zh.md https://www.53ai.com/news/qianyanjishu/2015.html 代码拉取&#xff1a; git clone https://github.com/hiyouga/LLaMA-Factory.git cd LLaMA-Factorybuild镜像和执行镜像&#xff1a; …

如何借助生成式人工智能引领未来的科技狂潮

如何借助生成式人工智能引领未来的科技狂潮 1. 生成式AI的现状1.1 技术基础1.1.1 深度学习1.1.2 生成对抗网络&#xff08;GANs&#xff09;1.1.3 变分自编码器&#xff08;VAEs&#xff09; 1.2 主要应用1.2.1 语言模型1.2.2 图像生成1.2.3 音频与视频生成 2. 未来的发展趋势2…

2024/7/23 英语每日一段

As malware has improved and evolved, it has pushed defense software to require constant connection and more extensive control. That deeper access also introduces a far higher possibility that security software—and updates to that software—will crash the …

6.乳腺癌良性恶性预测(二分类、逻辑回归、PCA降维、SVD奇异值分解)

乳腺癌良性恶性预测 1. 特征工程1.1 特征筛选1.2 特征降维 PCA1.3 SVD奇异值分解 2. 代码2.1 逻辑回归、二分类问题2.2 特征降维 PCA2.3 SVD奇异值分解 1. 特征工程 专业上&#xff1a;30个人特征来自于临床一线专家&#xff0c;每个特征和都有医学内涵&#xff1b;数据上&…

SpringBoot启动命令过长

Error running DromaraApplication: Command line is too long. Shorten command line for DromaraApplication or also for Spring Boot default configuration?

探索LLM世界:新手小白的学习路线图

随着人工智能的发展&#xff0c;语言模型&#xff08;Language Models, LLM&#xff09;在自然语言处理&#xff08;NLP&#xff09;领域的应用越来越广泛。对于新手小白来说&#xff0c;学习LLM不仅能提升技术水平&#xff0c;还能为职业发展带来巨大的机遇。那么&#xff0c;…

matlab仿真 模拟调制(下)

&#xff08;内容源自详解MATLAB&#xff0f;SIMULINK 通信系统建模与仿真 刘学勇编著第五章内容&#xff0c;有兴趣的读者请阅读原书&#xff09; clear all ts0.001; t0:ts:10-ts; fs1/ts; dffs/length(t); msgrandi([-3 3],100,1); msg1msg*ones(1,fs/10); msg2reshape(ms…

opencv grabCut前景后景分割去除背景

参考&#xff1a; https://zhuanlan.zhihu.com/p/523954762 https://docs.opencv.org/3.4/d8/d83/tutorial_py_grabcut.html 环境本次&#xff1a; python 3.10 提取前景&#xff1a; 1、需要先把前景物体框出来 需要坐标信息&#xff0c;可以用windows自带的画图简单提取像素…

敏捷CSM认证:精通敏捷Scum估算方法,高效完成项目!

咱们做项目的时候可能都遇到过这种情况&#xff1a;项目一开始信心满满&#xff0c;觉得 deadline 稳了。结果呢&#xff1f;各种意外状况频出&#xff0c;时间好像怎么都不够用了&#xff0c;最后项目只能无奈延期&#xff0c;整个团队都像霜打的茄子。 说到底&#xff0c;还…

Elasticsearch:Golang ECS 日志记录 - zap

ECS 记录器是你最喜欢的日志库的格式化程序/编码器插件。它们可让你轻松地将日志格式化为与 ECS 兼容的 JSON。 编码器以 JSON 格式记录日志&#xff0c;并在可能的情况下依赖默认的 zapcore/json_encoder。它还处理 ECS 错误格式的错误字段记录。 默认情况下&#xff0c;会添…

集合的概念

目录 概述 1 集合定义 1.1 基本定义 1.2 元素和集合的关系表述 1.3 集合分类 1.4 集合描述 1.5 集合关系描述 2 集合的运算 2.1 集合关系的定义 2.2 集合的运算 概述 在高等数学中&#xff0c;集合是指由一些具有共同特征的对象组成的整体。这些对象可以是数字、字母…

最短路径 | 743. 网络延迟时间之 Dijkstra 算法和 Floyd 算法

目录 1 基于 Dijkstra 算法1.1 代码说明1.2 完整代码 2 基于 Floyd 算法2.1 代码说明2.2 完整代码 前言&#xff1a;我在做「399. 除法求值」时&#xff0c;看到了基于 Floyd 算法的解决方案&#xff0c;突然想起来自己还没有做过最短路径相关的题。因此找来了「743. 网络…

灰色关联分析【系统分析+综合评价】

系统分析&#xff1a; 判断哪个因素影响最大 基本思想&#xff1a;根据序列曲线几何形状的相似程度来判断其练习是否紧密 绘制统计图并进行分析 确定子序列和母序列 对变量进行预处理&#xff08;去量纲、缩小变量范围&#xff09; 熟练使用excel与其公式和固定&#xff08…

微服务安全——OAuth2详解、授权码模式、SpringAuthorizationServer实战、SSO单点登录、Gateway整合OAuth2

文章目录 Spring Authorization Server介绍OAuth2.0协议介绍角色OAuth2.0协议的运行流程应用场景授权模式详解客户端模式密码模式授权码模式简化模式token刷新模式 OAuth 2.1 协议介绍授权码模式PKCE扩展设备授权码模式拓展授权模式 OpenID Connect 1.0协议Spring Authorizatio…