c++实现哈希桶

闭散列的回顾

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

当插入的数据为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;
	}
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

一百五十二、Kettle——Kettle9.3.0本地连接Hive3.1.2(踩坑,亲测有效,附截图)

一、目的 由于先前使用的kettle8.2版本在Linux上安装后&#xff0c;创建共享资源库点击connect时页面为空&#xff0c;后来采用如下方法&#xff0c;在/opt/install/data-integration/ui/menubar.xul文件里添加如下代码 <menuitem id"file-openZiyuanku" label&…

Zass主题 - 手工艺术家和工匠的WooCommerce商城主题

Zass主题是适合手工艺术家和工匠的完美 WordPress / WooCommerce 主题。无论您是想为您的手工制作业务构建功能齐全的 Etsy Style 在线商店、博客还是作品集&#xff0c;Zass 主题都是您的正确选择。凭借其极其强大的自定义电子商务功能、无限的作品集风格、不同的博客风格和无…

DNNGP模型解读-early stopping 和 batch normalization的使用

一、考虑的因素&#xff08;仅代表个人观点&#xff09; 1.首先我们看到他的这篇文章所考虑的不同方面从而做出的不同改进&#xff0c;首先考虑到了对于基因组预测的深度学习方法的设计 &#xff0c;我们设计出来这个方法就是为了基因组预测而使用&#xff0c;这也是主要目的&…

sklearn机器学习库(二)sklearn中的随机森林

sklearn机器学习库(二)sklearn中的随机森林 集成算法会考虑多个评估器的建模结果&#xff0c;汇总之后得到一个综合的结果&#xff0c;以此来获取比单个模型更好的回归或分类表现。 多个模型集成成为的模型叫做集成评估器&#xff08;ensemble estimator&#xff09;&#xf…

NLP中的RNN、Seq2Seq与attention注意力机制

目录 NLP自然语言处理 的RNN、Seq2Seq与attention注意力机制 RNN循环神经网络 前馈网络入门 前馈网络 循环网络 多层感知器架构示例 循环神经网络的运作原理 展开 RNN seq2seq模型 Attention&#xff08;注意力机制&#xff09; 总结 引用 NLP自然语言处理 的RNN、…

iPhone删除的照片能恢复吗?不小心误删了照片怎么找回?

iPhone最近删除清空了照片还能恢复吗&#xff1f;大家都知道&#xff0c;照片对于我们来说是承载着美好回忆的一种形式。它记录着我们的平淡生活&#xff0c;也留住了我们的美好瞬间&#xff0c;具有极其重要的纪念价值。 照片不小心误删是一件非常难受的事&#xff0c;那么iP…

【Redis】Redis的持久化(备份)

【Redis】Redis的持久化&#xff08;备份&#xff09; Redis的数据全部在内存里&#xff0c;如果突然宕机&#xff0c;数据就会全部丢失&#xff0c;因此必须有一种机制来保证Redis的数据不会因为故障而丢失&#xff0c;这种机制就是Redis的持久化机制。 如图所示&#xff0c…

改善神经网络——优化算法(mini-batch、动量梯度下降法、Adam优化算法)

改善神经网络——优化算法 梯度下降Mini-batch 梯度下降&#xff08;Mini-batch Gradient Descent&#xff09;指数加权平均包含动量的梯度下降RMSprop算法Adam算法 优化算法可以使神经网络运行的更快&#xff0c;机器学习的应用是一个高度依赖经验的过程&#xff0c;伴随着大量…

2023年Android性能优化常见30道面试题解

在Android开发领域&#xff0c;性能优化是一个关键而广泛讨论的话题。对于任何一位Android开发者而言&#xff0c;了解和掌握性能优化的技巧是至关重要的。无论是在开发过程中还是在面试环节中&#xff0c;掌握Android性能优化的知识都能展示出你作为一名优秀开发者的能力。 本…

利用HTTP代理实现请求路由

嘿&#xff0c;大家好&#xff01;作为一名专业的爬虫程序员&#xff0c;我知道构建一个高效的分布式爬虫系统是一个相当复杂的任务。在这个过程中&#xff0c;实现请求的路由是非常关键的。今天&#xff0c;我将和大家分享一些关于如何利用HTTP代理实现请求路由的实用技巧&…

HTML(JavaEE初级系列12)

目录 前言&#xff1a; 1.HTML结构 1.1认识HTML标签 1.2HTML文件基本结构 1.3标签层次结构 1.4快速生成代码框架 2.HTML常见标签 2.1注释标签 2.2标题标签&#xff1a;h1-h6 2.3段落标签&#xff1a;p 2.4换行标签&#xff1a; br 2.5格式化标签 2.6图片标签&#…

【k8s】基于Prometheus监控Kubernetes集群安装部署

目录 基于Prometheus监控Kubernetes集群安装部署 一、环境准备 二、部署kubernetes集群 三、部署Prometheus监控平台 四、部署Grafana服务 五、grafana web操作 基于Prometheus监控Kubernetes集群安装部署 一、环境准备 IP地址 主机名 组件 192.168.100.131 k8s-ma…

Activiti6

一、Activiti介绍与搭建开发环境 二、运行官方例子 三、编写第一个Activiti程序 3.1 流程部署 代码 /*** 部署流程* 涉及到的表有&#xff1a; * 1.act_ge_bytearray 两条记录&#xff0c;一条xml数据&#xff0c;一条png图片信息 * 2.act_re_deployment 一条记录 * 3.a…

Android FrameWork 层 Handler源码解析

Handler生产者-消费者模型 在android开发中&#xff0c;经常会在子线程中进行一些耗时操作&#xff0c;当操作完毕后会通过handler发送一些数据给主线程&#xff0c;通知主线程做相应的操作。 其中&#xff1a;子线程、handler、主线程&#xff0c;其实构成了线程模型中经典的…

【独立版】新零售社区团购电商系统生鲜水果商城兴盛优选十荟团源码

【独立版】新零售社区团购电商系统生鲜水果商城兴盛优选十荟团源码

CentOS系统环境搭建(七)——Centos7安装MySQL

centos系统环境搭建专栏&#x1f517;点击跳转 坦诚地说&#xff0c;本文中百分之九十的内容都来自于该文章&#x1f517;Linux&#xff1a;CentOS7安装MySQL8&#xff08;详&#xff09;&#xff0c;十分佩服大佬文章结构合理&#xff0c;文笔清晰&#xff0c;我曾经在这篇文章…

5G无人露天矿山解决方案

1、5G无人露天矿山解决方案背景 ①2010.10&#xff0c;国家安监总局《金属非金属地下矿山安全避险“六大系统”安装使用和监督检查暂行规定》 ②2016.03&#xff0c;国家发改委《能源技术革命创新行动计划&#xff08;2016-2030&#xff09;》&#xff0c;2025 年重点煤矿区采…

Java:PO、VO、BO、DO、DAO、DTO、POJO

&#x1f497;wei_shuo的个人主页 &#x1f4ab;wei_shuo的学习社区 &#x1f310;Hello World &#xff01; Java&#xff1a;PO、VO、BO、DO、DAO、DTO、POJO PO持久化对象&#xff08;Persistent Object&#xff09; PO是持久化对象&#xff0c;用于表示数据库中的实体或表…

LeetCode 37题:解数独

题目 编写一个程序&#xff0c;通过填充空格来解决数独问题。 数独的解法需 遵循如下规则&#xff1a; 数字 1-9 在每一行只能出现一次。数字 1-9 在每一列只能出现一次。数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。&#xff08;请参考示例图&#xff09; 数独…

你真的了解数据结构与算法吗?

数据结构与算法&#xff0c;是理论和实践必须紧密结合的一门学科&#xff0c;有关数据结构和算法同类的课程或书籍&#xff0c;有些只是名为“数据结构”&#xff0c;而非“数据结构与算法”&#xff0c;它们在内容上并无很大区别。 实际上&#xff0c;数据结构和算法&#xf…