数据结构进阶 哈希表

news2024/11/16 6:26:54

作者:@小萌新
专栏:@数据结构进阶
作者简介:大二学生 希望能和大家一起进步!
本篇博客简介:模拟实现高阶数据结构 哈希表

哈希表 哈希桶

  • 哈希概念
    • 举例
  • 哈希冲突
  • 哈希函数
  • 哈希冲突的解决方式之一
    • 闭散列 --开放定址法
  • 哈希表的闭散列实现
    • 哈希表的结构
    • 哈希表的插入
    • 哈希表的查找
    • 哈希表的删除

哈希概念

顺序结构和平衡树中 元素关键码和它的储存位置之间没有对应的关系

因此 在我们查找一个元素时 必须要经过关键码的多次比较

搜索的效率取决于搜索过程中的比较次数

  • 在顺序结构中 这个效率是N
  • 在平衡树结构中 这个效率是Log(N)

对于我们来说 最理想的搜索方法是经过常数次比较 也就是在时间复杂度O(1)的情况下找到元素

为了达到我们上面的效果 我们可以创造出一种结构 该结构通过某种函数让元素的储存位置和关键码之间建立一一映射的关系

向该结构插入和搜索元素的时候遵循以下规则

  • 插入元素: 根据待插入元素的关键码 经过计算找到储存位置 并将元素储存到该位置
  • 搜索元素: 对元素的关键码进行相同的计算找到理论储存位置 并与该位置的元素进行比较 若相同则搜索成功

这种构造方式即为哈希(散列)方法

哈希方法中使用的转换函数叫做哈希(散列)函数

构造出来的表被称为哈希表(散列表)

举例

例如 当集合为{1, 7, 6, 4, 5, 9}

我们将哈希函数设置为 hash(key) = key % capacity

其中capacity为储存元素空间的总大小

若我们将该集合储存在capacity大小为10的哈希表中 各元素的储存位置对应如下

在这里插入图片描述

使用该方法进行存储 则只需要通过哈希函数判断对应位置是否存放着待查找元素

而不必进行多次关键码的比较 因而效率较高

哈希冲突

不同的关键字通过相同的哈希函数计算出相同的哈希地址 这种情况叫做哈希冲突或者叫哈希碰撞

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

比如说在上面的例子中如果我们再插入一个数据11 就会引起哈希冲突

hash(11) = 11 % 10 = 1

在这里插入图片描述

哈希函数

引起哈希冲突的一个原因可能是哈希函数设计的不够合理

这里给出哈希函数设计的三条原则

  1. 哈希函数的定义域必须要包括需要储存元素的所有关键码 若散列表允许有m个地址 其值域必须要在0~m-1之间
  2. 哈希函数计算出来的地址要尽可能均匀的分布在整个空间中
  3. 哈希函数应该比较简单

这里给出两个常用的哈希函数

一 直接定址法

取关键字的某个线性函数的哈希地址为 : Hash(key) = A * key + B

优点 每个值都有一个唯一的对应位置 效率很高 一次就能找到

缺点 使用场景比较局限 只能是整数且数据范围比较集中

二 除留余数法

假设散列表中的允许的存在的地址数为m 则取一个不大于m但是接近于m的质数p作为除数

按照哈希函数 Hash(key) = key % p 将关键码转化为哈希地址

优点: 使用场景广泛 不受限制
缺点: 存在哈希冲突 哈希冲突越多 效率下降越厉害

哈希冲突的解决方式之一

闭散列 --开放定址法

闭散列 也叫开放定址法 当哈希发生冲突的时候 如果哈希表未被装满 则表示一定有剩余的位置 那么可以把冲突的元素存放到下一个位置去

寻找下一个位置的方式有很多种 还是一样 我们介绍比较常见的两种

一 线性探测

当哈希冲突发生时候 从发生冲突位置开始 依次向后探测 直到找到下一个空位置为止

例如我们上面的例子

这里的11就找到了下一个空的位置 也就是1后面的一个位置
在这里插入图片描述
当然随着我们插入数字的增多 哈希冲突的概率必然会增加

为了解决这个问题 我们引入一个叫做负载因子的概念

负载因子 = 表中有效数据的个数 / 空间的大小

  • 负载因子越大 产生冲突的概率越高 增删查改的效率越低
  • 负载因子越小 产生冲突的概率越低 增删查改的效率越高

但是当负载因子越小 实际上也说明了空间利用率越低 因此大量的空间实际上都被浪费掉了

对于闭散列来说 负载因子是十分重要的一个参数 一般控制在0.7~0.8之间

负载因子高于0.8则会导致在查表示缓存不命中按照指数上升(因为多次哈希冲突)

所以一些采用开放定址法的hash库 比如说java的系统库 限制了负载因子为0.75

如果超过0.75则会对哈希表进行增容

  • 线性探测的优点: 实现非常的简单
  • 线性探测的缺点: 一旦发生冲突 所有的冲突连在一起 容易产生数据的堆积 即不同的关键码占据了可利用的空位置 使得寻找某关键码的位置需要比较多次 导致搜索效率变低

二 二次探测

线性探测的缺点是产生冲突的数据堆积在一起 这个缺点和它总是找下一个空位置有关系

为了解决这个缺陷我们发明了二次探测

我们使用下面的哈希函数

Hi=(H0+i^2)%m ( i = 1 , 2 , 3 , . . . ) (i=1,2,3,…)

这样子即使发生哈希冲突了 数据寻找下一个位置的时候也不会那么集中

能够很有效的缓解堆积的问题

和线性探测一样 二次探测也需要关注负载因子

因此 闭散列最大的缺陷就是空间利用率不足 这也是哈希的缺陷

哈希表的闭散列实现

哈希表的结构

在闭散列的哈希表中 哈希表的每个位置除了储存所给数据之外 还需要存储当前位置的状态

哈希表的每个位置可能有以下三种状态

  1. EMPTY(空)
  2. EXIST(存在)
  3. DELETE(存在过数据 但是被删除了)

我们可以使用枚举来定义这三种状态

// 标记每个位置的状态
enum State
{
	EMPTY,
	EXIST,
	DELETE
};

这里抛出一个问题

我们为什么需要标记哈希表每个位置的状态呢?

我们首先来看一个场景 以除留余数法的线性探测为例 假设一个哈希表的大小是10

我们现在要查找一个元素40 步骤如下

  1. 通过除留余数法求得40在哈希表中的映射是0
  2. 从0下标开始往后找 若找到40则存在

在这里插入图片描述

但是我们寻找40的时候不可能将整个哈希表遍历一遍 这样子就失去了哈希表的意义

那么什么时候停止呢?

我们说 只要找到一个空位置就停止

为什么 因为根据线性探测的规则 我们从计算出的位置开始往后找 只要找到之后就会插入

所以说如果到空位置了还没找到就一定是不存在这个元素

但是到这里就会引发两个新问题

  1. 我们如何标志一个位置是存在还是不存在呢? 使用数据0来标志吗? 可是假如这个位置本来存储的数据就是0呢?
  2. 假如查找的时候中间有一个位置被删除了 那么这个位置是空吗? 如果是空 我们是不是就找不到后面的数据了

所以说 我们针对这几个问题设计了哈希表数据的三种状态 存在 空 和删除

使用这三种状态就能解决上面的所有问题

我们这里先给出哈希数据的结构

template<class K,class V>
struct HashData
{
	pair<K, V> _kv;
	State _state = EMPTY; // 状态默认设置为空
};

为了能够更好的计算负载因子 我们还应该储存整个哈希表的有效元素

template<class K, class V>
class HashTable
{
public:
	// ..
private:
	vector<HashData<K, V>> _table; //哈希表
	size_t _n = 0; // 哈希表中有效元素个数
};

哈希表的插入

向哈希表中插入数据的步骤如下

  1. 查看哈希表中是否存在该键值对 若存在则插入失败
  2. 判断是否需要调整哈希表的大小
  3. 将键值对插入哈希表
  4. 调整哈希表的大小

其中哈希表的调整方式如下

  • 如果哈希表的大小为0 则扩容至10
  • 如果哈希表的负载因子大于0.7 则先创建一个新哈希表 新的哈希表大小为原来的两倍 之后遍历原哈希表 将其中的所有元素放到新哈希表中 最后将原哈希表和新哈希表交换即可

将键值对插入哈希表的方式如下

  1. 通过哈希函数计算出对应的哈希地址
  2. 若产生哈希冲突 则从哈希地址处开始 采用线性探测向后寻找一个状态为EMPTY或DELETE的位置
  3. 将键值对插入到该位置 并将该位置的状态设置为EXIST
	bool Insert(const pair<K, V>& kv)
	{
		// 1. 查看哈希表中是否存在该键值的键值对 
		HashData<K, V>* ret = Find(kv.first);
		if (ret)
		{
			return false; // 如果存在则表示插入失败
		}

		// 2. 判断是否需要调整哈希表的大小
		if (_table.size() == 0)
		{
			_table.resize(10);
		}
		else if ((double)_n / (double)_table.size() > 0.7) // 这里涉及到小数 有两种方式 一种是扩大十倍 一种是转换double 
		{
			// 扩容
			HashTable<K, V> newHT;
			newHT._table.resize(2 * _table.size());
			// 将原哈希表中的数据插入到新哈希表中
			for (auto& e : _table)
			{
				if (e._state == EXIST)
				{
					newHT.Insert(e._kv);
				}
			}
			// 交换这两个哈希表
			_table.swap(newHT._table);
		}


		// 将键值对插入哈希表
		size_t start = kv.first % _table.size();
		size_t index = start;
		size_t i = 1;
		// 找到一个EMPTY或者是DELETE的位置
		while (_table[index]._state == EXIST)
		{
			index = start + i;
			index% _table.size(); // 防止下标超出哈希范围
			i++;
		}
		_table[index]._kv = kv;
		_table[index]._state = EXIST;

		_n++;
		return true;	
	}

哈希表的查找

哈希表的查找分为以下几步

  1. 先判断哈希表中的大小是否为0 为0则查找失败
  2. 通过哈希函数计算出一个哈希地址
  3. 在这个地址的基础上一直往后找直到找到待查找的值或者空

代码表示如下

	HashData<K, V>* Find(const K& key)
	{
		if (_table.size() == 0)
		{
			return nullptr;
		}

		size_t start = key % _table.size();
		size_t index = start;
		size_t i = 1;
		while (_table[index]._state != EMPTY)
		{
			if (_table[index]._state == EXIST && _table[index]._kv.first == key)
			{
				return &_table[index];
			}

			index = start + i;
			index% _table.size(); // 防止下标超出哈希范围
			i++;

		}



		return nullptr;
	}

哈希表的删除

哈希表的删除就十分简单了

我们只需要找到删除元素 然后将改数据的状态改为DELETE即可

步骤如下

  1. 查看哈希表中是否有该键值对存在 若不存在则返回失败
  2. 若存在 则将该键值对所在位置的状态改为DELETE即可
  3. 之后别忘记将有效元素数据减一

代码表示如下

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

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

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

相关文章

Python CalmAn(Calcium Imaging Analysis)神经生物学工具包安装及环境配置过程

文章目录CalmAn简介安装要求我的设备1>CalmAn压缩包解压&#xff08;caiman文件夹要改名&#xff09;2>conda创建虚拟环境3>requirements依赖包配置&#xff08;包括tensorflow&#xff09;4>caiman安装(mamba install)5>caimanmanager.py install6>PyCharm添…

51单片机独立按键

文章目录前言一、按键原理图二、代码编写三、模块化管理按键总结前言 本篇文章将带大家学习独立按键按键的基本操作。 独立按键式直接用I/O口线构成的单个按键电路&#xff0c;其特点是每个按键单独占用一根I/O口线&#xff0c;每个按键的工作不会影响其他I/O口线的状态。 一…

MongoDB学习笔记【part5】基于 MongoRepository 开发CURD

一、MongoRepository Spring Data 提供了对 mongodb 数据访问的支持&#xff0c;只需继承 MongoRepository 类&#xff0c;并按照 Spring Data 规范就可以实现对 mongodb 的操作。 SpringData 方法定义规范&#xff1a; 注意事项&#xff1a; 不能随便声明&#xff0c;必须要…

汇编语言学习笔记 下

本文承接汇编语言学习笔记 上 上篇文章记录了汇编语言寄存器&#xff0c;汇编语言基本组成部分&#xff0c;数据传送指令&#xff0c;寻址指令&#xff0c;加减法指令&#xff0c;堆栈&#xff0c;过程&#xff0c;条件处理&#xff0c;整数运算的内容 高级过程 大多数现代编程…

day24-网络编程02

1.NIO 1.1 NIO通道客户端【应用】 客户端实现步骤 打开通道指定IP和端口号写出数据释放资源 示例代码 public class NIOClient {public static void main(String[] args) throws IOException {//1.打开通道SocketChannel socketChannel SocketChannel.open();//2.指定IP和端…

你是真的“C”——2023年除夕夜 牛客网刷题经验分享~

2023年除夕夜 牛客网刷题经验分享~&#x1f60e;前言&#x1f64c;BC89 包含数字9的数 &#x1f60a;BC90 小乐乐算多少人被请家长 &#x1f60a;BC91 水仙花数 &#x1f60a;BC92 变种水仙花 &#x1f60a;BC93 公务员面试 &#x1f60a;总结撒花&#x1f49e;&#x1f60e;博…

Android Studio 支持安卓手机投屏

有时当我们在线上做技术分享或者功能演示时&#xff0c;希望共享连接中的手机屏幕&#xff0c;此时我们会求助 ApowerMirror&#xff0c;LetsView&#xff0c;Vysor&#xff0c;Scrcpy 等工具。如果你是一个 Android Developer&#xff0c;那么现在你有了更好的选择。 Android…

Day59| 503. 下一个更大元素 II | 42. 接雨水 --三种方法:1.双指针 2.动态规划 3.单调栈

503. 下一个更大元素 II注意点&#xff1a;初始化了2倍的题目中的nums.size()&#xff0c;最后直接/2即可分清逻辑nums[i] > nums[st.top()]的时候&#xff0c;才进行st.pop()操作class Solution { public:vector<int> nextGreaterElements(vector<int>& nu…

STM32——独立看门狗

目录 看门狗产生背景 看门狗的作用 STM32 独立看门狗 独立看门狗特点 &#xff08;IWDG&#xff09; 独立看门狗常用寄存器 独立看门狗超时时间计算 独立看门狗工作原理 独立看门狗操作库函数 看门狗产生背景 当单片机的工作受到来自外界电磁场的干扰&#xff0c;造…

bert-bilstm-crf提升NER模型效果的方法

1.统一训练监控指标和评估指标评估一个模型的最佳指标是在实体级别计算它的F1值&#xff0c;而不是token级别计算它的的准确率&#xff09;。自定义一个f1值的训练监控指标传给回调函数PreliminaryTP&#xff1a;实际为P&#xff0c;预测为PTN&#xff1a;实际为N&#xff0c;预…

【Java IO流】缓冲流及原理详解

文章目录前言字节缓冲流原理字符缓冲流Java编程基础教程系列前言 前面我们已经学习了四种对文件数据操作的基本流&#xff0c;字节输入流&#xff0c;字节输出流&#xff0c;字符输入流&#xff0c;字符输出流。为了提高其数据的读写效率&#xff0c;Java中又定义了四种缓冲流…

LwIP系列--内存管理(堆内存)详解

一、目的小型嵌入式系统中的内存资源&#xff08;SRAM&#xff09;一般都比较有限&#xff0c;LwIP的运行平台一般都是资源受限的MCU。基于此为了能够更加高效的运行&#xff0c;LwIP设计了基于内存池、内存堆的内存管理以及在处理数据包时的pbuf数据结构。本篇的主要目的是介绍…

Ubuntu22.10安装和配置R/FSL/Freesurfer

2018年购入的台式机&#xff0c;一直处于吃灰状态&#xff0c;决定安装Ubuntu方便学习和使用一些基于Linux系统的软件。本文记录相关软件的安装和配置。>安装Ubuntu22.10<到官网下载ISO文件。将一个闲置U盘通过Rufus制作为安装盘。重启按F11设置引导盘&#xff0c;选择U盘…

国家实用新型发明专利:一种机器视觉的流水线智能检测报警系统

国家实用新型发明专利&#xff1a;一种机器视觉的流水线智能检测报警系统 【系统装置设计图】 文章目录国家实用新型发明专利&#xff1a;一种机器视觉的流水线智能检测报警系统【说明书摘要】【权利要求书】【说明书】***技术领域******背景技术******发明内容******有益效果…

C++类和对象(上): 封装与this指针

目录 一.前言 二. 类的引入和定义 1.C和C结构体的区别 2.C类的定义 3.类的成员方法的声明和定义是可分离的 三.面向对象之封装特性 1.封装思想的介绍 2.类封装编程模式的优点 四. 类实例(对象)的内存模型 五.this指针 章节导图&#xff1a; 一.前言 面向过程和面向对…

分享167个PHP源码,总有一款适合您

PHP源码 分享167个PHP源码&#xff0c;总有一款适合您 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&#xff0c; 167个PHP源码下载链接&#xff1a;https://pan.baidu.com/s/1fzoQ4_4VXc1e1ZHOUKuhbQ?pwdsb6s 提取码&#x…

数学表达式的处理

概述 在OJ上 会遇到一些这样的题目&#xff1a; 小明同学写数学四则运算&#xff0c;有把括号写多、写少、写错的情况&#xff0c;比如&#xff08;AB)*(C-D &#xff0c;请你输入一个表达式&#xff0c;判断此表达式的括号是否正确(不考虑运算的结果正确性)。 每次我看到 &q…

【操作系统】—— Windows压缩工具 “ Bandizip与7-zip ”(带你快速了解)

&#x1f4dc; “作者 久绊A” 专注记录自己所整理的Java、web、sql等&#xff0c;IT技术干货、学习经验、面试资料、刷题记录&#xff0c;以及遇到的问题和解决方案&#xff0c;记录自己成长的点滴。 &#x1f341; 操作系统【带你快速了解】对于电脑来说&#xff0c;如果说…

Python OpenCV 图片滑块验证码 滑块图片验证码 自动识别方案 模板匹配识别 识别成功率调试 源码分析 通用解决方案

前言 通过本专栏前面两篇文章大家已对图片滑块验证码有了初步的了解,对于滑块验证的实现和校验原理有了一定的了解,通过由浅入深的实战案例可直接应用于实战,对于滑块如何在前端实现滑动或接口调用可自行查阅相关资料实现,本文主要讲解 滑块验证码 模板匹配 识别的通用解决…

史上最详细的AVL树的实现(万字+动图讲解旋转)

&#x1f525;&#x1f525; 欢迎来到小林的博客&#xff01;&#xff01;       &#x1f6f0;️博客主页&#xff1a;✈️小林爱敲代码       &#x1f6f0;️文章专栏&#xff1a;✈️小林的C之路       &#x1f6f0;️欢迎关注&#xff1a;&#x1f44d…