C++数据结构——哈希表

news2024/11/17 23:34:06

前言:本篇文章将继续进行C++数据结构的讲解——哈希表。


目录

一.哈希表概念

二.哈希函数

1.除留取余法

三.哈希冲突

1.闭散列

线性探测

 (1)插入

(2)删除

 2. 开散列

开散列概念

四.闭散列哈希表

1.基本框架 

2.插入

3.寻找

4.删除

5.数据类型问题

五.开散列哈希表

1.基本框架

2.插入

3.寻找

4.删除

总结


一.哈希表概念

顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(log_2 N),搜索的效率取决于搜索过程中元素的比较次数
理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素
如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。

比如说:要统计一个全是小写英文字母的字符串中各个英文字母出现的个数,该怎么做???

很容易,因为小写英文字母有26个,所以我们直接创建一个大小为26的int数组并将值全部初始化为0让数组的每一位都代表一个小写英文字母该字母每出现一次,就让该怎么对于位置的值++,最终每个位置的值便是对应小写英文字母的个数

该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表) 


二.哈希函数

有时候我们可能不能像上边一样有多少种数据就创建多大的哈希表,比如说虽然我们只有1,12,103,1004,10005四个数要统计个数,难道要创建一个10005大小的数组吗??? 


1.除留取余法

实际上只需要创建大小为6的数组即可,此时我们可以使用哈希函数:除留取余法

找到一个合适的除数,能够让上述数字取余之后分别为不同的值,从而拉进各个数字之间的距离,创建更小的哈希表

比如说让上述数字均取余上10,得到的就会是1,2,3,4,5,刚好对应数组各个位置


三.哈希冲突

虽然上述函数可以解决大部分问题,但不妨有些时候会出现像1,11,111,1111这样的数字,它们%10之后得到的数字均为1,这样就会导致不同的值映射到相同的位置,从而导致哈希冲突

 那么为了解决哈希冲突,有两种常用的方法:闭散列和开散列


1.闭散列

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


线性探测

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

 (1)插入

通过哈希函数获取待插入元素在哈希表中的位置

如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素

(2)删除

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

哈希表的每个空间都给上一个标记
EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除,通过枚举实现:
enum State{EMPTY, EXIST, DELETE};  


 2. 开散列

开散列概念

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

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。 由于其形状很像一个桶,所以开散列哈希表又叫哈希桶


四.闭散列哈希表

1.基本框架 

 下面来看代码,我们先给出哈希表的基本框架:

namespace close_address
{
    enum State
    {
    	EMPTY,
    	EXIST,
    	DELETE
    };
    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>> _tables;
    	size_t _n = 0;
    };
}

这里的_n用来记录哈希表中数据的个数。 


2.插入

	bool Insert(const pair<K, V> kv)
	{
		if (Find(kv.first))
			return false;
		size_t hashi = kv.first % _tables.size();
		//线性探测
		while (_tables[hashi]._state == EXIST)
		{
			++hashi;
			hashi %= _tables.size();
		}
		_tables[hashi]._kv = kv;
		_tables[hashi]._state = EXIST;
		++_n;
        return true;
	}

插入步骤还是容易实现的,值得注意的是,哈希表也不允许相同数据存在,所以需要提前判断,这里直接调用后边的Find()函数来判断

除此之外还有一个非常关键的点在于——扩容

 因为虽然我们的哈希表是用vector作为底层,但是实际上填入数据时并一定是挨着存放的,所以我们需要在插入之前,提前创造空间

 那么哈希表也要等数据存满的时候才扩容吗???并不是。

对于散列表,存在一个荷载因子的定义:

α = 填入表中的元素 / 散列表的长度

当表中的元素足够多但并未满时,此时如果继续插入数据,发生哈希冲突的可能性就会极大,导致插入时间变长。所以这里我们规定,当荷载因子的值 >= 0.7时就进行扩容

	HashTable()
	{
		_tables.resize(10);
	}

在扩容之前,应该给表一个初始的大小,这里通过构造函数使哈希表的初始长度为10

		//扩容
		if (_n * 10 / _tables.size() >= 7)
		{
			HashTable<K, V> newHT;
			newHT.tables.resize(_tables.size() * 2);
			for (size_t i = 0; i < _tables.size(); i++)
			{
				if (_tables[i]._state == EXIST)
				{
					newHT.Insert(_tables[i]._kv);
				}
			}
			_tables.swap(newHT._tables);
		}

随后进行扩容判断,这里有一个细节, 因为荷载因子是一个浮点数,如果直接进行比较,我们还需进行类型转换,所以我们直接让哈希表数据个数乘10再来进行计算。

判断成立则进行扩容,注意,哈希表的扩容并非像顺序表那样进行复制粘贴,因为哈希表扩容,代表着哈希函数的分母发生变化,所以其对应的取余后的下标位置也会发生变化

这里我们通过新建一个二倍大小的哈希表遍历原表数据并在新表中调用插入函数进行插入,最后在通过交换即可。


3.寻找

	//寻找
	HashData<K, V>* Find(const K& key)
	{
		size_t hashi = key % _tables.size();
		//线性探测
		while (_tables[hashi]._state == EXIST &&_tables[hashi]._state != EMPTY)
		{
			if (_tables[hashi]._kv.first == key)
			{
				return &_tables[hashi];
			}
			++hashi;
			hashi %= _tables.size();
		}
		return nullptr;
	}

寻找就比较简单了, 通过要寻找的值计算出其在哈希表中的下标判断是否存在且是否为空存在且不为空则进行判断,相等返回如果不相等,因为可能存在哈希冲突,所以循环往后遍历,直至遇到空位置仍然没有找到,说明其不在哈希表中,返回nullptr

这里有一个细节,返回值为哈希表中对应位置的地址,这是为后边的删除做伏笔。


4.删除

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

删除我们直接借用Find函数去找到对应位置,无需在通过哈希函数去寻找。

如果要删除的元素不存在,则返回失败,反之,将其位置的标记改为DELETE,即伪删除

下面来理解一下伪删除

因为我们在寻找和插入时的判断条件均为标记位EMPTY,所以删除时只需将该位置的标记改为DELETE,这样就不会影响该位置对应的冲突位的寻找以及新的插入了


5.数据类型问题

我们上述的哈希表实现能够采用哈希函数进行取余操作的前提是数据为int类型,那如果是其他类型的数据,不能进行取余操作,又该如何使用哈希函数呢???

这里的方法是,通过建立仿函数将其他类型转换成int类型

struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

 此外,有一个特例,string类型不能直接转换为int类型,所以想要满足string类型,我们需要为其单独创造一个仿函数

struct HashStringFunc
{
	size_t operator()(const string& s)
	{
		size_t hash = 0;
		for (auto e : s)
		{
			hash += e;
		}
		return hash;
	}
};

思想为,将string类型的每个字符的ascll码值加起来作为其int类型的数据

随后需要在模板中进行添加:

template<class K, class V,class Hash = HashFunc<K>>

这里采用了缺省参数默认情况下为其他类型,当为string类型时,在传入独有的模版。 


五.开散列哈希表

1.基本框架

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

		HashNode(const pair<K,V>& kv)
			:_kv(kv)
			,_next(nullptr)
		{}
	};
	template<class K,class V>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		HashTable()
		{
			_tables.resize(10, nullptr);
			_n = 0;
		}
	private:
		vector<Node*> _tables;//指针数组
		size_t _n;
	};
}

不同于闭散列的哈希表vector中存放的是数据本身,哈希桶中vector存放的为节点指针


2.插入

因为哈希桶可能会在一个位置下面插入很多的数据如果采用尾插,就必须找到尾结点才能进行插入,效率会很低,所以我们采用头插的方式:

		//插入
		bool Insert(const pair<K, V>& kv)
		{
            //判断是否存在
			if (Find(kv.first))
				return false;
			//扩容
            //......
			size_t hashi = kv.first % _tables.size();
			Node* newnode = new Node(kv);
			//头插
			newnode->_next = _tables[hashi];
			_tables[hashi] = newnode;
			++_n;
		}

创造一个新节点,让新节点去指向原来的头结点,再让新节点成为头结点

下面我们来关注扩容问题,虽然哈希桶是通过链表的方式进行插入,原则上不用进行扩容就可以满足所有数据的存放。但是如果数据过大,会导致每个桶中的数据量过于庞大导致寻找操作的效率大大降低。所以规定,当数据个数等于哈希表的大小,即荷载因子α为1时,进行扩容

那么哈希桶的扩容又该如何进行呢???

因为是节点的缘故,如果像闭散列那样复用插入去扩容,那样就等于同一个节点又创建了一份,而原节点最后还需要进行销毁,这样未免过于麻烦和浪费时间。

所以更好一点的做法是,新建立一个哈希桶,遍历原桶的节点,让其按照哈希函数去直接指向新桶,最后再将两个桶进行交换

			//扩容
			if (_n == _tables.size())
			{
				vector<Node*> newtables; (_tables.size() * 2, nullptr);
				for (size_t i = 0; i < _tables.size(); i++)
				{
					Node* cur = _tables[i];
					while (cur)
					{
						Node* next = cur->_next;
						size_t hashi = kv.first % newtables.size();
						cur->_next = newtables[hashi];
						newtables[hashi] = cur;
						cur = next;
					}
					_tables[i] = nullpter;
				}
				_tables.swap(newtables);
			}

3.寻找

		//寻找
		Node* Find(const K& key)
		{
			size_t hashi = key % _tables.size();
			Node* cur = _tables[hashi];
			while (cur)
			{
				if (cur->_kv.first == key)
					return cur;
				cur = cur->_next;
			}
			return nullptr;
		}

寻找操作较为简单,就不在过多分享,注意返回值为节点类型


4.删除

链表的删除无非就三种情况,删除的是头结点,或者是中间节点,尾结点。其中尾结点可以和中间节点共用一种方式

那么如果删除中间节点,就必须提前记录该节点的前一个节点

此外就是头结点,提前定义一个prev,并置空如果cur不为头结点,就让prev继承cur,cur在往后,所以如果首次循环prev就为空,说明要删除的节点即为头结点。 

		bool Erase(const K& key)
		{
			size_t hashi = key % _tables.size();
			Node* cur = _tables[hashi];
			Node* prev = nullptr;
			while (cur)
			{
				if (cur->_kv.first == key)
				{
					//删除的是第一个节点
					if (prev == nullptr)
					{
						_tables[hashi] = cur->_next;
					}
					else
					{
						prev->_next = cur->_next;
					}
					delete cur;
				}
				else
				{
					prev = cur;
					cur = cur->_next;
				}
			}
			return false;
		}

最后我们仍需使用仿函数来解决数据类型的问题,因方法与闭散列完全相同,这里不再重复


总结

关于哈希表就分享这么多,喜欢本篇文章记得一键三连,我们下期再见!

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

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

相关文章

Excel实现将A列和B列的内容组合到一个新的列(例如C列)中,其中A列的每个值都与B列的所有值组合。

利用Excel中vba代码宏实现 原始数据&#xff1a; 自动生成后数据&#xff1a; vba实现代码&#xff1a; Sub CombineColumns()Dim ws As WorksheetDim lastRowA As Long, lastRowB As Long, i As Long, j As LongDim MyIndex As IntegerDim strCombine As String, strColA As…

Vue 3 组件基础与模板语法详解

title: Vue 3 组件基础与模板语法详解 date: 2024/5/24 16:31:13 updated: 2024/5/24 16:31:13 categories: 前端开发 tags: Vue3特性CompositionAPITeleportSuspenseVue3安装组件基础模板语法 Vue 3 简介 1. Vue 3 的新特性 Vue 3引入了许多新的特性&#xff0c;以提高框…

【计算机视觉(3)】

基于Python的OpenCV基础入门——图形与文字的绘制 图形与文字的绘制&#xff1a;画线画矩形画圆画多边形加文字 图形与文字绘制的代码实现&#xff1a; 图形与文字的绘制&#xff1a; 画线 img cv2.line(img, pt1, pt2, color, thickness) 参数&#xff1a; img&#xff1a;…

瑞芯微RV1126——ffmpeg环境搭建

本篇文章来介绍一下&#xff0c;在ubuntu上搭建一个比较完整的ffmpeg环境需要的步骤以及流程。为后期将我们开发的应用程序移植到RV1126开发板上做准备。 在安装ffmpeg之前&#xff0c;为了方便后续的操作&#xff0c;我们可以先搭建好samba服务器。所以本节将分为两个部分&am…

ThingML的学习——在ecplise里面配置maven

前置工作&#xff1a; 1.在ecplise里面配置maven之前&#xff0c;首先需要在windows里面下载maven。 2.配置环境变量 3.修改maven配置文件&#xff08;最好改为阿里云&#xff09; 1.配置Java环境&#xff0c;需要jdk版的&#xff08;jar不行&#xff09; 以上不在这里面详细介…

ACM实训第十七天

Is It A Tree? 问题 考试时应该做不出来&#xff0c;果断放弃 树是一种众所周知的数据结构&#xff0c;它要么是空的(null, void, nothing)&#xff0c;要么是一个或的集合满足以下属性的节点之间有向边连接的节点较多。 •只有一个节点&#xff0c;称为根节点&#xff0c;它…

探索生态农业,守护绿色家园

在繁忙的都市生活中&#xff0c;我们往往忽略了与自然和谐相处的重要性。而生态农业&#xff0c;正是让我们重拾与大自然亲密关系的桥梁。通过采用生态友好的耕作方式&#xff0c;生态农业不仅能够提供健康、营养的农产品&#xff0c;还能够保护生态环境&#xff0c;实现人与自…

登录安全分析报告:创蓝云智注册

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 暴力破解密码&#xff0c;造成用户信息泄露短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造成亏损无底洞…

ubuntu使用记录——如何使用wireshark网络抓包工具进行检测速腾激光雷达的ip和端口号

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言wireshark网络抓包工具1.wireshark的安装2.wireshark的使用3.更改雷达ip 总结 前言 Wireshark是一款备受赞誉的开源网络协议分析软件&#xff0c;其功能之强大…

在做题中学习(61):连续数组

525. 连续数组 - 力扣&#xff08;LeetCode&#xff09; 思路&#xff1a;前缀和 哈希表 转化&#xff1a;将 0 ——> -1 转变为&#xff1a;找到和为0的最长子数组 细节&#xff1a; 1.哈希表存什么 前缀和 &#xff0c; 长度 2.什么时候存入哈希表 先处理前一个&…

【LeetCode】【4】寻找两个正序数组的中位数(2105字)

文章目录 [toc]题目描述样例输入输出与解释样例1样例2 提示Python实现二分查找划分数组 个人主页&#xff1a;丷从心 系列专栏&#xff1a;LeetCode 刷题指南&#xff1a;LeetCode刷题指南 题目描述 给定两个大小分别为m和n的正序&#xff08;从小到大&#xff09;数组nums1…

基于Pytorch框架的卷积神经网络MNIST手写数字识别

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 一、项目背景与意义 MNIST手写数字数据集是机器学习领域中的一个经典数据集&#xff0c;它包含了大量的手写数字图…

Pytorch深度学习实践笔记2

&#x1f3ac;个人简介&#xff1a;一个全栈工程师的升级之路&#xff01; &#x1f4cb;个人专栏&#xff1a;pytorch深度学习 &#x1f380;CSDN主页 发狂的小花 &#x1f304;人生秘诀&#xff1a;学习的本质就是极致重复! 《PyTorch深度学习实践》完结合集_哔哩哔哩_bilibi…

正则表达式(知识总结篇)

本篇文章主要是针对初学者&#xff0c;对正则表达式的理解、作用和应用 正则表达式&#x1f31f; 一、&#x1f349;正则表达式的概述二、&#x1f349;正则表达式的语法和使用三、 &#x1f349;正则表达式的常用操作符四、&#x1f349;re库主要功能函数 一、&#x1f349;正…

【设计模式】JAVA Design Patterns——Balking(止步模式)

&#x1f50d;目的 止步模式用于防止对象在不完整或不合适的状态下执行某些代码。 &#x1f50d;解释 真实世界例子 洗衣机中有一个开始按钮&#xff0c;用于启动衣物洗涤。当洗衣机处于非活动状态时&#xff0c;按钮将按预期工作&#xff0c;但是如果已经在洗涤&#xff0c;则…

从零到一:手把手教你将项目部署上线-环境准备

部署步骤 引言1.Java环境配置2.ngnix安装好书推荐 引言 将自己的项目从本地开发环境顺利部署上线&#xff0c;是每个开发者必经的里程碑。今天&#xff0c;我们就从零开始&#xff0c;一步一步教你如何将手中的项目部署到线上&#xff0c;让全世界见证你的创造力。 首先&#x…

3D 高斯泼溅(Gaussian Splatting)-3D重建的3DGS时代

3D重建自从NeRfs出现之后又热闹了一次&#xff0c;3D GS技术一时间燃变了整个三维重建和Slam领域&#xff0c;几个月不见&#xff0c;沧海桑田。 NeRF貌似已成为过去式&#xff0c;三维重建进入了3DGS时代&#xff0c;且3DGS在各方面比NeRF落地更快。3D 高斯泼溅&#xff08;S…

Payload SDK dji

开发硬件 感谢您的耐心等待&#xff0c;建议您可以考虑下树莓派4B或Jetson Nano开发板&#xff0c;看您需求选择&#xff0c;OSDK即将停止服务&#xff0c;我们建议您使用PSDK来进行开发&#xff0c;PSDK包含了OSDK的功能。Payload SDK 感谢您对大疆产品的支持&#xff01;祝…

DOS学习-目录与文件应用操作经典案例-type

新书上架~&#x1f447;全国包邮奥~ python实用小工具开发教程http://pythontoolsteach.com/3 欢迎关注我&#x1f446;&#xff0c;收藏下次不迷路┗|&#xff40;O′|┛ 嗷~~ 目录 一.前言 二.使用 三.案例 1. 查看文本文件内容 2. 同时查看多个文本文件内容 3. 合并文…