【C++】哈希表的模拟实现

news2024/10/21 12:02:05

目录

一、闭散列(开放定址定法)

1、哈希表的结构:

2、哈希表的插入:

3、哈希表的查找:

4、哈希表的删除:

二、开散列(哈希桶)

1、哈希表的结构:

2、构造与析构:

3、哈希表的插入:

4、哈希表的查找:

5、哈希表的删除:

三、拓展:


前言:

在模拟实现哈希表的过程中,每当发生哈希冲突之后有两种方法进行解决:分别是开放定址法链地址法(哈希桶)

一、闭散列(开放定址定法)

闭散列开放定址法是当使用哈希函数计算出的地址发生哈希冲突后,依次向下一个位置去寻找,直到找到空位置,然后填进去。

1、哈希表的结构:

每个位置中存储的数据中需要有两个信息:键值对和这个位置的状态

键值对:可以是K结构或者是K/V结构。

所处位置的状态:

	enum STATE
	{
		EXIST,
		EMPTY,
		DELETE
	};

EXIST :存在,表示这里存在数据

EMPTY :空,表示这里是空

DELETE :删除,表示这里的值被删除了,这也是需要状态的原因,因为当对哈希表进行删除的时候,并不是将里面的值进行删除,更好的是将这里面的状态进行修改就可以了。

以下就是哈希表中每个位置所存储的信息

	template<class K, class V>
	struct HashDate
	{
		pair<K, V> _kv;
		STATE _state = EMPTY;
	};

哈希表的框架:

_table这个是存放哈希表的数组,

_n是这个哈希表中存在元素的个数

template<class K, class V>
class HashTable
{
public:
	HashTable()
	{
		_table.resize(10);
	}
private:
	vector<HashDate<K, V>> _table;
	size_t _n = 0;
};

接着在public下面实现插入,查找,删除等等。

2、哈希表的插入:

思路:

首先通过哈希函数找到所处位置,接着依次判断是否为存在,如果是存在就向后走,如果找到第一个不存在的将这个位置的_kv修改为kv,并且将这个位置的状态修改为EXIST存在。

接着++_n。

但是在插入之前要进行判断,如果负载因子大于0.7的时候就进行扩容

判断扩容思路

首先看负载因子的大小,当负载因子大于0.7的时候就进行扩容,首先创建一个新的哈希数组,要resize为原来数组的两倍大小,开好后再进行映射过去。

映射思路:

for循环中,每当找到一个位置的状态为EXIST的时候,就将这个数插入到新开好的数组中,因为在插入的时候会判断负载因子所以就不会出现无限循环,这个新的数组就是我需要的,

最后将_table和新创的哈希表进行交换即可

bool Insert(const pair<K, V>& kv)
{

	//看负载因子的大小
	if (_n * 10 / _table.size() >= 7)
	{
		size_t newsize = _table.size() * 2;
		HashTable<K, V> ht;
		ht._table.resize(newsize);

		//开一个新的哈希数组,把这个原来的映射过去
		for (size_t i = 0; i < _table.size(); i++)
		{
			if (_table[i]._state == EXIST)
			{
				ht.Insert(_table[i]._kv);
			}
		}
		_table.swap(ht._table);
	}
	//扩容逻辑
	size_t hashi = kv.first % _table.size();
	while (_table[hashi]._state == EXIST)
	{
		++hashi;
		hashi = hashi % _table.size();//防止hashi大于capacity
	}
	_table[hashi]._kv = kv;
	_table[hashi]._state = EXIST;
	++_n;
	return true;
}

注意:

因为存在负载因子的原因,所以哈希表是不可能被装满的。

在扩容的时候,是将原哈希表重新映射到新位置,而不仅仅是拷贝过去,如下图15就可以看出是重新映射而不是拷贝。

3、哈希表的查找:

思路:

首先:通过哈希函数计算出对应的地址。
然后,从哈希地址处开始向后找,直到找到待查找的元素则为查找成功,或找到一个状态为EMPTY的位置判定为查找失败。


注意: 在查找过程中,找到的元素的状态必须是EXIST,并且key值匹配的元素,才算查找成功。若仅仅是key值匹配,但该位置当前状态为DELETE,则还需继续进行查找,因为该位置的元素已经被删除了,如果是EMPTY就证明是在这个哈希表中不存在这个元素,查找失败

		HashDate<const K, V>* Find(const K& key)
		{
			size_t hashi = key % _table.size();
			while (_table[hashi]._state != EMPTY)
			{
				if (_table[hashi]._state == EXIST && _table[hashi]._kv.first == key)
				{
					return (HashDate<const K, V>*) & _table[hashi];
				}
				hashi++;
				hashi %= _table.size();
			}
			return nullptr;
		}

4、哈希表的删除:

哈希表的删除就比较好搞了,并不是传统意义上的删除,而是直接将所删除的位置的状态修改为DELETE即可。

思路:

首先用Find函数进行查找,没找到删除失败,找到后就将这个位置的状态修改一下,将_n--

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

测试插入,查找,删除:

int main()
{
	open_address::HashTable<int, int> ht;
	ht.Insert(make_pair(1, 1));
	ht.Insert(make_pair(9, 9));
	ht.Insert(make_pair(15, 15));
	ht.Insert(make_pair(4, 4));
	ht.Insert(make_pair(13, 13));
	ht.Insert(make_pair(2, 2));
	ht.Insert(make_pair(7, 7));
	ht.Insert(make_pair(17, 17));
	ht.Insert(make_pair(6, 6));
	
	if (ht.Find(7))
	{
		cout << "7在哈希表里面" << endl;
	}
	else
	{
		cout << "7不在哈希表里面" << endl;
	}
	ht.Erase(7);
	if (ht.Find(7))
	{
		cout << "7在哈希表里面" << endl;
	}
	else
	{
		cout << "7不在哈希表里面" << endl;
	}
	return 0;
}

二、开散列(哈希桶)

在每一个位置中,不仅仅是只有数据和状态了,还挂着一个单链表,和指向这个单链表的下一个节点的指针,哈希表的每个位置存储的实际上是某个单链表的头结点,

总体框架大概像下面

(每个单链表的整体看做一个哈希桶)

1、哈希表的结构:

每一个节点的定义

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:

private:
	vector<Node*> _table;
	size_t _n = 0;
};

2、构造与析构:

构造函数就是将这个哈希表进行初始化,开好空间,都置为空。

析构函数就是将每一个哈希桶都删除掉,然后将哈希表中的每一个指针都置空

HashTable()
{
	_table.resize(10, nullptr);
}

~HashTable()
{
	for (size_t i = 0; i < _table.size(); i++)
	{
		Node* cur = _table[i];
		while (cur)
		{
			Node* next = cur->_next;
			delete cur;
			cur = next;
		}
		_table[i] = nullptr;
	}
}

3、哈希表的插入:

思路:

总体来说和闭散列的差不多

首先通过find函数查找这个数,如果有就不能够插入但会false,再通过哈希函数找到待插入的位置为hashi,接着new一个新节点作为待插入节点,将这个节点头插到哈希桶中。

待插入节点的next指针指向了newTable的下标为hashi的这个元素中链表的第一个节点,再把newTable[hashi]的值改成cur的指针,就可以吧cur头插到下标为hashi的这个链表中了

bool Insert(const pair<K, V>& kv)
{
	if (Find(kv.first))
	{
		return false;
	}
	if (_table.size() == _n)
	{
		size_t newSize = _table.size() * 2;
		vector<Node*> newTable;
		newTable.resize(newSize, nullptr);

		for (size_t i = 0; i < _table.size(); i++)
		{
			Node* cur = _table[i];
			while (cur)
			{
				Node* next = cur->_next;
				size_t hashi = cur->_kv.first / newTable.size();
				//将cur的_next指向newTable所在位置的第一个节点
				cur->_next = newTable[hashi];
				//newTable[hashi]处的指针指向cur
				newTable[hashi] = cur;

				cur = next;
			}
			_table[i] = nullptr;
		}
		_table.swap(newTable);
	}
	size_t hashi = kv.first % _table.size();
	Node* newnode = new Node(kv);
	newnode->_next = _table[hashi];
	_table[hashi] = newnode;
	++_n;
	return true;
}

4、哈希表的查找:

思路:

首先通过哈希函数找到哈希地址,之后遍历当前位置的哈希桶,找到就返回已查找的节点否则返回空。

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

5、哈希表的删除:

思路:

首先通过哈希函数找到哈希地址,之后遍历当前位置的哈希桶,找到待删除节点的位置后就将待删除节点的上一个位置的next指针指向待删除节点的下一个位置,在delete待删除节点即可。

bool Erase(const K& key)
{
	size_t hashi = key % _table.size();
	Node* cur = _table[hashi];
	Node* prev = nullptr;
	while (cur)
	{
		if (cur->_kv.first == key)
		{
			if (prev == nullptr)
			{
				_table[hashi] = cur->_next;
			}
			else
			{
				prev->_next = cur->_next;
			}
			delete cur;
			return true;
		}
		prev = cur;
		cur = cur->_next;
	}
	return false;
}

测试插入,查找,删除:

int main()
{
	hash_bucket::HashTable<int, int> ht;
	ht.Insert(make_pair(1, 1));
	ht.Insert(make_pair(9, 9));
	ht.Insert(make_pair(15, 15));
	ht.Insert(make_pair(4, 4));
	ht.Insert(make_pair(13, 13));
	ht.Insert(make_pair(2, 2));
	ht.Insert(make_pair(7, 7));
	ht.Insert(make_pair(17, 17));
	ht.Insert(make_pair(6, 6));
	
	ht.Print();
	ht.Erase(17);
	ht.Erase(13);
	cout << "删除17,删除13后" << endl;
	ht.Print();
	return 0;
}

三、拓展:

上述的哈希表只能插入或者删除整型的,如果是字符串就搞不好,所以就需要仿函数来进行操作,将每一个键值对中取first的通过仿函数将字符串类转化为整型类。

思路:

首先在仿函数中,将模版进行半特化,如下,如果不是string类就走上面的,这就是走个过场,直接返回key,但如果是string类的那么就通过重载运算符(),将str类转化为整型

template<class K>
struct HashFunctest
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

template<>
struct HashFunctest<string>
{
	size_t operator()(const string& str)
	{
		size_t n = 0;
		for (auto e : str)
		{
			n *= 131;
			n += e;
		}
		return n;
	}
};

测试:

int main()
{
    hash_bucket::HashTable<string, string> ht;
	ht.Insert(make_pair("apple", "苹果"));
	ht.Insert(make_pair("pear", "梨子"));
	ht.Insert(make_pair("banana", "香蕉"));
	ht.Insert(make_pair("watermelon", "西瓜"));
	ht.Print();
	cout << "删除pear后" << endl;
	ht.Erase("pear");
	ht.Print();

	return 0;
}

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

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

相关文章

若依前后分离版集成积木报表进行token传递

若依分离板集成积木报表就不说了需要的请移步&#xff1a;若依前后分离版集成积木报表-CSDN博客 考虑到前端摸鱼不干活,所以一般都是前后端都干&#xff0c;我这里前后端都搞上&#xff0c;你们直接抄&#xff0c;抄完接着去摸鱼&#xff0c;代码不美观&#xff0c;轻喷 一、…

【JavaEE】【多线程】synchronized和死锁

目录 一、synchronized详解1.1 互斥1.2 可重入 二、死锁2.1 死锁成因2.2 避免死锁 一、synchronized详解 1.1 互斥 synchronized 会起到互斥效果, 某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执行到 同一个对象 synchronized 就会阻塞等待. 语法&#xff1…

AI时代程序员何去何从?提升自我还是被淘汰出局!

AI 在编程界的使用变得越来越普遍了。随着 ChatGPT 的横空出世&#xff0c;各种大语言模型如雨后春笋不断出现。国外如谷歌 Bard、Anthropic 的 Claude&#xff0c;国内如百度文心一言、阿里通义千问、讯飞星火认知大模型、昆仑万维天工大模型等。 想想看&#xff0c;以前得花好…

支持国密算法的数字证书-国密SSL证书详解

在互联网中&#xff0c;数字证书作为标志通讯各方身份信息的数字认证而存在&#xff0c;常见的数字证书大都采用国际算法&#xff0c;比如RSA算法、ECC算法、SHA2算法等。随着我国加强网络安全技术自主可控的大趋势&#xff0c;也出现了支持国密算法的数字证书-国密SSL证书。那…

【网络安全】缓存欺骗问题之查看个人资料接口

未经许可,不得转载。 文章目录 正文正文 目标网站 target.com,查看个人资料页面时,API 端点为/get_user,完整的 URL 是 https://target.com/web-api/v1/get_user?timestamp=123456(其中 timestamp 是一个易受攻击的参数)。 我注意到响应中有一个 cf-cache-status= MISS…

k8s部署Kafka集群超详细讲解

准备部署环境 Kubernetes集群信息 NAMEVERSIONk8s-masterv1.29.2k8s-node01v1.29.2k8s-node02v1.29.2 Kafka&#xff1a;3.7.1版本&#xff0c;apche版本 Zookeeper&#xff1a;3.6.3版本 准备StorageClass # kubectl get sc NAME PROVISIONER RECLA…

Docker安装ActiveMQ镜像以及通过Java生产消费activemq示例

拉取镜像 docker pull docker.io/webcenter/activemq 启动容器 docker run -d --name myactivemq -p 61616:61616 -p 8162:8161 docker.io/webcenter/activemq:latest 这样就代表启动成功了 浏览器访问 http://localhost:8162/ admin admin 开启验证 修改配置文件/opt/ac…

关于k8s集群高可用性的探究

1. k8s的高可用的核心是什么&#xff1f; 说到核心、本质 意味着要从物理层来考虑技术 k8s是一个容器编排管理工具&#xff0c;k8s受欢迎的时机 是docker容器受欢迎时&#xff0c;因为太多的docker容器&#xff0c;管理起来是一个大工程 那么刚好k8s是google自己用了十来年…

STM32L031F6P6基于CubeMX的串口通信调试笔记

用CubeMX创建项目 本实例用的PA14、PA13两个引脚&#xff0c;LPUART1。 对串口参数进行设置&#xff1a; 开启串口中断&#xff1a; 时钟源设置成内部高频时钟&#xff1a; 对项目进行设置&#xff1a; 生成代码&#xff1a; 在串口初始化函数中加入 __HAL_UART_ENA…

Asp.net Core MVC 动态路由

动态路由 asp.net core 3.0 就支持了 // 映射关系public class TranslationDatabase{private static Dictionary<string, Dictionary<string, string>> Translations new Dictionary<string, Dictionary<string, string>>{{"en", new Dictio…

《Vue3 踩坑》expose 和 defineExpose 暴露属性或方法注意事项

选项式写法 使用 选项式API - 状态选项 - expose 一定要注意&#xff1a; 接下来&#xff0c;进一步看示例说明&#xff1a; 设置 expose 仅显示列出的属性/方法才能被父组件调用&#xff1b;代码第 2 行&#xff0c;父组件可访问属性 a 和 方法 myFunc01&#xff0c;不可访…

Windows server 2022 数据中心版本的安装

安装前的准备工作&#xff1a; 1.准备好VMware虚拟机&#xff08;略&#xff09; 2.准备好镜像 图1-1 准备安装镜像 3.准备好安装的位置&#xff08;F盘2022vm文件夹&#xff09; 图1-2 选择并设定安装位置 4.开始安装 图1-3 开心VMware安装向导 图1-4 插入光盘镜像 图1-5…

视频转文字工具搜集

视频转文字工具是一种能够将视频中的音频内容转化为文字的软件或在线服务。这类工具通常支持多种视频格式和语言&#xff0c;适用于不同的场景和需求。以下是一些推荐的视频转文字工具及其特点&#xff1a; 媒关系&#xff1a;这是一款免费的视频转文字工具&#xff0c;支持多种…

ABAQUS应用11——支座弹簧

文章目录 0、背景1、ABAQUS中几类弹簧的简介2、SPRING1的性质初探 0、背景 1、ABAQUS中几类弹簧的简介 先说参考来源&#xff0c;ABAQUS2016的帮助文档里第4卷&#xff0c;32.1.1节&#xff0c;有三种弹簧&#xff08;SPRING1 、SPRING2 以及SPRINGA&#xff09;。 三种弹簧里…

【AIGC】ChatGPT提示词Prompt高效编写模式:Self-ask Prompt、ReACT与Reflexion

博客主页&#xff1a; [小ᶻZ࿆] 本文专栏: AIGC | ChatGPT 文章目录 &#x1f4af;前言&#x1f4af;自我提问 (Self-ask Prompt)如何工作应用实例优势结论 &#x1f4af;协同思考和动作 (ReACT)如何工作应用实例优势结论 &#x1f4af;失败后自我反思 (Reflexion)如何工作…

oracle数据恢复—文件损坏导致Oracle数据库打开报错的数据恢复案例

oracle数据库故障&分析&#xff1a; 打开oracle数据库时报错&#xff0c;报错信息&#xff1a;“system01.dbf需要更多的恢复来保持一致性&#xff0c;数据库无法打开”。急需恢复zxfg用户下的数据。 出现上述报错的原因有&#xff1a;控制文件损坏、数据文件损坏、数据文件…

FastDFS单节点部署

FastDFS单节点部署 1、FastDFS入门1.1 分布式文件系统1.2 FastDFS 简介1.3 FastDFS 发展历史1.4 FastDFS 整体架构1.5 FastDFS 线上使用者 2、FastDFS 环境搭建2.1 FastDFS 安装2.1.1 安装前的准备2.1.2 安装 libfastcommon库2.1.3 安装 FastDFS 2.2FastDFS 配置2.2.1 去掉/etc…

python爬虫快速入门之---Scrapy 从入门到包吃包住

python爬虫快速入门之—Scrapy 从入门到包吃包住 文章目录 python爬虫快速入门之---Scrapy 从入门到包吃包住一、scrapy简介1.1、scrapy是什么?1.2、Scrapy 的特点1.3、Scrapy 的主要组件1.4、Scrapy 工作流程1.5、scrapy的安装 二、scrapy项目快速入门2.1、scrapy项目快速创建…

Spring Boot框架下JavaWeb在线考试系统的创新实现

2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统&#xff0c;它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等&#xff0c;非常…

JavaFx学习--chapter02(网络对话)

chapter02(网络对话) 简单网络对话程序 设计任务&#xff1a;客户端向服务器发送字符串&#xff0c;并能读取服务器返回的字符串。 知识点&#xff1a;TCP套接字技术&#xff0c;C/S软件架构程序设计 重点理解&#xff1a;Java客户套接字类Socket和服务器套接字类ServerSoc…