数据结构(14)——哈希表(1)

news2024/11/15 17:15:55

欢迎来到博主的专栏:数据结构
博主ID:代码小豪

文章目录

    • 哈希表的思想
    • 映射方法(哈希函数)
      • 除留余数法
    • 哈希表
      • insert
      • 闭散列
      • 负载因子
      • 扩容
      • find和erase

哈希表的思想

在以往的线性表中,查找速度取决于线性表是否有序,如果是无序的线性表,我们就要从表头开始匹配key值是否相同,因此时间复杂度取决于线性表的元素个数,为O(N)。

如果线性表有序,则可以通过二分查找法大大的减少查找时间,时间复杂度为O(logN),这个时间复杂度看起来让人满意,但是我们要考虑到一点,那就是保持线性表的有序性是要付出代价的,使用排序算法也有O(N*logN)的时间复杂度。

即使是使用关联式容器,它的查找速度也是O(logN),但是好处在于插入和删除元素并不会破坏查找速度。但是查找的速度就局限于此了吗?

已现实为例,如果我们想要在城里找到大舅妈,肯定不会遍历整个城的街道,从街道口走到街道尾去找大舅妈的家,我们会先知道大舅妈的住址,直接去这个住址就能找到大舅妈。

这就是哈希表的查找的方式了,将key值放在映射的地址处,这样查找key值就不用从头开始遍历了。举个例子,假设现在有一个能容纳十个元素的哈希表。我们规定key值的个位数就是映射的地址值,即让1位于1号,2位于2号。以此类推,而10则放在0号。

在这里插入图片描述

假设我们要查找13,那么去3号处就能找到对应值,查找速度为O(1)。这就是哈希表查找的优势。

映射方法(哈希函数)

哈希表中的映射方法叫做哈希函数,以上表为例,其哈希函数为F(key)=key%10。于是F(13)=3,F(25)=5。一个好的哈希函数很重要。这将决定哈希表的查找,插入,删除的速率(最主要还是查找)。哈希函数需要能将key值的数据转换成整形的能力。

但是并非所有类型都可以和整形转换。比如string就不能通过F(key)=key%10的方式获得映射值,此时我们就需要设计一个使用于string的哈希函数,比如可以设计让string的所有元素相加为映射值的哈希函数,即F(string)={a1+a2+……+an}。实际上字符串的哈希函数设计绝没这么简单,感兴趣可以在网上搜索。

那么自定义类型当然也要依靠自定义的哈希函数才能获得其映射值,通常哈希函数的设计需要考虑一下几点:
(1)选取的key值的独特性,比如我们在数据库中查找一个人,如果选择“姓名”作为映射值,那么肯定效率不佳,因为全国同名同性的人是在太多了,如果我们选择“生日+姓名”作为映射值就会好很多。

(2)哈希函数的结果要尽可能的分散,假设现在有N个元素要插入到哈希表,那么它们的映射值越分散在哈希表就越好,如果N个元素经过哈希函数的运算结果的映射值都是1,那么肯定是毫无效率可言的。

(3)哈希函数的计算结果要包含在哈希表的域中,如果一个哈希表能容纳100个元素(即可容纳映射值0-99的元素),那么如果一个key值计算出映射值为101,那么肯定是没法插入的。

在c++标准库中的unordered_map,就是用哈希表为底层的容器,其允许我们传入自定义的哈希函数,已适配那些自定义的类型的映射值计算。

template < class Key,                                    // unordered_map::key_type
           class T,                                      // unordered_map::mapped_type
           class Hash = hash<Key>, //可以传入自定义的哈希函数// unordered_map::hasher
           class Pred = equal_to<Key>,                   // unordered_map::key_equal
           class Alloc = allocator< pair<const Key,T> >  // unordered_map::allocator_type
           > class unordered_map;

除留余数法

除留余数法是一个比较通用,而且简单的哈希函数。其主要方法为:

设哈希表可容纳最大地址数为m,取一个不超过m的值p作为除数,其哈希函数为:hash(key)=key%p。

比如当前哈希表的最大地址数为50,待插入的key的哈希值为313,则其映射值为313%50=13。

在后续哈希表的底层设计中,博主将采用这个方法。

哈希表

hash表的底层可以用序列式容器vector,或者deque,因为哈希表的映射值与下标很适配。

template<class key,class value>
class hash_tab
{
public:
	hash_tab()
	{
		_tab.resize(10);
		_n = 0;
	}

	typedef pair<key, value> value_type;
	
private:
	vector<hash_data<value_type>> _tab;
	size_t _n;//当前有效数据
};

而哈希表每个元素都要存储两个数据,分别是data,以及状态state。由于vector一次性开了十个元素的空间,因此有些空间是没有有效数据的。这些没有有效数据的元素的状态记为空(empty),如果元素具有有效数据则记为有(exist),如果元素的有效数据被删除,则记为删除(delete)。

enum state
{
	EXIST,//存在映射
	DELETE,//删除映射
	EMPTY//空的映射
};

template<class T>
struct hash_data
{
	hash_data()
	{
		_state = EMPTY;
	}

	hash_data(const T& data,state state)
	{
		_data = data;
		_state = state;
	}

	const hash_data& operator=(const hash_data& hashdata)
	{
		_data = hashdata._data;
		_state = hashdata._state;
		return *this;
	}

	T _data;
	state _state;
};

insert

insert的方式如下:
(1)先通过hash函数计算出key值的映射地址处。博主采用除留余数法。
(2)将元素插入到映射值对应的地方。

比如现在插入的元素key值为60,由于当前哈希表的最大空间为10(默认构造函数),因此除留余数法为hash(60)=60%10=0。插入在0下标处。
在这里插入图片描述
ok,现在我们来面临第一个问题,如果我们现在插入30,那么它该插入在什么位置呢?根据哈希函数hash(30)=30%10=0。那么它应该插入在映射值为0的下标处,但是0下标处已经存在60这个数据了,那么该如何处理呢?

这种情况被称为“哈希冲突”,即不同的key值(30与60),但是经过哈希函数计算后,得到了相同的映射值(0)。如果想要减少哈希冲突,优化哈希函数是一个不错的选择,但是这并不能解决哈希冲突,通常我们会用两种方法解决哈希冲突,闭散列和开散列。这篇文章我们先来了解闭散列的哈希表,在下一篇中博主再谈谈开散列的哈希表如何实现。

闭散列

闭散列:也称开放地址法,当发生哈希冲突时。探测哈希表的空位置,将key值插入在冲突位置的下一个空位置处。探测方法也分为多种

方法(1)线性探测

线性探测:从冲突位置开始,依次往后探测,直到找到下一个新位置为止。以上例为例,由于30插入的位置与60发生了哈希冲突,那么30从0下标开始线性探测后续位置,直到遇到第一个空位置(EMPTY),DELETE也算作空位置。
在这里插入图片描述
如果此时在插入一个20,与60依然发生了哈希冲突,根据线性探测的方法,20应该插入在下标2的位置。
在这里插入图片描述
假如这个哈希表满了,那么再次插入一个key值会发生什么事呢?
在这里插入图片描述
根据线性探测的规则,插入2的位置会陷入一个死循环,因为在这个哈希表中已经不存在空位置了,在这个哈希表中无论探测多少次都找不到位置。

实际上,当哈希表中的元素占比整个空间越来越多时,哈希冲突发生的几率会越来越高,而每次发生哈希冲突时,都会带来额外的时间开销(线性探测)。因此,最好的解决方法是控制元素与空间之间的占比。

负载因子

我们将哈希表中元素与空间之间的占比成为负载因子。即:

负载因子=元素个数÷哈希表的总容量

负载因子与元素个数成正比,与哈希表的总容量成反比,当负载因子小时,发生哈希冲突的几率低,插入效率高,但是空间利用率也低。当负载因子大时,发生哈希冲突的几率大,插入效率低,但是空间利用率高。因此控制负载因子在一定的数值内也是很重要的。

通常来说,采取线性探测的哈希表,其负载因子应该控制到0.7~0.8。如果负载因子超过这个区间,就要对哈希表进行扩容。以降低哈希表的负载因子。

扩容

哈希表的扩容策略是异地扩容,即先构造一个新的哈希表,这个新的哈希表是原哈希表的二倍,然后再讲原哈希表的元素重新插入在新哈希表当中。最后交换新旧两个哈希表。那么为什么要这样做呢?与其说异地扩容的好处,不如讲讲原地扩容的坏处。

假如我们将上例中的哈希表扩容两倍,即新哈希表的容量为20.
在这里插入图片描述
由于哈希表的最大容量发生了变化,那么哈希函数也会随之变化,因为我们设定的哈希函数为

hash(key)=key%size

由于size从10变成了20,那么哈希函数就变成了key%20。那么哈希表中17的映射值为
hash(17)=17%20=17,这个插入位置就不对。因此更好的方式是新建一个新哈希表。将旧表的内容插入至新表当中。
在这里插入图片描述

	bool insert(const value_type& kv)
	{
		//负载因子过多,需要扩容 负载因子等于size%_n
		if (double(_n) / _tab.size() >= 0.7)
		{
			hash_tab new_tab;
			new_tab._tab.resize(_tab.size() * 2);//建立新表
			for (auto& e : _tab)
			{
				if (e._state == EXIST)//只插入有效值
				{
					new_tab.insert(e._data);//将旧表的值插入到新表当中
				}
			}
			_tab.swap(new_tab._tab);//交换新旧两表
		}

		size_t hashnum = kv.first % _tab.size();//哈希函数——除留余数法
		while (_tab[hashnum]._state ==EXIST )//线性探测
		{
			hashnum++;
			hashnum %= _tab.size();
		}
		_tab[hashnum] = hash_data<value_type>(kv, EXIST);//插入新值
		_n++;
		return true;
	}

find和erase

find函数的方法如下:

通过哈希函数计算出映射值,找到映射的位置,然后线性检测找到正确的key值,然后返回节点,如果线性检测到空节点(DELETE不算空节点,仅EMPTY)。就说明哈希表中不存在对应key值,返回nullptr。

在这里插入图片描述

	hash_data<value_type>* find(const key& key)
	{
		size_t hashnum = key % _tab.size();
		while (_tab[hashnum]._state != EMPTY)
		{
			if (key == _tab[hashnum]._data.first&&_tab[hashnum]._state!=DELETE)
			{
				return &_tab[hashnum];
			}
			hashnum++;
			hashnum %= _tab.size();
		}
		return nullptr;

	}

erase的实现就更简单,我们先通过find查找到待删除的元素,然后将该元素的状态调整成DELETE就可以了,因为对这个元素的data进行修改不是一个明智的选择。这会影响到映射关系。


bool erase(const key& key)
{
	size_t hashnum = key % _tab.size();
	hash_data<value_type>* ptr=find(key);
	if (ptr == nullptr)
		return false;
	ptr->_state = DELETE;
	_n--;
	return true;
}

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

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

相关文章

Origin 2024下载安装教程(中文版软件包) 百度网盘分享链接地址

Origin是什么软件&#xff1f; origin主要是绘图、数据分析、数据导入导出的功能。Origin 广泛应用于科学研究、工程技术、数据分析等领域&#xff0c;Origin 是一款功能强大、易于使用的科学绘图和数据分析软件&#xff0c;能够帮助你高效地处理和可视化数据&#xff0c;为你…

C程序设计——函数0

函数定义 前面说过C语言是结构化的程序设计语言&#xff0c;他把所有问题抽象为数据和对数据的操作&#xff0c;前面讲的变量、常量&#xff0c;都是数据。现在开始讲对数据操作——函数。 C语言的函数&#xff0c;定义方式如下&#xff1a; 返回值类型 函数名(参数列表) {…

论文速读|重新审视奖励设计与评估:用于强健人型机器人站立与行走控制的方法

论文地址&#xff1a;https://arxiv.org/pdf/2404.19173 这篇论文为类人机器人站立和行走&#xff08;SaW&#xff09;控制器的持续可衡量改进奠定了基础。通过引入一套定量实际基准测试方法&#xff0c;作者展示了现有控制器的优缺点&#xff0c;并通过基准测试指导新控制器的…

龙芯L2K0300开发板综合测试

CPU 查看cpu版本信息 cat /proc/cpuinfo可以看到cpu是64位的LoongsonArch架构 stress压力测试结果 RAM 久久派板载512MB DDR4-2666内存&#xff0c;查看内存信息 cat /proc/meminfo可以用memtester进行内存性能测试 memtester <size> <times>memtester测试结果…

Java 工程师转型大数据的优势——别小看自己!

时间&#xff1a;2024年09月05日 作者&#xff1a;小蒋聊技术 邮箱&#xff1a;wei_wei10163.com 微信&#xff1a;wei_wei10 音频地址: https://xima.tv/1_U3suSJ?_sonic0 希望大家帮个忙&#xff01;如果大家有工作机会&#xff0c;希望帮小蒋推荐一下&#xff0c;小蒋希…

2024国赛数学建模A题思路模型

完整的思路模型请查看文末名片

机器学习:opencv图像识别--模版匹配

目录 一、模版匹配的核心概念 1.图片模板匹配是一种用于在图像中查找特定模式或对象的技术。 2.模板图像 3.目标图像 4.滑动窗口 5.相似度度量 6.匹配位置 二、模版匹配的步骤 1.准备图像&#xff1a; 2.预处理&#xff1a; 3.匹配&#xff1a; 4.定位最佳匹配&…

【MySQL】初识MySQL—MySQL是啥,以及如何简单操作???

前言&#xff1a; &#x1f31f;&#x1f31f;本期讲解关于MySQL的简单使用和注意事项&#xff0c;希望能帮到屏幕前的你。 &#x1f308;上期博客在这里&#xff1a;http://t.csdnimg.cn/wwaqe &#x1f308;感兴趣的小伙伴看一看小编主页&#xff1a;GGBondlctrl-CSDN博客 目…

2024数学建模国赛题目A-E题

2024数学建模国赛题目A-E题已经发布 各个赛题题目如下 A题 板凳龙 闹元宵 B题 生产过程中的决策问题 C题 农作物的种植策略 D题 反潜航空深弹命中概率问题 E题 交通流量管控 Csdn在文末&#xff0c;关注云顶数模公众号&#xff0c;或者点击下方名片。

2024年高教社杯数学建模国赛赛题浅析——助攻快速选题

一图流——一张图读懂国赛 总体概述&#xff1a; A题偏几何与运动学模型&#xff0c;适合有几何与物理背景的队伍&#xff0c;数据处理复杂性中等。 B题侧重统计和优化&#xff0c;适合有运筹学和经济学背景的队伍&#xff0c;数据处理较为直接但涉及多步骤的决策优化。 C题…

新手c语言讲解及题目分享(十六)--文件系统专项练习

在我刚开始学习c语言的时候就跳过了这一章节&#xff0c;但在后面慢慢发现这一章节还是比较重要的,如果你报考了计算机二级c语言的话&#xff0c;你应该可以看到后面的三个大题有时会涉及到这章。所以说这章还是非常重要的。 目录 前言 一.打开文件 1.Fopen( )函数返回值 2&…

Keil发现Error: C9555E: Failed to check out a license

遇到这样的问题 解决办法&#xff1a; 换成这个版本 然后重新用keygen生成license

如何理解进程和线程之间的关系

目录 前言 一、进程和线程的关系 1、引入线程的原因 2、线程的特点 3、线程和进程的关系 二、如何在进程中创建线程 1、创建线程的函数 2、举例使用&#xff1a; 三、线程间的同步互斥机制 1、什么是同步互斥机制 2、如何在线程中使用同步互斥机制 3、实际举例 总结 前言 …

为什么要有RPC

​ 1. RPC&#xff08;Remote Procedure Call&#xff09; 定义&#xff1a; RPC&#xff08;Remote Procedure Call&#xff0c;远程过程调用&#xff09;是一种允许程序在不同的地址空间&#xff08;通常是在网络上的不同机器&#xff09;之间调用函数或方法的机制。它使得…

代码随想录算法训练营Day03 | 链表理论基础、203.移除链表元素 、707.设计链表、206.反转链表

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 链表理论基础203.移除链表元素思路与重点 707.设计链表思路与重点 206.反转链表思路与重点 链表理论基础 C/C的定义链表节点方式&#xff1a; // 单链表 struct L…

vue part 8

浏览器本地存储 application&#xff0c; local storage中 js方法肯定会用很多呀&#xff0c;只是不直接操作dom了但是对对象和数组进行操作还是原先的方法&#xff0c;jq的话想用引入就可以了。我是直接放弃jq了&#xff0c;在框架中用jq包不好 sessionStorage.HTML <!…

Git+word记笔记

程序员记笔记主要同步很重要&#xff0c;我这个方法只支持个人笔记&#xff0c;如果团队还是用企业微信开个企业会员比较方便。为什么用word&#xff0c;因为可以镶嵌代码和文档&#xff0c;不仅仅是文字&#xff0c;兼容性强 语雀&#xff0c;云笔记这些对于上传的word都是有…

AI大模型编写多线程并发框架(六十五):发布和应用

系列文章目录 文章目录 系列文章目录前言一、项目背景二、第十三轮对话-优化传参三、第十四轮对话-释放资源四、完善所有单元测试五、验证通过六、发布七、参考文章 前言 在这个充满技术创新的时代&#xff0c;AI大模型正成为开发者们的新宠。它们可以帮助我们完成从简单的问答…

IA——网络操作设备VRP简介

一&#xff0c;VRP简介 二&#xff0c;网络设备的管理 &#xff08;1&#xff09;console口&#xff1a; &#xff08;2&#xff09;talnet: &#xff08;3&#xff09;SSH: 安全的远程登陆 &#xff08;4&#xff09;通过WEB页面登录&#xff1a; 三&#xff0c;命令行常见…

TikTok养号一般养几天?账号起步方法

TikTok养号是一个关键的步骤&#xff0c;它可以帮助新账号快速积累粉丝和观众&#xff0c;增加视频的曝光和互动率&#xff0c;从而提升账号的影响力和可见性。但是养号也并不是简单的登录账号、互动点赞&#xff0c;而是从底层设备到分发频率都需要讲究方法&#xff0c;否则号…