C++——深部解析哈希

news2024/11/17 23:45:42

好久不见给大家分享一张图片吧

目录

前言

二、库文件

1、哈希冲突

2 哈希函数

3、闭散列

三 、闭散列的实现和底层逻辑

1、哈希表(闭散列)的定义

2、哈希表(闭散列)的插入

3、哈希表(闭散列)的查找

4.哈希表(闭散列)的删除

四、哈希桶

1、开散列

2、哈希桶(开散列)的定义

3、哈希桶(开散列)的析构

4、哈希桶(开散列)的插入

 5、哈希桶(开散列)的删除

 6、哈希桶(开散列)的查找

总结


相关博客
x​​​​​​​c++——map、set底层之AVL树(动图演示旋转)

vector

c语言顺序表+链表​​​​​​​


前言

在我们前面的学习中,我们知道了set和map相关的多个库文件和数据结构,今天我们我们将学习基础的哈希表和哈希桶。

学习set和map时,我们非常清楚它们都是树状结构,set为key类型,而map的为key、value类型,而哈希是一种顺序结构,那么就让我们来感受一下哈希表和哈希桶吧!!


一、哈希是什么?

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素 时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即 O($log_2 N$),搜索的效率取决于搜索过程中元素的比较次数。

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。

如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立 一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

当向该结构中:

插入元素

根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放

 搜索元素

对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置 取元素比较,若关键码相等,则搜索成功 该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称 为哈希表(Hash Table)(或者称散列表)

二、库文件

在学习哈希底层之前我们先了解一下大佬是怎么实现的哈希;

unordered_set

unordered_map

在这里我们理解几个和哈希相关的概念

1、哈希冲突

对于两个数据元素的关键字$k_i$和 $k_j$(i != j),有$k_i$ != $k_j$,但有:Hash($k_i$) == Hash($k_j$),即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突 或哈希碰撞。

把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。

发生哈希冲突该如何处理呢?

2 哈希函数

引起哈希冲突的一个原因可能是

哈希函数设计不够合理。

哈希函数设计原则: 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值 域必须在0到m-1之间 哈希函数计算出来的地址能均匀分布在整个空间中

哈希函数应该比较简单

常见哈希函数

1. 直接定址法--(常用)

取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B

优点:简单、均匀

缺点:需要事先知道关键字的分布情况

使用场景:适合查找比较小且连续的情况

2. 除留余数法--(常用)

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

3. 平方取中法--(了解)

假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址 平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况

4. 折叠法--(了解) 折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这 几部分叠加求和,并按散列表表长,取后几位作为散列地址。 折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况

5. 随机数法--(了解) 选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中 random为随机数函数。 通常应用于关键字长度不等时采用此法

6. 数学分析法--(了解)

设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定 相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只 有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散 列地址。

哈希冲突解决

解决哈希冲突两种常见的方法是:闭散列开散列

3、闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有 空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置 呢?

1. 线性探测

比如现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4, 因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。

线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

插入

通过哈希函数获取待插入元素在哈希表中的位置 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突, 使用线性探测找到下一个空位置,插入新元素

删除

采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素 会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影 响。因此线性探测采用标记的伪删除法来删除一个元素。

三 、闭散列的实现和底层逻辑

我们在学习二叉树和set和map时,我们存储数据有一个规律,那就是如果插入一个树,当这个数据比根大,那么我们就往右走,比根小我们就往左走,通过这个规律,我们就可以把一个节点放到正确的位置上去;

在闭散列的概念中我们略有了解,那就是我们的哈希存储是在一个顺序表里面的,当让这个数据和顺序表建立某种映射关系,这样我们就可以轻松找到了。

这里可能有人会问了,我们既然借助了顺序表为什么不直接用顺序表呢,这里可以参考上面的哈希的优点;再说说个人看法,当我们在顺序表里面去找一个数据的时候我们需要将整个顺序表给遍历一遍,而哈希找一个数据只需要在一个小的范围里去查找

1、哈希表(闭散列)的定义

我们这里存储的算法为除留余数

enum Status//将状态进行枚举
{
	EMPTY,//空
	EXIST,//存在
	DELETE//删除
};

template<class K,class V>
struct HashData
{
	pair<K, V> _kv;//存储数据
	Status _state= EMPTY;//状态
};
template<class K,class V>
struct HashData
{
public:
	bool Insert(const pair<K, V>& kv)//插入函数
    {}
    HashData<K, V>* Find(const K& key)//查找函数
    {}
    bool Erase(const K& key)//删除函数
    {}
private:
	vector<HashData<K, V>> _tables;//这里我们用的是顺序表
	size_t _n = 0; // 存储的数据个数
};

 现在我们知道了哈希表的基本结构;

现在然我们用图片理解哈希表吧

我们以前在顺序表里面存储数据的时候都是一个数据一个数据挨着存储,而现在我们在存储时,我们直接对要存储的数据进行取余,余数就是我们要存储的位置了,以后要是向对某个数据进行操作,我们只需要在这个数据的余数处去找这个数据

现在有一个问题那就是当我们的一个表里面要存储5、15,而表的长度为10,我们对5、15取余都是5,那怎么办呢?

这就是我们前面概念所提到过的哈希碰撞。这个时候我们只需要进行偏移一下就可以,也就是将hashi++(hashi=key%_tables.size());

2、哈希表(闭散列)的插入

bool Insert(const pair<K, V>& kv)
{
	if (Find(kv.first))
		return false;
	
	// 负载因子超过0.7就扩容
	//if ((double)_n / (double)_tables.size() >= 0.7)
	if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
	{
		size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
		HashTable<K, V> newht;
		newht._tables.resize(newsize);
	
		// 遍历旧表,重新映射到新表
		for (auto& data : _tables)
		{
			if (data._state == EXIST)
			{
				newht.Insert(data._kv);
			}
		}
	
		_tables.swap(newht._tables);
	}
	
	size_t hashi = kv.first % _tables.size();
	
	// 线性探测
	size_t i = 1;
	size_t index = hashi;
	while (_tables[index]._state == EXIST)
	{
		index = hashi + i;
		index %= _tables.size();
		++i;
	}
	
	_tables[index]._kv = kv;
	_tables[index]._state = EXIST;
	_n++;
	
	return true;
}

 我们在插入的时候,是通过映射关系进入插入的,这个关系就是key对空间进行取余数,然后在对应的位置放元素,当某个位置有数据时,我们就进行偏移,将hashi++,就是向后移动一位;

思考,如果余数相同的数据太多了,我们++一圈,也就是一轮回来还是没有位置存放数据时因该怎么办?这种情况的时间复杂度是不是还不如vector呢?

解决方法那就是就行扩容,将我们的_table.size()的大小变大,那也就是进行扩容;我们解决方案是定义一个负载因子,当负载因子大于0.7时,我们就就行扩容,我们重新开辟一个顺序表,将原来的哈希表进行遍历一遍,重新进行映射一次,否则就有取余过后那个位置上找不到某一个数据;

例如:15% 10=5和15%20=15这样,我们扩容完我们15应给放在15的位置,然而我们不遍历重新映射就会导致查找时在15位置找不到15;

思考:哈希表什么情况下进行扩容?如何扩容?

3、哈希表(闭散列)的查找

HashData<K, V>* Find(const K& key)
{
	if (_tables.size() == 0)
	{
		return false;
	}
	
	size_t hashi = key % _tables.size();
	
	// 线性探测
	size_t i = 1;
	size_t index = hashi;
	while (_tables[index]._state != EMPTY)
	{
		if (_tables[index]._state == EXIST
			&& _tables[index]._kv.first == key)
		{
			return &_tables[index];
		}
	
		index = hashi + i;
		index %= _tables.size();
		++i;
	
		// 如果已经查找一圈,那么说明全是存在+删除
		if (index == hashi)
		{
			break;
		}
	}
	
	return nullptr;
}

 我们查找这里也是非常简单的,就是在取余过后到hashi的位置上去找,如果这个位置上的数据不是我们想要的数据,那么我们就往它的后面去找,直到什么时候结束呢?

我们在hashi过面一直找到数据状态为空的时候停止,因为我们插入的时候为空的位置放上了偏移或没偏移的数据,若这段空间不连续有空的的时候,那么就说明没有找到;

4.哈希表(闭散列)的删除

bool Erase(const K& key)
{
	HashData<K, V>* ret = Find(key);
	if (ret)
	{
		ret->_state = DELETE;
		--_n;
		return true;
	}
	else
	{
		return false;
	}
}

这个函数就更简单了,我们只需要将这个位置的状态改为删除就可以了,然后让_n--,就可以了;

四、哈希桶

1、开散列

开散列概念

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地 址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链 接起来,各链表的头结点存储在哈希表中。

大家看这幅图好看嘛,我们的哈希桶(开散列)也就是长这个样子的,屋檐呢就相当于顺序表,然后呢每个挂件呢也就相当于hashi相同的数据串在一起的样子。

2、哈希桶(开散列)的定义

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

	HashNode(const pair<K, V>& kv)
		:_next(nullptr)
		, _kv(kv)
	{}
};

template<class K, class V>
class HashTable
{
	typedef HashNode<K, V> Node;
public:
	~HashTable()
    {}
    Node* Find(const K& key)
    {}
    bool Erase(const K& key)
    {}

    bool Insert(const pair<K, V>& kv)
    {}
    private:
	vector<Node*> _tables; // 指针数组
	size_t _n = 0; // 存储有效数据个数
};

 我们之前的哈希表呢是直接将数据放在顺序表上的,而现在呢,我们是在顺序表上放hashi相同节点的一个节点的地址,然后将剩下相同节点一个一个的串联在下面,也就是链表一样;

相当于在顺序表里面放每个链表的头节点;

桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可 能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希 表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点, 再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可 以给哈希表增容。

3、哈希桶(开散列)的析构

这里的析构就不只是将顺序表给销毁了,我们要将链表的每一个节点进行delete

~HashTable()
{
	for (auto& cur : _tables)
	{
		while (cur)
		{
			Node* next = cur->_next;
			delete cur;
			cur = next;
		}

		cur = nullptr;
	}
}

4、哈希桶(开散列)的插入

bool Insert(const pair<K, V>& kv)
{
	if (Find(kv.first))
	{
		return false;
	}

	// 负载因因子==1时扩容
	if (_n == _tables.size())
	{
		size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
		vector<Node*> newtables(newsize, nullptr);
		//for (Node*& cur : _tables)
		for (auto& cur : _tables)
		{
			while (cur)
			{
				Node* next = cur->_next;

				size_t hashi = cur->_kv.first % newtables.size();

				// 头插到新表
				cur->_next = newtables[hashi];
				newtables[hashi] = cur;

				cur = next;
			}
		}

		_tables.swap(newtables);
	}

	size_t hashi = kv.first % _tables.size();
	// 头插
	Node* newnode = new Node(kv);
	newnode->_next = _tables[hashi];
	_tables[hashi] = newnode;

	++_n;
	return true;
}

我们在每个hashi相同位置进行链表的链接,为了节省效率呢,我们就在链表的头部进行头插,然后顺序表里面就存放这个新进来元素的地址;

当我们的hashi不够用的时候呢,我们就将它进行扩容。扩容完以后呢,我们不能再像哈希表一样遍历原来表进行开空间释放空间了,这样非常麻烦。

我们通常是将他们的指针指向位置改变,定义一个next进行记录工作节点下一个节点的位置,以便于后续的更新,然后在新的顺序表里重新进行头插cur。

 5、哈希桶(开散列)的删除

bool Erase(const K& key)
{
	size_t hashi = key % _tables.size();
	Node* prev = nullptr;
	Node* cur = _tables[hashi];
	while (cur)
	{
		if (cur->_kv.first == key)
		{
			if (prev == nullptr)
			{
				_tables[hashi] = cur->_next;
			}
			else
			{
				prev->_next = cur->_next;
			}
			delete cur;

			return true;
		}
		else
		{
			prev = cur;
			cur = cur->_next;
		}
	}

	return false;
}

删除呢也就是我们链表的删除一样,我们找到hashi=key%size()的位置,然后在这个串上找key的位置,但是要定义一个prev来记录key的上一个节点的位置,便于链接;

 6、哈希桶(开散列)的查找

查找呢也就像链表一样,到key取完余数以后相同的链表上去找key即可;

非常简单,上菜

Node* Find(const K& key)
{
	if (_tables.size() == 0)
		return nullptr;

	size_t hashi = key % _tables.size();
	Node* cur = _tables[hashi];
	while (cur)
	{
		if (cur->_kv.first == key)
		{
			return cur;
		}

		cur = cur->_next;
	}

	return nullptr;
}

下期进行unordered的封装


总结

哈希表是在一个顺序表里面去找hashi(key%size())的位置,存在向后偏移,空就存储。

和哈希桶在顺序表里面存放单链表,把hashi相同的数据串联在一起;

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

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

相关文章

解决Linux服务器上下载pytorch速度过慢的问题

需要下载的是GPU版本的pytorch&#xff0c;版本torch1.13.1cu116 尝试方法1&#xff1a; pip install torch1.13.1cu116 torchvision0.14.1cu116 torchaudio0.13.1 --extra-index-url https://download.pytorch.org/whl/cu116 但是默认是从官网下载&#xff0c;龟速到200kb/s左…

Vscode中启动Vue2.x项目运行正常但templete部分UI组件红色波浪线报错 ts(2339)

Vscode中启动Vue2.x项目运行正常但templete部分UI组件红色波浪线报错 错误示例 原因 Vue - Official 插件升级导致的问题(具体原因有待查询) 解决方案 打开Vscode软件 —> 找到扩展插件 —> 选择Vue - Official —> 安装特定版本(版本 < V2.0.28就行) —> 重…

linux-L7-linux 查看json文件

输入如下进行查看 cat your_file.json | less

深入解析软硬复位

在集成电路IC设计中,复位是一个至关重要的过程,它用于保证芯片的各个模块在启动、故障或其他特定条件下能重新回到初始状态。复位通常可以分为三类:硬复位、软复位以及上电复位。这三类复位虽然都有相似的目标,但其产生机制和作用范围各不相同。 一、硬复位 1.1 定义与原…

Redis底层数据结构(详细篇)

Redis底层数据结构 一、常见数据结构的底层数据结构1、动态字符串SDS&#xff08;Simple Dynamic String&#xff09;组成 2、IntSet组成如何保证动态如何确保有序呢? 底层如何查找的呢? 3、Dict(dictionary)3.1组成3.2 扩容3.3 收缩3.4 rehash 4、ZipList连锁更新问题总结特…

论文阅读《Robust Steganography for High Quality Images》高质量因子图片的鲁棒隐写

TCSVT 2023 中国科学技术大学 Kai Zeng, Kejiang Chen*, Weiming Zhang, Yaofei Wang, Nenghai Yu, "Robust Steganography for High Quality Images," in IEEE Transactions on Circuits and Systems for Video Technology, doi: 10.1109/TCSVT.2023.3250750. 一、…

三方共建 | 网络安全运营中心正式揭牌成立

9月3日&#xff0c;广州迎来了一场网络安全领域的盛事。悦学科技、聚铭网络、微步在线联合打造的7x24小时网络安全运营中心&#xff08;以下简称“中心”&#xff09;正式成立&#xff0c;并在现场举行了庄重而热烈的揭牌仪式。众多行业专家、企业代表齐聚一堂&#xff0c;共同…

【C++ Primer Plus习题】16.2

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream> #include <string> #inc…

【Python决策树】ID3方法建立决策树为字典格式,并调用 treelib 显示

首先&#xff0c;我们使用 treelib 库来显示树结构 : ps : 如果 treelib 输出一堆乱码, 可以点进Tree修改 tree.py 大概 930 行左右的部分(去掉encode就行了) if stdout:print(self._reader) # print(self._reader.encode("utf-8"))else:return self._reader将字典…

基于Python的B站热门视频可视化分析与挖掘系统

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长 QQ 名片 :) 1. 项目简介 随着互联网视频平台的迅猛发展&#xff0c;如何从海量的数据中提炼出有价值的信息成为了内容创作者们关注的重点之一。B站&#xff08;哔哩哔哩&#xff09;作为国内领先的年轻人文化社区&#xf…

应用层协议 —— https

目录 http的缺点 https 安全与加密 运营商挟持 常见的加密方式 对称加密 非对称加密 数据摘要&#xff08;数据指纹&#xff09; 不安全加密策略 1 只使用对称加密 2 只使用非对称加密 3 双方都是用非对称加密 4 对称加密和非对称加密 解决方案 CA证书 http的缺点 我们可…

基于鸿蒙API10的RTSP播放器(八:音量和亮度调节功能的整合)

一、前言&#xff1a; 笔者在前面第六、七节文章当中&#xff0c;分别指出了音量和屏幕亮度的前置知识&#xff0c;在本节当中&#xff0c;我们将一并实现这两个功能&#xff0c;从而接续第五节内容。本文的逻辑分三大部分&#xff0c;先说用到的变量&#xff0c;再说界面&…

智慧环保平台建设方案

智慧环保平台建设方案摘要 政策导向与建设背景 背景&#xff1a;全国生态环境保护大会提出坚决打好污染防治攻坚战&#xff0c;推动生态文明建设&#xff0c;目标是在2035年实现生态环境质量根本好转。构建生态文明体系&#xff0c;包括生态文化、生态经济、目标责任、生态文明…

表格标记<table>

一.表格标记、 1table&#xff1a;表格标记 2.caption:表单标题标记 3.tr:表格行标记 4.td:表格中数据单元格标记 5.th:标题单元格 table标记是表格中最外层标记&#xff0c;tr表示表格中的行标记&#xff0c;一对<tr>表示表格中的一行&#xff0c;在<tr>中可…

Excel数据转置|Excel数据旋转90°

Excel数据转置|Excel数据旋转90 将需要转置的数据复制在旁边空格处点击鼠标右键&#xff0c;选择图中转置按钮&#xff0c;即可完成数据的转置。&#xff01;&#xff01;&#xff01;&#xff01;非常有用啊啊啊&#xff01;&#xff01;&#xff01;

嵌入式Linux学习笔记(2)-C语言编译过程

c语言的编译分为4个过程&#xff0c;分别是预处理&#xff0c;编译&#xff0c;汇编&#xff0c;链接。 一、预处理 预处理是c语言编译的第一个阶段&#xff0c;该任务主要由预处理器完成。预处理器会根据预处理指令对源代码进行处理&#xff0c;将预处理指令替换为相应的内容…

游戏各个知识小点汇总

抗锯齿原理记录 SSAA&#xff1a;把成像的图片放大N倍&#xff0c;然后每N个点进行平均值计算。一般N为2的倍数。比如原始尺寸是1000x1000&#xff0c;长宽各放大2倍变成2000x2000。 举例&#xff1a; 原始尺寸&#xff1a; 放大2倍后 最后平均值计算成像&#xff1a; MSAA&…

基于SpringBoot+Vue的网上蛋糕销售系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、SSM项目源码 系统展示 【2025最新】基于JavaSpringBootVueMySQL的…

踩坑记:Poco库,MySql,解析大文本的bug

这两天在调试一个小功能&#xff0c;使用c,读取MySql。使用的是Poco库。按照官网的写法&#xff1a; std::cout << "read normal data by poco recordset "<<std::endl;Poco::Data::MySQL::Connector::registerConnector();Poco::Data::Session session(…

.NET 6.0 + WPF 使用 Prism 框架实现导航

合集 - .NET 基础知识(3) 1..NET 9 优化&#xff0c;抢先体验 C# 13 新特性08-202.《黑神话&#xff1a;悟空》神话再现&#xff0c;虚幻引擎与Unity/C#谁更强&#xff1f;08-21 3..NET 6.0 WPF 使用 Prism 框架实现导航09-11 收起 阅读目录 前言什么是Prism?安装 Prism使…