数据结构与算法详解——散列表篇(附c++实现代码)_设计算法,将数组a 进行散列存储,以解决-程序员宅基地

技术标签: c++  散列表  数据结构与算法  数据结构  哈希表  

散列表

  前面数组、链表、栈、队列都是序列式容器,存储的都是一个元素。而散列表又叫哈希表(hash table),是一种关联式容器,存储的是一对值,一般是一个key对应一个value(又叫键值对)。
  c++ stl中的map就是一个散列表,举个例子:

std::map<std::string,int> m;
m["小明"]=170;
std::cout<<"小明的身高是"<<m["小明"]<<std::endl;

  m是一个key是字符串类型,value是整型的哈希表,这里我们用来存储人名和身高,(“小明”,170)就是一对键值对,访问m[“小明”]得到的就是一个int类型的数字170。

散列函数

  上述例子中m[“小明”]的形式看起来是不是很像数组,只不过数组的下标是非负整数,而散列表的下标可以是任意类型。我们可以用数组来实现散列表,那就可以用到数组支持随机访问时间复杂度为O(1)的特性了。
  那么我们就需要将key转换成数组的下标,这里就用到了散列函数,散列函数的要求:

  1. hash(key)的结果是非负整数(数组下标)
  2. if key1==key2,hash(key1)==hash(key2)
  3. if key1!=key2,hash(key1)!=hash(key2)

  这三条规则应该都比较好理解,然而实际情况中,第三点是基本做不到的,就算是著名的哈希算法MD5,SHA都难以避免会有不同key算出相同的hash值的情况,又称为哈希冲突。
  哈希算法有很多种,但是好的哈希算法应该尽量少地出现哈希冲突。举个例子,上面key为"小明",我们可以将中文转换成GB2312编码,相加然后取余数组的空间大小capacity,就能转换成[0,capacity)的数组下标了,但是这样简单的算法会出现很多哈希冲突,只要任意n个中文字的GB2312编码相加的结果相同,就会得到相同的哈希值,造成哈希冲突。

哈希冲突

  前面说到哈希冲突是无法避免的,假设hash(“小明”)=hash(“小红”)=x,那么我们在存储时就会发生错误:

//a是数组
a["小明"]=170;	//实际执行a[x]=170
a["小红"]=160;	//实际执行a[x]=160

  小红的数据把小明的数据覆盖了,当访问a[“小明”]时就会得到160,显然是错误的。
  那如何解决哈希冲突呢?一般有两种方法:开放地址法和链表法。

开放地址法

线性探测

  线性探测的思想很简单,当我要插入数据时,如果这个下标已经有数据就往后找,找到一个没有数据的位置就可以插入。
  插入:上述例子中,当我们想要插入小红的数据时,我们发现a[x]已经有数据了(已经插入了小明的170),那我们可以往后找一个空的位置插入160,如果x+1的位置是空的就插入a[x+1]=160,如果x+1有数据了就看x+2的位置有没有数据,以此类推。如果超过了capacity,就循环回到0的位置。
  查找:查找的过程和插入一样,我们可以让定义一个含有key和value的类,然后创建一个存储该类对象指针的数组。如果我们要访问a[“小红”],计算hash(“小红”)=x,然后对比a[x]->key是否等于"小红",如果不是就往后继续对比,直到遇到第一个空闲的位置。
  如果要查找160是否在a中存在,也是一样的做法,只不过对比的是a[x]->value。
  删除:这里要注意的是,删除的时候不能直接把元素设置为空或者初始值。为什么?我们来看一个例子:

数组下标 数组存储的值
0 (小明,170)
1 (小红,160)
2 (小陈,180)
3

  现在我们要删除(小红,160),如果把元素设为空:

数组下标 数组存储的值
0 (小明,170)
1
2 (小陈,180)
3

  那我们现在要查找小陈的数据的时候,就找不到了,而小陈的数据确实是存在的。因此删除的时候不能直接设置为空或者初始值,我们可以设置一个flag标志这个元素已经删除,查找的时候遇到删除标志继续往下探测,查找到这个数据的时候再查看这个标志来确定这个元素是否已经删除。
  当散列表插入的数据越来越多的时候,哈希冲突发生的概率越来越大,线性探测需要的时间也越来越久,最坏的情况下探测需要遍历整个数组,时间复杂度为O(n),同理查找和删除也是。

二次探测

  假设hash(key)=x,线性探测就是按x,x+1,x+2,x+3的步长进行探测,而二次探测是按x,x+11,x+22,x+3*3的步长进行探测。

双重散列

  双重散列的思路就是使用多个哈希函数,当hash1(key)的位置已经有数据的时候,计算hash2(key),hash3(key)…,直到找到一个空闲位置插入数据。

链表法

  链表法在数组的每个槽都维护一个链表,当发生散列冲突的时候,存储在链表的结点里,插入查找删除的时间复杂就是链表插入查找删除的时间复杂度。
在这里插入图片描述

两种方法优缺点比较

开放地址法:

优点:

  1. 所有数据都存在数组里,可以有效利用CPU缓存
  2. 所有数据都存在数组里,便于序列化

缺点:

  1. 删除需要使用特殊标志
  2. 发生冲突时插入查找删除都需要探测,代价较高
  3. 装载因子不能太大,需要提前申请好内存,这也导致了比链表法更加浪费内存

适用情况:

  • 数据量小,装载因子小

链表法:

优点:

  1. 需要的时候才申请结点而不是一开始就申请好,内存利用率更高
  2. 对装载因子容忍度高,就算装载因子很大,只要哈希函数分布比较平均,链表长度虽然变长,但是相当于均摊到每一条链表了,所以比起纯数组还是要快。
  3. 当数据量大时,可以使用红黑树或者跳表代替链表实现优化,使得查找效率从O(n)变成O(logn)。

缺点:

  1. 链表结点在内存中不连续,对CPU缓存不友好
  2. 需要存储额外的指针,如果存储小的对象会消耗更多的内存,如果存储较大的对象的话指针的消耗可以忽略不计。

适用情况:存储大对象,大数据量

装载因子

装载因子loadfactor = 填入表中的元素个数 / 散列表的长度

  装载因子越大,说明散列表越满,哈希冲突的概率越大,开放地址法的探测次数增加,链表法的链表长度会增大,导致散列表的性能会下降。
  一般我们会为装载因子设置一个阈值,当到达或者超过这个阈值后散列表进行动态扩容。
  装载因子的阈值设置需要权衡时间,空间的需求。如果对内存充足,对执行效率性能要求比较高,可以将阈值设置小一点;如果内存紧张,对执行效率性能要求不敏感,可以将阈值设置大一些,甚至可以超过1。Java的HashMap中默认的最大装载因子是0.75。

动态扩容

  上面说到装载因子达到某个阈值时,散列表需要动态扩容以减小散列冲突:申请更大的数组空间,旧数据重新计算哈希值,搬移到新的空间上。
  在实际中,如果我们提供对外服务,在插入某个数据后启动动态扩容,一次性将所有旧数据计算哈希值并搬移到新空间上,可能会导致某一段时间无法响应用户的请求。
  因此,在动态扩容时,我们可以先申请空间,但是先不计算哈希值和搬移数据。在需要插入新数据时,我们将新数据插入新的空间,然后从旧散列表中取一个数据计算哈希值插入新的空间里。这样的话在查找的时候也需要兼顾新旧两个散列表,先从新散列表里找,如果找不到再到旧散列表里找。
  这种做法我们将动态扩容均摊到插入操作中,每次插入操作的时间复杂度是O(1),这样的做法会更加地柔和,避免了一次性动态扩容耗时过高。

实现

类的定义

  这里我们使用链表法解决哈希冲突,我们定义链表的结点,需要存储键值对和next指针:

template <typename K, typename V>
class Entry {
   
    
public:
	K key;
	V value;
	Entry<K, V>* next;
	Entry(Entry<K, V>* next) {
   
    
		this->next = next;
	}
	Entry(K key, V value, Entry<K, V>* next) {
   
    
		this->key = key;
		this->value = value;
		this->next = next;
	}
};

  哈希表类的定义:
  注意table是一个二维指针,因为table是一个指针数组,存储的是链表结点的指针。

template <typename K,typename V>
class hashtable {
   
    
private:
	const int default_init_capacity = 8;		//初始化大小
	const float load_factor = 0.75f;		//装载因子的阈值
	Entry<K, V>** table;
	int capacity = default_init_capacity;		//容量
	int size=0;			//实际数量
	int used=0;			//已经使用的索引的数量,也就是table的下标已经使用了的数量
	const std::hash<K> _hasher;
public:
	hashtable() {
   
    
		table = new Entry<K, V>*[default_init_capacity];
		for (int i = 0; i < default_init_capacity; i++)
			table[i] = nullptr;
	}
	~hashtable();
	void put(K key, V value);
	void remove(K key);
	V& get(K key);
	V& operator[](K key);
	void print();
private:
	size_t hash(K key);
	void resize();			//扩容
}
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/JMasker/article/details/124745806

智能推荐

Vue 父组件axios异步更新数据,子组件props 获取不到数据(mounted中),解决办法_vue axios异步请求没有返回就调用了mounted-程序员宅基地

文章浏览阅读1.2k次。当父组件axjos获取数据,子组件使用props接收数据时,执行mounted的时候axjos还没有返回数据,而且mounted只执行一次,这时 props中接收的数据为空解决方案:在对应组件中判断数据的长度..._vue axios异步请求没有返回就调用了mounted

基于Java+SpringBoot+Vue前后端分离仓库管理系统设计实现_仓库管理后端接口案例-程序员宅基地

文章浏览阅读6.2w次,点赞203次,收藏705次。仓库管理系统进行了字典管理、公告管理、用户管理、物资管理、物资申请管理、仓库员管理、统计报表等服务。设备采用关联数据库里的MySQL做为全面的数据库,合理存放数据,合理备份数据,确保数据稳定性。除此之外,程序流程还具备程序流程所需要的所有功能,大大提升了实际操作安全度,使库房管理系统软件从概念迈向实际,真真正正提升了信息资源管理效率。_仓库管理后端接口案例

电脑端播放m3u8视频_m3u8直接打开-程序员宅基地

文章浏览阅读8.2k次,点赞2次,收藏3次。电脑端M3U8文件转mp4概述:在视频这块,m3u8是一种索引文件,其文件内容索引至多个无格式或ts格式的文件。m3u8在ios自带浏览器可直接打开,安卓上的uc浏览器\QQ浏览器等也能运行行。ts格式的windows可以打开,无格式的windos不能直接打开,windows端有几种方案可合成并打开这些文件。我实现的是windows下,借助bat脚本语言和格式工厂的方法,实现m3u8文件所索引..._m3u8直接打开

Linux服务器开发一(基础)_linux 服务器-程序员宅基地

文章浏览阅读6k次,点赞9次,收藏62次。Linux1、Linux介绍Linux是类Unix计算机操作系统的统称。Linux操作系统的内核的名字也是“Linux”。Linux这个词本身只表示Linux内核,但在实际上人们已经习惯了用Linux来形容整个基于Linux内核的系统。Linux是由芬兰大学Linus Torvalds于1991年编写的。2、Linux发行版组成Linux内核应用软件一些GNU程_linux 服务器

【知识回顾】Java常用类库-Java Runtime-程序员宅基地

文章浏览阅读223次。Java Runtime 类是 Java 标准库中的关键类之一,它提供了对当前Java虚拟机实例的访问和控制,允许程序动态地修改和管理运行时环境。每个Java应用程序都有一个Runtime类实例,使得程序能够与其运行的环境相连接。Runtime类所在包为java.lang包,因此在使用的时候不需要进行导包。并且Runtime类被public修饰了,因此该类是可以被继承的。

计算机同步与异步的概念,同步与异步到底是什么???-程序员宅基地

文章浏览阅读2.1k次,点赞3次,收藏4次。总得来说,同步异步出现在以下几个领域:1 计算机网络。数据通信技术中有同步通信与异步通信。同步通信简单的说就是你在发送数据时候我必须同时接受。这个过程有精确的时钟控制。而异步通信是你在发数据时候必须加上开始与结束符号,这样我才可以接受,异步通信没有时钟控制。因为没有了时钟的控制(额外硬件),所以成本低,设备简单,但是传输效率较低。(开始与结束符占了开销)。在网络协议(network protoc..._计算机里的同步和异步

随便推点

计算机毕业设计 asp.net高校失物招领网 毕设-程序员宅基地

文章浏览阅读417次,点赞4次,收藏6次。用户的需求具体体现在各种学习文件的提供、保存、更新和查询方面,这就要求数据库结构能充分满足各种信息的输入和输出。得到上面数据项和数据结构以后,就可以设计出能够满足用户需求的各种实体,以及它们之间的关系,为后面的逻辑结构设计打下基础。根据系统的需求,该系统应该具有六个功能模块:会员注册模块,新闻发布模块,留言本模块,拾到信息公布享模块。设计规划出的实体有:管理员信息实体、注册用户信息实体、信息实体、拾到信息实体、站内新闻实体。2)注册用户信息,包括数据项:ID(系统自动编号),姓名,性别、班级等。

Sora,OpenAI带来的视觉革新-程序员宅基地

文章浏览阅读499次,点赞9次,收藏13次。想象一下,如果你可以仅通过敲击键盘,就能让你的思维火花转化为眼前跳动的影像,那会是怎样的体验?这不是科幻小说的情节,而是OpenAI最新推出的神奇工具——Sora。

Ubuntu20.04 LTS 安装 ros Noetic 树莓派4/PC_树莓派 ubuntu 20.04安装-程序员宅基地

文章浏览阅读655次。Ubuntu 20.04 LTS安装树莓派系统_树莓派 ubuntu 20.04安装

Webug靶场之旅——显错注入-程序员宅基地

文章浏览阅读742次,点赞2次,收藏2次。显错注入显错注入是SQL注入的一种,而SQL注入是指web应用程序对用户输入数据的合法性没有判断,导致攻击者可以构造不同的sql语句来实现对数据库的操作。SQL注入漏洞产生的条件: 1、用户能够控制数据的输入。 2、原本执行需要执行的代码时,拼接了用户的输入。打开webug靶场,打开id为1的靶场后,如图:这里我们使用工具的是Hackbar2.1.3版本,这个是免费的。首先,我们需要判断是否存在SQL注入。在参数后面多加一个单引号,然后将它上传,发现报错了通过页面的返回结果,_webug靶场

AD辅域控制器升级为主域控制器(图形界面操作)_升级域控-程序员宅基地

文章浏览阅读3.5w次,点赞23次,收藏139次。环境介绍Active Directory域控制器已经搭建好主域控和辅域控,主域控故障,手动升级辅域控为主。主域控:2012DC1,ip:192.168.15.1辅域控:2012DC2,ip:192.168.15.21、升级前分别在2012dc1和2012dc2上查询fsmo的归属2、在2012dc1上,打开AD用户和计算机,更改连接的域控制器3、选择20..._升级域控

(一)Spring-Cloud源码分析之核心流程关系及springcloud与springboot包区别(新)_spring cloud 源码-程序员宅基地

文章浏览阅读1k次,点赞3次,收藏4次。很多人搞不懂springboot和spring-cloud的关系到底是什么,也不知道这两者时间有什么区别,今天简单的聊聊。2022年发了一篇Springcloud和Springboot的区别对比,但后面回看总觉得少了点东西,这次重新发个补充一下。_spring cloud 源码

推荐文章

热门文章

相关标签