C++语法(22)---- 哈希表的闭散列和开散列

news2024/11/26 5:44:39

C++语法(21)---- 模拟map和set_哈里沃克的博客-CSDN博客https://blog.csdn.net/m0_63488627/article/details/130354019?spm=1001.2014.3001.5501

目录

1.哈希表的介绍

1.stl中的哈希表使用

2.比较

3.哈希的原理

4.哈希映射的方法

1.直接定址法

2.除留余数法

5.解决哈希冲突

1.闭散列

2.开散列(哈希桶)


1.哈希表的介绍

1.stl中的哈希表使用

unordered_map(K,V)

unordered_set(K)

hash<KEY>其实是一个仿函数,是因为数据要和位置映射,那必然要取余,string等类无法使用取余,那么需要我们自己写一个进行判断。

2.比较

其上层的使用与map以及set及其的相近。但是又有什么不同呢

1.大量随机数据插入,map和set性能快

2.查找数据,哈希表级制性能,时间复杂度为O(1)

3.大量随机数据删除,也是map和set性能快

3.哈希的原理

就是将数据和存储位置做一个映射,这个映射就是哈希映射。所以使用哈希表的好处就是找到自己想要的数据时间复杂度很低

4.哈希映射的方法

1.直接定址法

取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B -- 某个值都要唯一的位置
优点:简单、均匀
缺点:需要事先知道关键字的分布情况,如果数据比较分散开辟空间会很浪费
使用场景:适合查找比较小且连续的情况

2.除留余数法

设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址  --  几个值都可能在同一个位置

优点:空间适合
缺点:这种情况,会存在可能多个值取余后得到相同的映射地址(哈希冲突)

使用场景:适合分散的场景

5.解决哈希冲突

1.闭散列

实现原理:冲突位置,那么往后判断是否是否为空

缺点:查找效率变低,互相冲突导致插入逻辑复杂

1.线性探测(一个萝卜一个坑,坑被占了线性往后找下一个空的)  ---  start+i

2.二次探测,是以平方为步调向后判断的  ---  start+i^2

线性探测实现:

表内元素的状态

冲突主要是因为一个数已经存在于表中,那么才需要往后判断是否为空,所以已经知道需要两个状态:存在和空。不过如果我们要往后搜索,如果只有两个结果,那么在连续冲突的数中删除,那么数据就中断了,那么实现就会出现错误,那么此时还需要一个状态表示:删除。

enum State
{
	EMPTY,
	EXIST,
	DELETE,
};

元素属性定义

此外为了保持和map的一致性,我们的节点元素也要使用pair与之对应,并且伴随节点的状态,节点先处理为empty,说明数组中的状态为空

template<class K, class V>
struct HashData
{
	pair<K, V> _kv;
	State _state = EMPTY;;
};

表的框架

当然,对于一个模板,并不知道比较大小是通过什么方式比较的,所以我们还得添加上仿函数进行比较。

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

//特化了一个string的仿函数,它的大小比较按照我想要的方式实现
template<>
struct HashFunc<string>
{
	size_t operator()(const string& k)
	{
		size_t hash = 0;
		for (auto& ch : k)
		{
			hash += ch;
		}
		return hash;
	}
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
	typedef HashData<K, V> Data;
public:
	HashTable()
	{
		_tables.resize(10);
	}
private:
	vector<Data> _tables;  //存储数据的空间
	size_t _n = 0;  //进去的数有多少
};

insert

关于capacity和size问题,首先我们插入的位置一定的在size内,因为如果是判断capacity,那么值的映射可能会在capacity和size之间,但是size只有那么大,所以找不到映射后的位置也有可能,所以判断靠判断和size的大小的。不过我们可以设计成size和capacity一样大,这样就不会出现这种尴尬。capacity和size一致,我们使用resize()函数就能解决。

实现逻辑就是:当判断映射位置为存在,那么我们就需要往后找,不过不能超过整体范围,所以我们需要对hashi进行取余处理。随后找到位置则插入,状态设置为EXIST,将_n加一。

Hash hf;  //仿函数
size_t hashi = hf(kv.first) % _tables.size();
//线性探测
while (_tables[hashi]._state == EXIST)
{
	hashi++;
	hashi %= _tables.size();
}

_tables[hashi]._kv = kv;
_n++;
_tables[hashi]._state = EXIST;

return true;

出现了有些问题:

问题:表的数据存储满了,那就有扩容问题  ---  此外,暂且不说存储满了,如果数据很多都被覆盖了,那么此时再想插入是不是也就意味着非常容易冲突呢?此时插入成本就很高了

结论:所以我们不能让它满,我们需要添加一个负载因子,使得我们在表中出现百分之几十后说明我们需要扩容。负载因子越小,说明固定空间中的数据越少,那么使得冲突的几率就小。不过太小浪费空间,太大浪费时间,所以设计的负载因子一般为0.7。

实现:

1.此时我们要注意,不能直接跟size比较是否小于0.7,因为int类型不能这样比较,所以我们要对_n*10随后比较是否小于7。

2.扩容不能直接把vector变大就好,因为里面的数据可能会对不上之后扩容的映射地址。所以我们新建一个表,再将原来的值依次insert进新表中,在把两表互换,新表出insert函数就被自动析构了。

3.此外map设置就不能出现两个相同的值,所以我们还会实现一个find函数,通过find函数找是否已经存在,如果存在则插入失败。

bool Insert(const pair<K, V>& kv)
{
	if (Find(kv.first) != nullptr)
		return false;
	if (_n * 10 / _tables.size() >= 7)
	{
		HashTable<K, V, Hash> tmp;
		tmp._tables.resize(2 * _tables.size());
		for (auto& e : _tables)
		{
			if (e._state == EXIST)
			{
                //复用了自己写的insert函数
				tmp.Insert(e._kv);
			}
		}
		_tables.swap(tmp._tables);
	}

	Hash hf;
	size_t hashi = hf(kv.first) % _tables.size();

	//线性探测
	while (_tables[hashi]._state == EXIST)
	{
		hashi++;
		hashi %= _tables.size();
	}

	_tables[hashi]._kv = kv;
	_n++;
	_tables[hashi]._state = EXIST;

	return true;
}

erase

先find(后面实现)想要删除的值,随后把插入的值的状态设置为DELETE即可,_n减一

bool Ereas(const K& k)
{
	Data* cur = Find(k.first);
	if (cur == nullptr)
		return false;
	cur->_state = DELETE;
	--_n;
	return true;
}

find

找对应的位置,查看是否是我们想要的值,是则返回,不是则需要往下判断,直到走到空,说明找不到,则返回nullptr。

Data* Find(const K& k)
{
	Hash hf;
	size_t hashi = hf(k) % _tables.size();
	while (_tables[hashi]._state!=EMPTY)
	{
		if (_tables[hashi]._state == EXIST && _tables[hashi]._kv.first == k)
			return &_tables[hashi];
		++hashi;
		hashi %= _tables.size();
	}
	return nullptr;
}

整体实现

namespace closehash
{
	enum State
	{
		EMPTY,
		EXIST,
		DELETE,
	};

	template<class K>
	struct HashFunc
	{
		size_t operator()(const K& k)
		{
			return (size_t)k;
		}
	};
	
	/*struct HashFuncString
	{
		size_t operator()(const string& k)
		{
			return k[0];
		}
	};*/

	template<>
	struct HashFunc<string>
	{
		size_t operator()(const string& k)
		{
			size_t hash = 0;
			for (auto& ch : k)
			{
				hash += ch;
			}
			return hash;
		}
	};

	template<class K, class V>
	struct HashData
	{
		pair<K, V> _kv;
		State _state = EMPTY;
	};

	template<class K, class V, class Hash = HashFunc<K>>
	class HashTable
	{
		typedef HashData<K, V> Data;
	public:
		HashTable()
		{
			_tables.resize(10);
		}

		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first) != nullptr)
				return false;
			if (_n * 10 / _tables.size() >= 7)
			{
				HashTable<K, V, Hash> tmp;
				tmp._tables.resize(2 * _tables.size());
				for (auto& e : _tables)
				{
					if (e._state == EXIST)
					{
						tmp.Insert(e._kv);
					}
				}
				_tables.swap(tmp._tables);
			}

			Hash hf;
			size_t hashi = hf(kv.first) % _tables.size();

			//线性探测
			while (_tables[hashi]._state == EXIST)
			{
				hashi++;
				hashi %= _tables.size();
			}

			_tables[hashi]._kv = kv;
			_n++;
			_tables[hashi]._state = EXIST;

			return true;
		}

		Data* Find(const K& k)
		{
			Hash hf;
			size_t hashi = hf(k) % _tables.size();
			while (_tables[hashi]._state!=EMPTY)
			{
				if (_tables[hashi]._state == EXIST && _tables[hashi]._kv.first == k)
					return &_tables[hashi];
				++hashi;
				hashi %= _tables.size();
			}
			return nullptr;
		}
		
		bool Ereas(const K& k)
		{
			Data* cur = Find(k.first);
			if (cur == nullptr)
				return false;
			cur->_state = DELETE;
			--_n;
			return true;
		}

	private:
		vector<Data> _tables;  //存储数据的空间
		size_t _n = 0;  //进去的数有多少
	};
}

2.开散列(哈希桶)

实现原理:冲突位置,将对应位置加一个链表,冲突就挂到链表上(这也是stl不实现双向迭代器的理由)

缺点:链表太长会出现问题

优点:冲突间不会相互影响

哈希桶的实现

节点元素

其实就是所谓的链表节点

template<class K, class V>
struct HashNode
{
	pair<K, V> _kv;
	HashNode<K, V>* _next;
};

哈希表框架

向量中存储链表节点地址,冲突的节点被挂起即可

template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
	typedef HashNode<K, V> Node;	
private:
	vector<Node*> _table;
	size_t _n = 0;
};

insert

插入的逻辑其实就是:如果找到了地址,新节点的next指向表的下一个,表里填入新节点地址

size_t hashi = Hash()(kv.first) % _n;
Node* newnode = new Node(kv);
newnode->_next = _table[hashi];
_table[hashi] = newnode;
_n++;

因为如果不扩容,可能一个链表会很长一串,那么查找的效率就低了,那么我们规定负载因子一般取1.0;同样如果有重复的值,那么不需要插入,直接返回false;扩容的思想和上面基本一致,这里不多赘诉。

bool Insert(const pair<K, V> kv)
{
	if (Find(kv.first) != nullptr)
		return false;
	if (_n == _table.size())
	{
		HashTable<K, V, Hash> tmp;
		tmp._table.resize(2 * _table.size(),nullptr);
		for (auto cur : _table)
		{
			while (cur)
			{
				tmp.Insert(cur->_kv);
				cur = cur->_next;
			}
		}
		_table.swap(tmp._table);
	}

	size_t hashi = Hash()(kv.first) % _table.size();
	Node* newnode = new Node(kv);
	newnode->_next = _table[hashi];
	_table[hashi] = newnode;
	_n++;
	return true;
}

关于释放空间,对于闭散列,其实不需要手动释放,因为析构函数直接释放vector结构。不过我们现在的vector中存放的是节点,这些节点是需要被释放。

~HashTable()
{
	for (int i = 0; i < _table.size(); i++)
	{
		Node* cur = _table[i];
		while (cur)
		{
			Node* next = cur->_next;
			delete cur;
			cur = next;
		}
		_table[i] = nullptr;
	}
}

这样的设计其实瑕疵很大,因为我们不仅插入实现要创造新节点,而且扩容时也创建了新节点,析构也是依次释放节点。时间效率低。我们是否可以直接将原有节点转到新表中呢?

我们只需要开辟一个新的vector,随后将原先表中的值依次遍历的存到新表中,最后交换一下表的地址,这样就能达到节省拷贝时间和析构时间的目的了。

bool Insert(const pair<K, V> kv)
{
	if (Find(kv.first) != nullptr)
		return false;
	if (_n == _table.size())
	{
		vector<Node*> newTable;
		newTable.resize(2 * _table.size(),nullptr);
		for (int i = 0; i < _table.size(); i++)
		{
			Node* cur = _table[i];
			while (cur)
			{
				Node* next = cur->_next;
				size_t hashi = Hash()(cur->_kv.first) % newTable.size();
				cur->_next = newTable[hashi];
				newTable[hashi] = cur;
				cur = next;
			}
			_table[i] = nullptr;
		}
		_table.swap(newTable);
	}

	size_t hashi = Hash()(kv.first) % _table.size();
	Node* newnode = new Node(kv);
	newnode->_next = _table[hashi];
	_table[hashi] = newnode;
	_n++;
	return true;
}

find

Node* Find(const K& k)
{
	size_t hashi = Hash()(k) % _table.size();
	Node* cur = _table[hashi];
	while (cur)
	{
		if (cur->_kv.first == k)
			return cur;
		else
			cur = cur->_next;
	}
	return nullptr;
}

Erase

删除链表节点有些时候需要前驱,所以不能复用find函数

bool Erase(const K& k)
{
	size_t hashi = Hash()(k) % _table.size();
	Node* cur = _table[hashi];
	Node* prev = nullptr;
	while (cur)
	{
		if (cur->_kv.first == k)
		{
			if (cur == _table[hashi])
				_table[hashi] = cur->_next;
			else
				prev->_next = cur->_next;
			delete cur;
			_n--;
			return true;
		}
		prev = cur;
		cur = cur->_next;
	}
	return false;
}

保持表的大小为素数,这样表的冲突会减少。所以stl中会有存储素数的表,扩容的大小就按照该表中的值进行扩容。

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

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

相关文章

FreeRTOS 软件定时器

文章目录 一、软件定时器简介二、定时器服务/Daemon 任务三、单次定时器和周期定时器四、复位软件定时器1. 函数 xTimerReset()2. 函数 xTimerResetFromISR() 五、创建软件定时器1. 函数 xTiemrCreate()2. 函数 xTimerCreateStatic() 六、开启软件定时器1. 函数 xTimerStart()2…

条件构造器Wrapper

本文来说下mybatis-plus中的条件构造器Wrapper 文章目录 条件构造器程序实例 条件构造器 十分重要&#xff1a;Wrapper 我们写一些复杂的sql就可以使用它来替代&#xff01; 程序实例 测试一 Overridepublic List<OrderInfo> getOrderInfo() {// id等于2的数据QueryWrapp…

K210单片机的按键检测

这个图片是程序的效果&#xff0c;按下按键后蓝灯亮起&#xff0c;松开按键后蓝灯熄灭。 主要用的的就是函数的构造方法和使用方法&#xff1a; GPIO(ID,MODE,PULL,VALUE) GPIO 对象。 【ID】内部 GPIO 编号; 【MODE】GPIO 模式&#xff1b; GPIO.IN &#x…

保龄球游戏的获胜者、找出叠涂元素----2023/4/30

保龄球游戏的获胜者----2023/4/30 给你两个下标从 0 开始的整数数组 player1 和 player2 &#xff0c;分别表示玩家 1 和玩家 2 击中的瓶数。 保龄球比赛由 n 轮组成&#xff0c;每轮的瓶数恰好为 10 。 假设玩家在第 i 轮中击中 xi 个瓶子。玩家第 i 轮的价值为&#xff1a; …

SpringMVC学习总结(三)SpringMVC获取请求参数的几种方法解决获取请求参数的乱码问题CharacterEncodingFilter过滤器

SpringMVC学习总结&#xff08;三&#xff09;SpringMVC获取请求参数的几种方法/解决获取请求参数的乱码问题/CharacterEncodingFilter过滤器 一、通过ServletAPI获取请求参数 将HttpServletRequest作为控制器方法的形参&#xff0c;此时HttpServletRequest类型的参数表示封装…

Elasticsearch --- 数据聚合、自动补全

一、数据聚合 聚合&#xff08;aggregations&#xff09;可以让我们极其方便的实现对数据的统计、分析、运算。例如&#xff1a; 什么品牌的手机最受欢迎&#xff1f; 这些手机的平均价格、最高价格、最低价格&#xff1f; 这些手机每月的销售情况如何&#xff1f; 实现这…

山东专升本计算机第十章-计算思维

计算思维 分支主题 3 10.2计算思维与算法 考点 3 算法概论 算法就是对计算思维解决问题的方法的描述是计算机求解的核心和关键。 基本特征 • 有穷性 • 算法必须能执行有线各步骤之后停止 • 确定性 • 算法的每一个步骤都必须有明确的定义不应该在理解时产生二义性 • 可…

【软考数据库】第六章 数据库技术基础

目录 6.1 基本概念 6.1.1 关于数据的基本概念 6.1.2 数据库管理系统的功能 6.1.3 数据各个发展阶段的特点 6.1.4 数据库系统的体系结构 6.2 数据模型 6.2.1 三级模式两级映像 6.2.2 数据模型_模型分类 6.2.3 数据模型_组成…

Linux man 命令详解

man 命令 Linux man 命令用于显示 Linux 操作系统中的手册页&#xff08;manual page&#xff09;&#xff0c;它提供了对 Linux 操作系统中各种命令、函数、库等的详细说明&#xff0c;man 命令有许多参数。 参数介绍 下面简要介绍一下主要参数的功能&#xff1a; -f&…

Java面试题总结 | Java面试题总结7- Redis模块(持续更新)

Redis 文章目录 Redisredis的线程模型Redis的Mysql的区别Redis和传统的关系型数据库有什么不同&#xff1f;Redis常见的数据结构zset数据结构Redis中rehash过程redis为什么不考虑线程安全的问题呢Redis单线程为什么还能这么快&#xff1f;为什么Redis是单线程的&#xff1f;red…

第十五章 角色移动旋转实例

本章节我们创建一个“RoleDemoProject”工程&#xff0c;然后导入我们之前创建地形章节中的“TerrainDemo.unitypackage”资源包&#xff0c;这个场景很大&#xff0c;大家需要调整场景视角才能看清。 接下来&#xff0c;我们添加一个人物模型&#xff0c;操作方式就是将模型文…

《CTFshow-Web入门》07. Web 61~70

Web 61~70 web61~65题解 web66知识点题解 web67知识点题解 web68知识点题解 web69知识点题解 web70知识点题解 ctf - web入门 web61~65 题解 这几个题都和 web58 一样。可能内部禁用的函数不一样吧。但 payload 都差不多。不多解释了。 以下解法随便挑一个即可。可能不同题会…

使用Quartz.net + Topshelf完成服务调用

概述&#xff1a; Quartz.NET 是一个开源作业调度库&#xff0c;可用于在 .NET 应用程序中调度和管理作业。它提供了一个灵活而强大的框架&#xff0c;用于调度作业在特定的日期和时间或以固定的时间间隔运行&#xff0c;并且还支持复杂的调度场景&#xff0c;例如 cron 表达式…

39.Java-interface接口

interface接口 1.interface2.接口的定义和使用3.接口中成员的特点4. 接口和类之间的关系5. 实例6. 接口中新增的方法6.1 JDK8以后新增2种方法6.1.1 允许在接口中定义默认方法6.1.2 允许在接口中定义静态方法 6.2 JDK9以后新增的方法6.3 小结 7. 接口总结 1.interface 接口就是…

Netty内存管理--内存池PoolArena

一、写在前面 到这里, 想必你已知道了Netty中的内存规格化(SizedClass), Page和SubPage级别的内存分配, 但是具体使用者不应该关心应该申请page还是subpage。而且从过去的经验来说, 申请page/subpage的数量也是个动态值, 如果申请使用完之后就释放那使用内存池的意义就不大。N…

Linux 之十九 编译工具链、.MAP 文件、.LST 文件

.map 文件和 .lst 文件是嵌入式开发中最有用的俩调试辅助文件。现在主要从事 RISC-V 架构&#xff0c;开始与 GCC 打交道&#xff0c;今天就重点学习一下 GCC 的 .map 文件、.lst 文件&#xff0c;并辅助以 ARMCC 和 IAR 作为对比。 编译工具链 .map 文件和 .lst 文件都是由编…

【数据结构】第十三站:排序性质

文章目录 一、文件外与文件内排序二、非比较排序之计数排序1.绝对映射2.相对映射3.代码实现 三、排序的稳定性 一、文件外与文件内排序 如下图所示是我们常见的的排序算法&#xff0c;也是我们已经使用代码实现过的 上面这七种排序算法我们都可以称之为文件内排序。但是归并排…

Fetch

Fetch 也是前后端通信的一种方式。是 Ajax 的一种替代方案。 Fetch 的优缺点&#xff1a; Fetch 的优点&#xff1a; 原生就有 fetch 对象&#xff0c;使用简单。基于 Promise。 Fetch 的缺点&#xff1a; 兼容性没有 Ajax 好。原生没有提供 abort、timeout等机制。 fetc…

【笔记】cuda大师班7-11 索引

一. block&#xff0c;grid 的 idx & dim 注意区分threadIdx&#xff0c;blockIdx 1.1 blockIdx 每一个线程在cuda运行时唯一初始化的blockIdx变量只取决于所属的坐标&#xff0c;blockIdx同样也是dim3类型 1.1. 对比blockIdx和threadIdx blockIdx只取决于当前block在…

786. 第k个数(C++和Python3)——2023.4.30打卡

文章目录 QuestionIdeasCode Question 给定一个长度为 n 的整数数列&#xff0c;以及一个整数 k &#xff0c;请用快速选择算法求出数列从小到大排序后的第 k 个数。 输入格式 第一行包含两个整数 n 和 k 。 第二行包含 n 个整数&#xff08;所有整数均在 1∼109 范围内&…