哈希闭散列的实现与机制

目录

哈希的介绍

哈希冲突

原因

影响

解决方法

实例

哈希函数

哈希函数设计原则:

常见哈希函数

闭散列

线性探测的实现

代码解读

1. 命名空间和枚举定义

2. 哈希表节点结构体

3. 哈希函数模板

4. 哈希表类

5. 插入、查找和删除逻辑

二次探测


哈希的介绍

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

O(logN),搜索的效率取决于搜索过程中元素的比较次数。

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

这就是哈希思想的体现。

当向该结构中:
插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称
为哈希表(Hash Table)(或者称散列表)
例如:数据集合{1,7,6,4,5,9};
哈希函数设置为:hash(key) = key % capacity; capacity为存储元素底层空间总的大小

哈希冲突

哈希冲突(哈希碰撞)是指在使用哈希表(或哈希函数)的过程中,两个或多个不同的输入值(键)通过哈希函数映射到同一个输出值(哈希值)的情况。这是哈希表实现中的一个基本问题,因为理想的哈希函数应该能够为每个可能的键生成唯一的哈希值,但实际上这是不可能的,因为键的空间通常远大于哈希值的范围。我们把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”

以下是哈希冲突的几个关键点:

原因

  1. 有限的范围:哈希函数通常将输入映射到一个有限的整数范围,而可能的输入(键)的数量是无限的,这导致必然会有多个输入映射到同一个输出。

  2. 哈希函数设计:如果哈希函数设计不当,可能会增加冲突的概率。一个好的哈希函数应该尽可能均匀地分布键。

影响

  1. 性能下降:哈希冲突会导致哈希表的性能下降,因为需要额外的步骤来解决冲突,这可能会增加查找、插入和删除操作的时间复杂度。

  2. 数据结构复杂化:为了处理冲突,哈希表通常需要额外的数据结构和算法,如链表法(separate chaining)或开放寻址法(open addressing)。

解决方法

  1. 链表法:每个哈希桶(bucket)维护一个链表,所有映射到同一个哈希值的键都存储在这个链表中。当发生冲突时,只需将新键插入到对应链表中。

  2. 开放寻址法:当发生冲突时,哈希表会寻找下一个空闲的槽位来存储冲突的键。这可以通过线性探测(linear probing)、二次探测(quadratic probing)或双重哈希(double hashing)等方法实现。

  3. 再哈希:当哈希表中的元素太多,导致冲突率上升时,可以通过增加哈希表的大小并重新计算所有元素的哈希值来减少冲突。

  4. 更好的哈希函数:设计或选择能够更均匀分布键的哈希函数,可以减少冲突的概率。

实例

假设有一个简单的哈希函数 h(k) = k % m,其中 k 是键,m 是哈希表的大小。如果 m = 10,那么键 15 和 25 都会映射到同一个哈希值 5,因为 15 % 10 = 5 和 25 % 10 = 5

哈希冲突是哈希表实现中不可避免的问题,但通过合理的设计和策略,可以有效地管理和减少它们的影响。

哈希函数

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

哈希函数设计原则

哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有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种不同的符号在各位上出现的频率不一定
相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只
有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散
列地址

注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突
解决哈希冲突两种常见的方法是:闭散列开散列

 本文着重介绍闭散列的实现与机制。

闭散列

闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有
空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置
呢?
1. 线性探测
比如2.1中的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4,
因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
插入 通过哈希函数获取待插入元素在哈希表中的位置 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突, 使用线性探测找到下一个空位置,插入新元素
删除
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影响。因此线性探测采用标记的伪删除法来删除一个元素

线性探测的实现

#pragma once
#include <utility>
#include <string>
#include <vector>using namespace std;
namespace open_address	//闭散列:开放寻址法
{enum Status{DELETE,EMPTY,EXIST};template<class K, class V>struct HashDate{pair<K, V> _data;Status _status;};//可以进行int类型的强转template<class K>struct HashFunc{size_t operator()(const K& key)		//传入键值,返回哈希值(映射到哈希表的位置)	{return size_t(key);}};//string类型template<>		//模板特化,需要保留<>struct HashFunc<string>	//特化成<string>{size_t operator()(const string& key)		//传入键值,返回哈希值(映射到哈希表的位置){// BKDRsize_t hash = 0;for (auto& e : key){hash *= 31;hash += e;}return hash;}};template<class K, class V, class HashFunc = HashFunc<K>>class HashTable{public:HashTable(){_t.resize(10);		//初始分配10个HashDate的空间_n = 0;}bool Insert(const pair<K, V>& kv){if (Find(kv.first))return false;if (n * 10 / _t.size() >= 7)	//装载因子大于70%,扩容{//开一个新表,重新映射HashTable<K, V> newtable;size_t newsize = _t.size() * 2;	newtable._t.resize(newsize);//遍历旧表,重新映射for (size_t i = 0; i < _t.size(); i++){if (_t[i]._status == EXIST){newtable.Insert(_t[i]._data);		//调用newtable的Insert}}_t.swap(newtable._t);		//交换表}		//先执行扩容,再执行插入,所以不需要++_nHashFunc hf;size_t hashi = hf(kv.first) %  _t.size();	//除留余数法求哈希值。/*要保证哈希值在0~_t.size()-1之间,即需要保证哈希值在可用空间范围之内。如果%capacity,可能导致得到的哈希值大于哈希表的最大长度?*/while (_t[hashi]._status == EXIST)		//出现碰撞,进行探测{hashi++;hashi %= _t.size();		//循环到头,重新开始,避免无限循环、越界}_t[hashi]._data = data;_t[hashi]._status = EXIST;_n++;		//键值个数+1return true;}HashDate<K, V>* Find(const K& key)	//1.要么直接找到2.要么出现冲突,再empty之前探测{HashFunc hf;size_t hashi = hf(key) % _t.size();while (_t[hashi]._status != EMPTY){if (_t[hashi]._data.first == key && _t[hashi]._status == EXIST)	//防止伪删除,不能进入DELETE状态查找return &_t[hashi];hashi++;hashi %= _t.size();		//循环到头,重新开始,避免无限循环、越界}return nullptr;}//伪删除,只是标记状态为DELETE,不释放空间bool Erase(const K& key){HashDate<K, V>* ret = Find(key);if (ret)	//存在(找不到返回空){ret->_status = DELETE;_n--;		//键值个数-1(删除需要--_n)return true;}elsereturn false;}private:vector<HashDate<K, V>> _t;	//闭散列,存储数据size_t _n;	//键值的个数};}

代码解读

这段代码实现了一个基于开放寻址法的哈希表,以下是对代码的详细解读:

1. 命名空间和枚举定义

namespace open_address {enum Status {DELETE,EMPTY,EXIST};
  • open_address 命名空间用于包含所有与开放寻址法哈希表相关的类和函数。
  • Status 枚举用于表示哈希表中每个槽的状态,包括已删除(DELETE)、空(EMPTY)和存在(EXIST)。

2. 哈希表节点结构体

template<class K, class V>
struct HashDate {pair<K, V> _data;Status _status;
};
  • HashDate 是一个模板结构体,用于表示哈希表中的一个节点,包含一个键值对 _data 和一个状态 _status

3. 哈希函数模板

template<class K>
struct HashFunc {size_t operator()(const K& key) {return size_t(key);}
};template<>
struct HashFunc<string> {size_t operator()(const string& key) {size_t hash = 0;for (auto& e : key) {hash *= 31;hash += e;}return hash;};
};
  • HashFunc 是一个模板结构体,用于生成哈希值。它对 K 类型进行特化,默认实现是将键值转换为 size_t
  • 对于 string 类型,HashFunc 进行了特化,使用 BKDR 哈希算法来生成哈希值。

4. 哈希表类

template<class K, class V, class HashFunc = HashFunc<K>>
class HashTable {
public:HashTable() {_t.resize(10);_n = 0;}bool Insert(const pair<K, V>& kv) {// ...(插入逻辑)}HashDate<K, V>* Find(const K& key) {// ...(查找逻辑)}bool Erase(const K& key) {// ...(删除逻辑)}private:vector<HashDate<K, V>> _t;size_t _n;
};
  • HashTable 是一个模板类,用于实现基于开放寻址法的哈希表。
  • 构造函数初始化一个大小为10的 vector 来存储哈希表节点,并设置键值对数量 _n 为0。
  • Insert 方法用于插入键值对,如果装载因子超过70%,则进行扩容。
  • Find 方法用于查找键值对,如果找到则返回指针,否则返回 nullptr
  • Erase 方法用于删除键值对,实际上是进行伪删除,即将状态设置为 DELETE
  • 私有成员 _t 是一个 vector,用于存储哈希表节点,_n 用于记录当前键值对的数量。

5. 插入、查找和删除逻辑

  • 插入逻辑中,如果找到相同的键,则返回 false。如果装载因子超过70%,则进行扩容,然后使用哈希函数找到空槽插入新节点。
  • 查找逻辑中,使用哈希函数找到键对应的槽,然后进行线性探测直到找到空槽或匹配的键。
  • 删除逻辑中,使用查找逻辑找到节点,然后将其状态设置为 DELETE 并减少键值对数量。

这段代码是一个简单的哈希表实现,它通过开放寻址法解决了哈希冲突,并且提供了基本的插入、查找和删除操作。

线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同
关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降

二次探测

线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题

研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任
何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在
搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出
必须考虑增容。
因此:比散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。

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

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

相关文章

【React】事件机制

事件机制 react 基于浏览器的事件机制自身实现了一套事件机制&#xff0c;称为合成事件。比如&#xff1a;onclick -> onClick 获取原生事件&#xff1a;e.nativeEvent onClick 并不会将事件代理函数绑定到真实的 DOM节点上&#xff0c;而是将所有的事件绑定到结构的最外层…

latex本地运行(MiKTeX+VScode)-20241006

1、安装 LaTex 主流的分发版本应该就是 TeXLive 和 MikTeX 了,这里使用 MikTex(只有几百M)—— TeXLive 太大了、默认安装全部包,可选自选部分安装单实在有些许麻烦,MikTeX 则方便得多,需要的时候可以自动安装全部包 点击跳转到 MiKTeX 官网,直接下载即可:不用担心什…

体系结构论文(五十五):Full Stack Optimization of Transformer Inference

Full Stack Optimization of Transformer Inference 一、文章介绍 背景 Transformer模型被广泛应用于各种任务&#xff0c;如计算机视觉、自然语言处理、语音识别等&#xff0c;原因是它们的准确度很高。然而&#xff0c;这些模型的复杂性和规模不断增加&#xff0c;导致它们…

连续时间傅里叶变换

一、非周期信号的表示&#xff1a;连续时间傅里叶变换 傅里叶变换对&#xff1a; 通常称为的频谱 二、傅里叶变换的收敛 1、绝对可积 2、在任何有限区间内&#xff0c;只有有限个最大值和最小值 3、在任何有限区间内&#xff0c;有有限个不连续点&#xff0c;且在每个不连…

信息安全工程师(36)访问控制主要产品与技术指标

前言 访问控制是确保系统资源安全的重要手段&#xff0c;其主要产品和技术指标对于理解和实施有效的访问控制策略至关重要。 一、访问控制主要产品 访问控制产品种类繁多&#xff0c;根据应用场景和需求的不同&#xff0c;可以分为以下几类&#xff1a; 防火墙&#xff1a; 功能…

Linux环境搭建(云服务器)

前言 Linux是一款由林纳斯托瓦兹开源的操作系统&#xff0c;时至今日拥有着丰富的讨论资源和系统完整性&#xff0c;基本普及于市场中的公司内部&#xff0c;所以有着很大的学习价值。学习Linux主要分为两大部分&#xff0c;一是学习Linux的系统操作&#xff0c;包括且不限于掌…

codetop标签树刷题(二)!!暴打面试官!!!!

个人复习用 1.二叉搜索树中第k小的元素2.删除给定值的叶子节点3.把二叉搜索树转换为累加树4.合并二叉树5.翻转二叉树6.二叉树中所有距离为k的节点7.路径总和II8.寻找重复的子树9.二叉树的序列化和反序列化 1.二叉搜索树中第k小的元素 给定二叉搜索树的根节点root&#xff0c;和…

【一起学NLP】Chapter3-使用神经网络解决问题

目录 使用神经网络解决问题Tip:数据集划分学习使用的代码Tip:epochTip:数据打乱Trainer类Tip-高速化计算 使用神经网络解决问题 import sys sys.path.append(..) # 为了引入父目录的文件而进行的设定 from dataset import spiral import matplotlib.pyplot as pltx,t spiral.…

【Spring】“请求“ 之传递单个参数、传递多个参数和传递对象

文章目录 请求1. 传递单个参数注意事项1 . **正常传递参数**2 . **不传递 age 参数**3 . **传递参数类型不匹配** 2. 传递多个参数3. 传递对象 请求 访问不同的路径&#xff0c;就是发送不同的请求。在发送请求时&#xff0c;可能会带一些参数&#xff0c;所以学习 Spring 的请…

传奇GOM引擎架设好进游戏后提示请关闭非法外挂,重新登录,如何处理?

今天在架设一个GOM引擎的版本时&#xff0c;进游戏之后刚开始是弹出一个对话框&#xff0c;提示请关闭非法外挂&#xff0c;重新登录&#xff0c;我用的是绿盟登陆器&#xff0c;同时用的也是绿盟插件&#xff0c;刚开始我以为是绿盟登录器的问题&#xff0c;于是就换成原版gom…

如何构建LSTM神经网络模型

一、了解LSTM 1. 核心思想 首先&#xff0c;LSTM 是 RNN&#xff08;循环神经网络&#xff09;的变体。它通过引入细胞状态 C(t) 贯穿于整个网络模型&#xff0c;达到长久记忆的效果&#xff0c;进而解决了 RNN 的长期依赖问题。 2. 思维导图 每个LSTM层次都有三个重要的门结构…

VMware ESXi更改https的TLS协议版本

简要概述 TLS 1.0 和 1.1 是已弃用的协议&#xff0c;具有广为人知的缺点和漏洞。应在所有接口上启用 TLS 1.2&#xff0c;并在支持的情况下禁用 SSLv3、TL 1.1 和 1.0。强制要求 TLS 1.2 可能会破坏 vSphere 的第三方集成和加载项。在实施 TLS 1.2 后仔细测试这些集成&#x…

maven指定模块快速打包idea插件Quick Maven Package

问题背景描述 在实际开发项目中&#xff0c;我们的maven项目结构可能不是单一maven项目结构&#xff0c;项目一般会用parent方式将各个项目进行规范&#xff1b; 随着组件的数量增加&#xff0c;就会引入一个问题&#xff1a;我们只想打包某一个修改后的组件A时就变得很不方便…

C++ 算法学习——1.8 悬线法

1.问题引入&#xff1a;对于一个矩形图&#xff0c;图中放置着不少障碍&#xff0c;要求出最大的不含障碍的矩形。 2.分析&#xff1a;显然一个极大矩形是左右上下都被障碍挡住&#xff0c;无法再扩大的矩形&#xff0c;此时障碍也包括边界。 3.方法&#xff1a;悬线法考虑以…

01 从0开始搭建django环境

1 安装相关版本的django&#xff0c;这里&#xff0c;我以5.1.1为例子 pip3 install django5.1.1 (.venv) D:\DjangoCode\MS>pip3 install django5.1.1 Looking in indexes: https://pypi.tuna.tsinghua.edu.cn/simple Collecting django5.1.1Using cached https://pypi.t…

STM32定时器(TIM)

目录 一、概述 二、定时器的类型 三、时序 四、定时器中断基本结构 五、定时器定时中断代码 六、定时器外部时钟代码 一、概述 TIM(Timer)定时器 定时器可以对输入的时钟进行计数&#xff0c;并在计数值达到设定值时触发中断16位计数器、预分频器、自动重装寄存器的时基…

TM1618数码管控制芯片使用共阳极数码管过程中的问题和解决办法

控制芯片的基本了解 相比于不用控制芯片的电路&#xff1a;这里带2根电源线和3个信号线&#xff0c;共使用了5根线&#xff0c;但可以控制4个8段数码管显示。若是电路直接控制4个8段数码管需要84113个接口&#xff0c;这对于MCU的珍贵引脚简直是浪费。 这里不会出现余晖效应也…

Python编程常用的35个经典案例

Python 的简洁和强大使其成为许多开发者的首选语言。本文将介绍35个常用的Python经典代码案例。这些示例覆盖了基础语法、常见任务、以及一些高级功能。 1.列表推导式 这个例子展示了列表推导式&#xff0c;用于生成FizzBuzz序列。 fizz_buzz_list ["FizzBuzz" i…

ultralytics yolo pose 示例:加载官方pose模型进行推理

Ultralytics YOLO 是计算机视觉和 ML 领域专业人士的高效工具。 安装 ultralytics 库&#xff1a; pip install ultralytics 官方YoLo Pose 模型列表信息&#xff1a; 实现代码如下&#xff1a; from ultralytics import YOLO import cv2 # Load a model ckpt_dir "…

HTB:Ignition[WriteUP]

目录 连接至HTB服务器并启动靶机 1.Which service version is found to be running on port 80? 2.What is the 3-digit HTTP status code returned when you visit http://{machine IP}/? 3.What is the virtual host name the webpage expects to be accessed by? 4.…