c++实现哈希桶

news2024/11/18 23:41:53

闭散列的回顾

在前面的学习中我们知道了闭散列的运算规则,当两个数据计算得到的位置发生冲突时,它会自动的往后寻找没有发生冲突的位置,比如说当前数据的内容如下:

当插入的数据为33时计算的位置为3,可是位置3已经被占领了并且4也被占领了,但是位置5没有被占领所以插入数据33就会占领位置5,那么这里的图片就如下:

这就是闭散列的插入原则,并且每个节点都有一个变量用来表示状态,这样在查找就不会出现漏查的情况,但是这样的实现会存在一个问题,扩容是根据有效数据的个数和vector容量来确定的,但是查找的时候是根据当前元素的状态是否为空来判断后面还有没有要查找的数据,如果为空的话则说明当前元素的后面没有我们要查找的元素,如果为存在或者被删除的话就说明当前元素的后面还有我们要查找的数据,如果我们不停的插入数据并且删除数据的话就会导致容器中的每个元素的状态都变成了被删除这样在查找一个不存的数据时,就会陷入死循环的状态那么这就是我们之前模拟实现的一个缺点,那么这里我们就来看看第二个解决数据不集中的方法:拉链法或者叫哈希桶法。

拉链法/哈希桶的原理

这个方法就是每个位置上都是一个链表,如果有多个位置发生冲突了,那么就挂在这个位置的链表上,这样就不会导致占领别人的位置,当我们要查找的时候就是先找到插入数据的位置,然后再通过这个位置的链表来按照顺序来进行查找,比如说下面的图片

当我们想要插入一个数据13时就会先计算13对应在哈希表上的位置,根据之前的计算原则这里得到的位置就是3,所以图片就变成了下面这样:

如果再插入数据23的话这里计算的位置依然是3,但是此时3上已经有元素了,所以这时就会使用链表的头插将数据23插入到13的前面,那么这里的图片就是下面这样:

如果再插入数据33的话计算的位置依然是3,所以就会把33放到3号位置对应的链表的头部,那么这里的图片就变成下面这样:

那么这就是哈希桶的插入规则,找到对应位置的链表将数据插入到头部即可,如果要查找的话也是相同的原理先找到数据对应的链表然后循环遍历这个链表找到出现的数据即可,删除也是相同的道理,先找到数据对应的下标然后根据下标找到对应的链表,最后遍历链表找到要删除的数据进行链表的删除即可,那么这就是哈希桶的实现思路接下来我们就来看看这种方法的准备工作。

准备工作

哈希的底层是一个vector的数组,数组中的每个节点都有一个pair类型的数据,其次还有一个指针指向当前链表节点的下一个节点,所以每个节点中有个一个pair类型的数据和一个指向节点的指针,所以这里得创建一个类来描述每个节点,并且类中有一个构造函数来初始化节点,这里的构造函数就需要一个pair类型的参数,在构造函数里面就将指针初始化为nullptr将pair类型的参数赋值为传递过来的参数,有因为这里的节点可能要存储各种各样的数据,所以这里得创建个模板来接收各种各样的参数,并且模板的参数得是两个,那么这里的代码就如下:

template<class K,class V>
struct HashNode
{
	HashNode(const pair<K,V>& kv)
		:_kv(kv)
		,_next(nullptr)
	{}
	pair<K, V> _kv;
	HashNode* _next;
};

根据前面的学习我们知道要想计算数据对应在哈希表上的位置就得添加对应的仿函数,那么这里的代码就如下

template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};
template<>
struct HashFunc<string>
{
	size_t operator()(const string& s)
	{
		size_t res = 0;
		for (auto& ch : s)
		{
			res *= 131;
			res += ch;
		}
		return res;
	}
};

最后就是哈希bucket类的准备工作,首先这个类有一个模板,模板中有三个参数,前两个表示存储数据的类型,最后一个表示的是仿函数,因为哈希的地城是vector数组,所以这里得添加一个vector容器用来存储数据和一个整型变量用来记录容器中有效字符的个数即可,并且vector中的每个元素都是节点类型,那么该类的构造函数就将vector容器的resize到10个元素即可,那么这里的代码就如下:

template<class K, class V, class Hash = HashFunc<K>>
class BucketTable
{
	typedef HashNode<K, V> Node;
public:
typedef HashNode<K, V> Node;
	BucketTable()
		:_n(0)
	{
		_tables.resize(10);
	}
private:
	vector<Node*> _tables;
	size_t _n;
};

看到这里我们的准备工作就完成了接下来就要实现哈希的每个函数。

find函数

find函数就是先根据传递过来参数找到这个参数可能出现的位置,找到了位置就意味着找了一个链表的头节点,所以这个时候就可以通过循环遍历的方式来一一对比链表中是否含有想要查找的数据,如果存在的话就返回这个节点所在的地址,如果不存在的话就返回一个空指针,所以该函数的第一步就创建一个仿函数对象,并计算数据出现的位置:

Node* Find(const K& key)
{
	Hash hf;
	size_t pos = hf(key) % _tables.size();
	Node* cur=_tables[pos]
}

cur指向的是链表的第一个元素,所以接下来就可以使用while循环一个一个的遍历每个元素,每次循环都会改变cur的指向让其指向下一个元素,知道cur本身变为空就停止查找,在循环体的内部如果出现了跟查找变量一样的值就直接返回这个节点的地址,如果循环结束了也没有找到想要的值的话就返回一个空指针,那么这里的代码就如下:

Node* Find(const K& key)
{
	Hash hf;
	size_t pos = hf(key) % _tables.size();
	Node* cur = _tables[pos];
	while (cur)
	{
		if (cur->_kv.first == key)
		{
			return cur;
		}
		else
		{
			cur = cur->_next;
		}
	}
	return nullptr;
}

插入函数

将数据插入的链表的时候得先判断一下要插入的元素当前是否已经存在,所以这里可以使用find函数来进行查找,根据find函数的返回值来判断是否存在,那么这里的代码就如下:

bool insert(const pair<K, V>& kv)
{
	if (Find(kv.first))
	{
		return false;
	}
}

如果当前的元素不存在的话就开始插入数据,这种实现方法也得根据传递过来的元素找到应该插入的位置,所以该函数的第一步就是创建一个仿函数对象然后根据传递过来的参数计算得出应该插入的位置,找到插入位置之后就使用头插来插入对应的数据,这里的头插就是先让newnode的_next指向当前位置的链表,然后修改vector中当前位置的指向使其指向newnode,那么这里的代码就如下:

bool insert(const pair<K, V>& kv)
{
	if (Find(kv.first))
	{
		return false;
	}
	Hash hf;
	size_t pos = hf(kv.first) % _tables.size();
	Node* newnode = new HashNode<K,V>(kv);
	newnode->_next = _tables[pos];
	_tables[pos] = newnode;
	++_n;
	return true;
}

这里依然得添加负载因子,官方库中可以通过一些函数来得到当前容器的负载因子和最大的负载因子,如果负载因子等于1了我们就扩容,将其容量变为之前的两倍,但是扩容不能直接把链表对应的移动到新的容器上去因为这里的映射关系已经改变了比如说当前容器的容量为10则数据18对应的位置就是8上面的链表,如果容器发生了扩容使得容量变成了20的话18就对应的不再是8而是18上面的链表,所以我们这里解决的方法就是创建一个新的哈希表,然后遍历容器中的每个位置,如果当前位置不为空就往这个位置里面进行遍历对每个元素都进行插入操作,如果当前位置为空的话就跳转到下一个元素,那么这里的代码就如下:

bool insert(const pair<K, V>& kv)
{
	if (!Find(kv.first))
	{
		return false;
	}
	if (_n / _tables.size() == 1)//平衡因子为1就更新
	{
		vector<Node*> newBH;
		newBH._tables.resize(_n * 2);
		for (auto& cur : _tables)
		{
			while (cur)
			{
				newBH.insert(cur->_kv);
				cur = cur->_next;
			}
		}
		_tables.swap(newBH._tables);
	}
	Hash hf;
	size_t pos = hf(kv.first) % _tables.size();
	Node* newnode = new HashNode<K,V>(kv);
	newnode->_next = _tables[pos];
	_tables[pos] = newnode;
	++_n;
	return true;
}

erase函数

erase函数也是分为三步来完成,首先找到节点对应的链表,然后再找链表中是否存在该元素,如果不存在的话就返回false,如果存在的话就对该链表的该节点进行删除,因为这里删除的时候得保证链表之间的上下链接,所以这里创建一个指向指向被删除节点的前一个节点,以此来保证删除前后的链接,这里大家要注意的一点就是当删除的节点是头节点时,得改变vector容器中的指针的指向,那么这里的代码就如下:

bool erase(const K& key)
{
	HashFunc<K> HF;
	size_t pos = HF(key) % _tables.size();
	Node* cur = _tables[pos];
	Node* prev = cur;
	while (cur)
	{
		if (cur->_kv.first == key)
		{
			if (cur == _tables[pos])
			{
				_tables[pos] = cur->_next;
			}
			else
			{
				prev->_next = cur->_next;
			}
			delete cur;
			_n--;
			return true;
		}
		else
		{
			prev = cur;
			cur = cur->_next;
		}
	}
	return false;
}

代码测试

可以通过下面的代码来进行相应的测试看看我们上面写的代码是否是正确的:

void TestHT1()
{
	BucketTable<int, int> ht;
	int a[] = { 18, 8, 7, 27, 57, 3, 38, 18 };
	for (auto e : a)
	{
		ht.insert(make_pair(e, e));
	}
	ht.insert(make_pair(17, 17));
	ht.insert(make_pair(5, 5));
	if (ht.Find(7)) { cout << "存在" << endl; }
	else { cout << "不存在" << endl; }
	ht.erase(7);
	if (ht.Find(7)) { cout << "存在" << endl; }
	else { cout << "不存在" << endl; }
}
int main()
{
	TestHT1();
	return 0;
}

代码的运行结果如下:

我们可以再用下面的代码来进行一下测试:

void TestHT2()
{
	string arr[] = { "苹果", "西瓜", "香蕉", "草莓", "苹果", "西瓜", "苹果", "苹果", "西瓜", "苹果", "香蕉", "苹果", "香蕉" };
	//HashTable<string, int, HashFuncString> countHT; 
	BucketTable<string, int> countHT;
	for (auto& e : arr)
	{
		HashNode<string, int>* ret = countHT.Find(e);
		if (ret)
		{
			ret->_kv.second++;
		}
		else
		{
			countHT.insert(make_pair(e, 1));
		}
	}
}

这段代码的运行结果如下:

有了这个游戏之后就可以对insert函数进行改进,但是这里先不要急还有一个地方需要我们改进的就是插入数据的时候,上面扩容在插入数据的时候是创建一个哈希桶然后再调用哈希桶来插入原来哈希桶的每个数据,如果这么做的话,在新的哈希桶里面又会不断地创建地节点,并且在函数结束地时候又会删除节点,如果节点的个数非常多的话这就会导致效率低下,所以我们这里就有一个改进思路就是能不能用已有的节点来插入到新创建的哈希表呢?答案是可以的,我们依然是创建一个新的哈希表然后改变其内部的大小,然后遍历之前的老哈希表找到里面的元素并计算他在新表上的位置,然后修改其节点内部指针的指向,那么这里的代码如下:

if (_n / _tables.size() == 1)//平衡因子为1就更新
{
	/*vector<Node*> newBH;;
	newBH.resize(_n * 2);
	for (auto& cur : _tables)
	{
		while (cur)
		{
			newBH.insert(cur->_kv);
			cur = cur->_next;
		}
	}
	_tables.swap(newBH._tables);*/
	vector<Node*> newBH;
	newBH._tables.resize(__stl_next_prime(_tables.size()));
	for (int i = 0; i < _tables.size(); i++)
	{
		Node* cur = _tables[i];
		while (cur)
		{
			Node* next = cur->_next;
			size_t pos = Hash()(cur->_kv.first);
			cur->_next = newBH[pos];
			newBH[pos] = cur;
			cur = next;
		}
		_tables[i] = nullptr;
	}
}

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

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

相关文章

经典人体模型SMPL介绍(一)

SMPL是马普所提出的经典人体模型&#xff0c;目前已成为姿态估计、人体重建等领域必不可少的基础先验。SMPL基于蒙皮和BlendShape实现&#xff0c;从数千个三维人体扫描结果得来&#xff0c;后通过PCA统计学习得来。 论文&#xff1a;SMPL: A Skinned Multi-Person Linear Mode…

教学资源VR设备中控系统为教师带来了许多便利

为提升VR实训室教学的质量&#xff0c;虚拟课堂VR中控系统作为一种新型的教学辅助工具&#xff0c;为学生和教师带来了许多独特的优势。 虚拟课堂VR中控系统作为一种新型的教育工具&#xff0c;为教师带来了许多便利&#xff0c;提高了教学质量和效果。教师可以在VR教室里的触摸…

常识判断

头像 carrin&#xff5e;&#x1f47b; 产品经理 225/753 75/302.5 30/152 15/101.5 等差数列&#xff0c;所以最后一个是10/101 收起 60 回复 发布于 2020-02-18 16:33

茶百道们掀起上市潮,折射出新茶饮即将迎来“惊险一跃”

一只憨态可掬的熊猫手捧一杯奶茶&#xff0c;它蓝白相间的颜色透露出一股清新纯净的气息。 这只名叫丁丁猫的蠢萌熊猫陪着茶百道走出四川&#xff0c;足迹踏遍了全国。 目前&#xff0c;茶百道在全国共有7117家门店&#xff0c;已经成长为新茶饮赛道的头部品牌。根据弗若斯特沙…

DRF 缓存

应用环境 django4.2.3 &#xff0c;python3.10 由于对于服务而言&#xff0c;有些数据查询起来比较费时&#xff0c;所以&#xff0c;对于有些数据&#xff0c;我们需要将其缓存。 最近做了一个服务&#xff0c;用的时 DRF 的架构&#xff0c;刚好涉及缓存&#xff0c;特此记…

mysql导入sqlserver数据库

1、 2、 3、 必须要保存密码 4、 5、 6、

史上最全的Qt控件

本软件是收费工具&#xff0c;学生党勿扰&#xff0c;闹眼子党勿扰&#xff0c;白嫖党勿扰 收费金额&#xff1a;1000元 1 概述 经过这两年的编写&#xff0c;写不少控件&#xff0c;甚至把刘某某90%的控件都绘制了一遍。当然后还有一些其他刘某没有控件。 2 功能 借用刘某博…

功率放大器在电火花加工中的作用有哪些

电火花加工技术是一种高精度、高效率的制造工艺&#xff0c;在模具制造、航空航天和汽车工业等领域得到了广泛应用。而功率放大器则是电火花加工设备的核心部件之一&#xff0c;它能够改善电火花加工的机械性能和加工质量&#xff0c;提高生产效率。下面我们来详细了解功率放大…

国内外常用的几款组态软件(Web组态)

组态软件&#xff0c;又称监控组态软件&#xff0c;译自英文SCADA&#xff0c;即 Supervision,Control and Data Acquisition(数据采集与监视控制)&#xff0c;组态软件的应用领域很广&#xff0c;实际上&#xff0c;这些软件也是一种通用级的软件工具&#xff0c;可以通过灵活…

OpenWrt -- OpenVPN配置ServerClient(TUN模式)

一、前言 目标是两台设备能通过OpenVPN TUN模式建立连接。 准备如下&#xff1a; 设备 友善R2S两台&#xff0c;一台做服务器&#xff0c;一台做客户端。 一台小米R1C&#xff0c;当作网关。 固件 采用openwrt-22.03版本&#xff0c;下载地址:https://downloads.openwrt.org/r…

EmbedPress Pro 在WordPress网站中嵌入任何内容

EmbedPress Pro可让您通过高级自定义、自定义品牌、延迟加载和更多惊人功能嵌入源。为古腾堡块和Elementor编辑器提供支持的一体化 WordPress 嵌入解决方案。使用 EmbedPress 在古腾堡创建交互式内容。使用 EmbedPress 的古腾堡块立即将任何内容嵌入到您的网站。 网址: EmbedP…

合成数据及其在AI领域中的作用

什么是合成数据&#xff1f; 合成数据是由人工创建而非从现实生活中获得的数据&#xff0c;它从机器学习对数据的需求发展而来。最初&#xff0c;为了精确训练AI模型&#xff0c;必须获得涵盖所有可能场景的训练数据。如果某个场景没有发生或未被获得&#xff0c;就没有相应的…

23.8.16日总结

原先写的评论是每级评论用缩进来区分&#xff0c;所以最多设置的是九级评论&#xff0c;修改了排版和格式&#xff1a; 还有管理员页面&#xff0c;查看文章时可以进行点赞&#xff0c;收藏的操作&#xff0c;现在进行了修改&#xff0c;将相关操作隐藏。 还有点击查看未发布…

UE4/UE5 照明构建失败 “Lightmass crashed”解决“数组索引越界”

在构建全局光照时,经常会出现“Lightmass crashed”的错误,导致光照构建失败。本文将分析这一问题的原因,并给出解决建议。 UE4 版本4.26 报错如下: <None> === Lightmass crashed: === Assertion failed: (Index >= 0) & (Index < ArrayNum) [File:d:\build…

Android Studio 新建module报错:No signature of method

android平台uni原生插件开发过程中&#xff0c;使用Android Studio 新增 module 报错 选择app --> create new module &#xff0c;填写相关信息 Android Studio 新建module报错&#xff1a; 原因&#xff1a;Android Studio 版本过高&#xff0c;新增了namespace&#x…

Redis——hash类型详解

概述 Redis本身就是键值对结构&#xff0c;而Redis中的value可以是哈希类型&#xff0c;为了区分这两个键值对&#xff0c;Redis中的键值对是key-value&#xff0c;而value中的哈希键值对则是field-value&#xff0c;其中value必须是字符串 下面介绍一些Redis的hash类型的常用…

数据可视化和数字孪生相互促进的关系

数据可视化和数字孪生是当今数字化时代中备受关注的两大领域&#xff0c;它们在不同层面和领域为我们提供了深入洞察和智能决策的机会&#xff0c;随着两种技术的不断融合发展&#xff0c;很多人会将他们联系在一起&#xff0c;本文就带大家浅谈一下二者之间相爱相杀的关系。 …

软件工程模型-架构师之路(四)

软件工程模型 敏捷开发&#xff1a; 个体和交互 胜过 过程和工具、可以工作的软件 胜过 面面俱到的文件、客户合作胜过合同谈判、响应变化 胜过 循序计划。&#xff08;适应需求变化&#xff0c;积极响应&#xff09; 敏捷开发与其他结构化方法区别特点&#xff1a;面向人的…

chromedriver、geckodriver、MicrosoftWebDriver、IEDriverServer和operadriver之间的恩怨纠葛

测试环境&#xff1a;操作系统为Windows10-64位 具体目标&#xff1a;安装五大浏览器及其驱动 目录 一、谷歌浏览器1.Google Chrome的安装2.chromedriver的下载 二、火狐浏览器1.Firefox的安装2.geckodriver的下载 三、Edge浏览器1.Microsoft Edge的安装2.MicrosoftWebDriver的…

563.二叉树的坡度(递归)

一、题目 563. 二叉树的坡度 - 力扣&#xff08;LeetCode&#xff09; 二、代码 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* …