【C++】模拟实现hash_table(哈希表)

news2024/10/8 19:44:48

🦄个人主页:修修修也

🎏所属专栏:实战项目集

⚙️操作环境:Visual Studio 2022


目录

一.了解项目功能

二.逐步实现项目功能模块及其逻辑详解

📌实现HashNode类模板

🎏构造HashNode类成员变量

🎏实现HashNode类构造函数

📌实现HashTable类模板

🎏构造HashTable类成员变量

🎏实现HashTable类构造函数

🎏实现HashTable类插入函数

🎏实现HashTable类查找函数

🎏实现HashTable类删除函数

🎏实现HashTable类析构函数

三.项目完整代码

test.cpp文件

HashTable.h文件

结语


一.了解项目功能

在本次项目中我们的目标是使用开散列的拉链法解决哈希冲突来实现一个哈希表模板,还不了解哈希表概念的朋友可以先移步[【数据结构】什么是哈希表(散列表)?],其结构图示如下:

        哈希结点(HashNode)需要包含两个成员:键值对_kv,后继结点指针域_next。逻辑结构图示如下:

           哈希表类模板提供的功能有:

  1. 哈希表结点类的构造函数
  2. 哈希表构造函数
  3. 哈希表的析构函数
  4. 哈希表的插入函数
  5. 哈希表的查找函数
  6. 哈希表的删除函数

二.逐步实现项目功能模块及其逻辑详解

通过第一部分对项目功能的介绍,我们已经对哈希表的功能有了大致的了解,虽然看似需要实现的功能很多,貌似一时间不知该如何下手,但我们可以分步分模块来分析这个项目的流程,最后再将各部分进行整合,所以大家不用担心,跟着我一步一步分析吧!


!!!注意,该部分的代码只是为了详细介绍某一部分的项目实现逻辑,故可能会删减一些与该部分不相关的代码以便大家理解,需要查看或拷贝完整详细代码的朋友可以移步本文第四部分。


📌实现HashNode类模板

🎏构造HashNode类成员变量

        我们在一开始需求分析时就已经明确了哈希结点(HashNode)需要包含两个成员:键值对_kv,后继结点指针域_next.结点(RBTreeNode)逻辑结构图示如下:

        综上所述,该部分代码如下:

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

🎏实现HashNode类构造函数

        HashNode的构造函数我们实现两个即可,一个是有参构造,一个是无参构造,而无参构造又可以通过给缺省值的方式和有参构造合二为一,所以我们用初始化列表来实现一下HashNode的构造函数:

HashNode(const pair<K,V>& kv = pair<K,V>())
	:_kv(kv)
	,_next(nullptr)
{}

📌实现HashTable类模板

🎏构造HashTable类成员变量

        HashTable类成员变量比较简单,底层的链表数组用vector来实现就行, 为了简化模板中出现的HashNode<K,V>类型的名字,我们将其简化命名为:Node。然后再设置一个变量_n来记录当前哈希表中有效元素个数, 方便我们后续扩容使用.

        该部分代码如下:

template<class K, class V, class HashFunc = DefaultHashFanc<K>>//最后一个参数是哈希函数模板
class HashTable
{
	typedef HashNode<K, V> Node;
public:

private:
		vector<Node*> _table;	//指针数组
		size_t _n;     //有效元素个数
};

🎏实现HashTable类构造函数

        HashTable类的构造函数非常简单,因为只有两个成员变量_table和_n。对于_table,最开始我们可以调用vector自带的接口来先初始化10个空间的大小方便使用,对于_n最开始肯定是置为0,综上,代码如下:

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

🎏实现HashTable类插入函数

        哈希表的插入逻辑比红黑树简单不少,简单来讲就是先使用哈希函数计算插入位置,然后在表里找对应位置的链表将新结点头插即可。但是在插入之前还有一些小细节,比如要先判断结点在不在哈希表中,如果在就不用插入了。还要判断哈希表的负载因子是否到达1,即哈希表中有效结点个数/哈希表的大小是否=1,如果等于1就需要进行哈希表扩容, 具体的扩容逻辑见代码注释。

        综上,代码如下:

bool Insert(const pair<K, V>& kv)
{
    //检查结点是否在哈希表中,如果在就返回插入失败
	if (Find(kv.first))
	{
		return false;
	}

	HashFunc hf;

	//扩容逻辑:负载因子到1就扩容
	if (_n == _table.size())
	{
		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 = hf(cur->_kv.first) % newSize;
				cur->_next = newTable[hashi];
				newTable[hashi] = cur;

				cur = next;
			}
		}
		_table.swap(newTable);
	}
	
    //用哈希函数计算插入位置 
	size_t hashi = hf(kv.first) % _table.size();

	//链表头插
	Node* newnode = new Node(kv);

	newnode->_next = _table[hashi];
	_table[hashi] = newnode;

	++_n;
	return true;
}

🎏实现HashTable类查找函数

        开链法哈希表的查找逻辑很简单,就是按照哈希函数去算key的位置,然后将该位置的链表向后遍历查找该元素即可,代码如下:

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

🎏实现HashTable类删除函数

        开链法哈希表的删除逻辑也很简单,就是按照哈希函数去算key的位置,然后将该位置的链表向后遍历查找该元素,找到之后按照链表结点的删除逻辑删除该结点即可,代码如下:

bool Erase(const K& key)
{
	HashFunc hf;
    //计算位置
	size_t hashi = hf(key) % _table.size();
	Node* prev = nullptr;
	Node* cur = _table[hashi];
    //查找并删除链表中的待删结点
	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;
}

🎏实现HashTable类析构函数

         哈希表的析构函数我们必须自己实现, 因为无论是vector的析构函数还是默认生成的都不能做到有效释放vector链表中的一个一个结点, 会导致内存泄漏, 所以我们需要自己手动实现.实现逻辑也不难, 逐一遍历哈希表然后逐一释放所有表中结点元素即可, 代码如下:

~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;
	}
}

三.项目完整代码

我们将程序运行的代码分别在两个工程文件中编辑,完整代码如下:

test.cpp文件

        该文件主要包含哈希表功能测试代码,可酌情参考.

#include"HashTable.h"

void test_openadd()
{
	open_address::HashTable<int, int> ht;
	int a[] = { 1,111,4,7,15,25,44,9 };
	for (auto e : a)
	{
		ht.Insert(make_pair(e, e));
	}

	auto ret = ht.Find(4);
	//ret->_kv.first = 40;
	ret->_kv.second = 400;

	//字符串做key. 利用仿函数,类模板的特化    相关算法BKDR Hash
	open_address::HashTable<string, string> dict;
	dict.Insert(make_pair("sort", "排序"));
	dict.Insert(make_pair("left", "xxx"));
	dict.Insert(make_pair("insert", "插入"));
	auto dret = dict.Find("left");

	dret->_kv.second = "左边";
}


int main()
{
	//test_openadd();
	hash_bucket::HashTable<int, int> ht;
	int a[] = { 1,111,4,7,15,25,44,9,14,27,24 };
	for (auto e : a)
	{
		ht.Insert(make_pair(e, e));
	}
	ht.Print();

	//字符串做key. 利用仿函数,类模板的特化    相关算法BKDR Hash
	hash_bucket::HashTable<string, string> dict;
	dict.Insert(make_pair("sort", "排序"));
	dict.Insert(make_pair("left", "xxx"));
	dict.Insert(make_pair("insert", "插入"));
	dict.Insert(make_pair("string", "字符串"));
	dict.Insert(make_pair("erase", "删除"));
	dict.Insert(make_pair("find", "查找"));
	auto dret = dict.Find("left");

	dret->_kv.second = "左边";

	dict.Print();

	return 0;
}

HashTable.h文件

        该文件中还实现了必散列的线性探测法实现哈希表,和文中主要讲的开链法分别实现在两个命名空间中,供大家参考.

#pragma once
#include<iostream>
#include<vector>
#include<string>
using namespace std;

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

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

//必散列的线性探测法实现哈希表
namespace open_address
{
	enum STATE
	{
	EXIST,	//存在
	EMPTY,	//空
	DELETE	//删除
	};

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


	template<class K,class V,class HashFunc = DefaultHashFanc<K>>
	class HashTable
	{
	public:
	HashTable()
	{
		_table.resize(10);
	}


	bool Insert(const pair<K, V>& kv)
	{
		if (Find(kv.first))
		{
			return false;
		}
		//扩容
		if ((double)_n / _table.size() >= 0.7)
		{
			size_t newSize = _table.size() * 2;
			//不能简单的只括容量,还要重新映射
			HashTable<K, V, HashFunc> newHT;
			newHT._table.resize(newSize);

			//遍历旧表的数据插入到新表
			for (size_t i = 0; i < _table.size(); i++)
			{
				if (_table[i]._state == EXIST)
				{
					newHT.Insert(_table[i]._kv);
				}
			}
			_table.swap(newHT._table);
		}
		HashFunc hf;
		//算插入位置
		size_t hashi = hf(kv.first) % _table.size();
		
		//线性探测找插入位置

		while (_table[hashi]._state == EXIST)
		{
			++hashi;

			hashi %= _table.size();	//如果找到表尾,回到表头继续找
		}

		//插入数据
		_table[hashi]._kv = kv;
		_table[hashi]._state = EXIST;
		++_n;

		return true;
	}

	HashData<const K, V>* Find(const K& key)
	{
		HashFunc hf;
		//线性探测
		size_t hashi = hf(key) % _table.size();

		while (_table[hashi]._state != EMPTY)
		{
			if (_table[hashi]._state == EXIST && _table[hashi]._kv.first == key)
			{
				return (HashData<const K, V>*) & _table[hashi];
			}
			++hashi;
			hashi %= _table.size();	//如果找到表尾,回到表头继续找
		}

		return nullptr;
	}

	bool Erase(const K& key)
	{
		HashData<K, V>* ret = Find(key);

		if (ret)
		{
			ret->_state = DELETE;
			--_n;
			return true;
		}
		return false;
	}

	private:
	vector<HashData<K,V>> _table;
	size_t _n = 10;
	};
}

//开散列的拉链法实现哈希表
namespace hash_bucket
{
	template<class K, class V>
	struct HashNode
	{
		HashNode(const pair<K,V>& kv = pair<K, V>())
			:_kv(kv)
			,_next(nullptr)
		{}

		pair<K, V> _kv;
		HashNode<K, V>* _next;
	};

	template<class K, class V, class HashFunc = DefaultHashFanc<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		HashTable()
		{
			_table.resize(10, nullptr);
			_n = 0;
		}

		~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;
			}
		}


		bool Insert(const pair<K, V>& kv)
		{
			if (Find(kv.first))
			{
				return false;
			}
			HashFunc hf;
			//负载因子到1就扩容
			if (_n == _table.size())
			{
				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 = hf(cur->_kv.first) % newSize;
						cur->_next = newTable[hashi];
						newTable[hashi] = cur;

						cur = next;
					}
				}
				_table.swap(newTable);
			}
			 
			size_t hashi = hf(kv.first) % _table.size();

			//链表头插
			Node* newnode = new Node(kv);

			newnode->_next = _table[hashi];
			_table[hashi] = newnode;

			++_n;
			return true;
		}

		void Print()
		{
			for (size_t i = 0; i < _table.size(); i++)
			{
				printf("[%d]->", i);
				Node* cur = _table[i];
				while (cur)
				{
					cout << cur->_kv.first << ":"<<cur->_kv.second <<"->";
					cur = cur->_next;
				}
				printf("NULL\n");
			}
		}


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

		bool Erase(const K& key)
		{
			HashFunc hf;
			size_t hashi = hf(key) % _table.size();
			Node* prev = nullptr;
			Node* cur = _table[hashi];
			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;
		}

	private:
		vector<Node*> _table;	//指针数组
		size_t _n; 
	};
}

结语

希望这篇哈希表(hash_table)的模拟实现详解能对大家有所帮助,欢迎大佬们留言或私信与我交流.

学海漫浩浩,我亦苦作舟!关注我,大家一起学习,一起进步!

相关文章推荐

【C++】模拟实现红黑树(RB-Tree)

【C++】模拟实现AVL树

【C++】模拟实现二叉搜索(排序)树

【C++】模拟实现priority_queue(优先级队列)

【C++】模拟实现queue

【C++】模拟实现stack

【C++】模拟实现list

【C++】模拟实现vector

【C++】模拟实现string类


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

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

相关文章

高效研究:Zotero的7个插件让你事半功倍

还在为海量文献管理头疼吗?还在为找不到合适的插件犯愁吗?别急,今天我就要带你解锁Zotero的终极武器 - 那些让你爱不释手的必备插件! 作为一个从小白到文献管理达人的过来人,我可以负责任地说:没有这些插件,你的Zotero只能发挥一半功力!安装了这些插件,你的效率绝对能飙升! …

字典树(单词查找树、Trie树)

题目 代码 #include <bits/stdc.h> using namespace std; const int N 1e510; int f[N][26], idx, cnt[N]; void insert(char str[]) {int p 0;for(int i 0; str[i]; i){int u str[i] - a;if(!f[p][u]) f[p][u] idx;p f[p][u];}cnt[p]; } int query(char str[]) …

相亲交友系统的商业模式探讨

在撰写关于相亲交友系统的商业模式探讨时&#xff0c;附带示例代码可能不太合适&#xff0c;因为软文通常是面向非技术读者&#xff0c;讲述商业模式、用户体验等方面的内容。不过&#xff0c;为了满足您的需求&#xff0c;我可以尝试结合一些简单的伪代码&#xff08;模拟代码…

CSS 3D转换

在 CSS 中&#xff0c;除了可以对页面中的元素进行 2D 转换外&#xff0c;您也可以对象元素进行 3D转换&#xff08;将页面看作是一个三维空间来对页面中的元素进行移动、旋转、缩放和倾斜等操作&#xff09;。与 2D 转换相同&#xff0c;3D 转换同样不会影响周围的元素&#x…

Cursor编辑器:10秒生成完美Git提交信息!

Cursor编辑器&#xff1a;10秒生成完美Git提交信息&#xff01; 亲爱的开发者们&#xff0c;是否还在为编写规范的Git提交信息而头疼&#xff1f;今天&#xff0c;就让我们一起揭开Cursor编辑器的神秘面纱&#xff0c;探索如何一键生成专业的Git Commit Message&#xff0c;让…

Android 电源管理各个版本的变动和限制

由于Android设备的电池容量有限&#xff0c;而用户在使用过程中会进行各种高耗电操作&#xff0c;如网络连接、屏幕亮度调节、后台程序运行等&#xff0c;因此需要通过各种省电措施来优化电池使用‌&#xff0c;延长电池续航时间&#xff0c;提高用户体验&#xff0c;并减少因电…

数据结构-八大排序之堆排序

堆排序 1.1 基础知识 原理&#xff1a; 1. 利用完全二叉树构建大顶堆 2. 堆顶元素和堆底元素进行交换&#xff0c;除堆底元素之外其余元素继续构建大顶堆 3. 重复2&#xff0c;直到所有元素都不参与构建 整个数组排序完成 完全二叉树&#xff1a; 数据从上到下&#x…

八大排序--05堆排序

假设数组 arr[] {5,7,4,2,0,3,1,6},请通过插入排序的方式&#xff0c;实现从小到大排列&#xff1a; 方法&#xff1a;①利用完全二叉树构建大顶堆&#xff1b; ②对顶元素和堆底元素进行交换&#xff0c;除堆底元素之外其余元素继续构造大顶堆&#xff1b; ③重复步骤②&…

2k1000LA iso 镜像的制作

问题: 已经有了buildroot ,内核也调试好了,但是没有loongnix 镜像。 首先是网上下载镜像,看看能不能用 首先是现在 网上的 iso 镜像进行烧写测试。 安装 7z 解压软件 进行U盘的烧写。 进行系统安装测试: <

视频剪辑软件推荐电脑版:这5款剪辑软件不容错过!

在视频剪辑领域&#xff0c;选择合适的软件至关重要。不同的软件各有千秋&#xff0c;有的简单易用&#xff0c;适合新手快速上手&#xff1b;有的功能强大&#xff0c;适合专业团队进行深度编辑。以下是一些电脑版视频剪辑软件的推荐&#xff0c;涵盖了从新手到专业级别的不同…

【RAG】HiQA:一种用于多文档问答的层次化上下文增强RAG

前言 文档领域的RAG&#xff0c;之前的工作如ChatPDF等很多的RAG框架&#xff0c;文档数量一旦增加&#xff0c;将导致响应准确性下降&#xff0c;如下图&#xff1b;现有RAG方法在处理具有相似内容&#xff08;在面对大量难以区分的文档时&#xff09;和结构的文档时表现不佳…

人才画像系统是什么?有哪些功能和作用?

人才画像系统是一种先进的人力资源管理工具&#xff0c;它运用大数据和人工智能技术对员工的多方面特征进行深度分析。系统通过汇聚个人的教育背景、工作经验、技能掌握、性格特质及行为数据等信息&#xff0c;结合数据挖掘和机器学习算法&#xff0c;构建出每位员工的数字化“…

openEuler 22.03 (LTS-SP3)上安装mysql8单机版

一、目标 在openEuler 22.03 (LTS-SP3) 上安装 mysql 8.0.23 单机版 二、安装 1、下载二进制包 MySQL :: Download MySQL Community Server (Archived Versions) 下载页面 下载链接 https://downloads.mysql.com/archives/get/p/23/file/mysql-8.0.23-linux-glibc2.12-x86…

新生培训 day1 C语言基础 顺序 分支 循环 数组 字符串 函数

比赛地址 b牛客竞赛_ACM/NOI/CSP/CCPC/ICPC算法编程高难度练习赛_牛客竞赛OJ C语言数据类型 字符 整型数 int 2e9 long long 9e18 浮点数 代码示例 /** Author: Dduo * Date: 2024-10-8* Description: 新生培训day1 */ #include <stdio.h>int main() {// 定义变量in…

【2024.10.8练习】宝石组合

题目描述 题目分析 由于是求最值&#xff0c;原本考虑贪心&#xff0c;但由于算式过于复杂&#xff0c;首先考虑对算式化简。 进行质因数分解&#xff1a; 因此: 不妨设对于每个&#xff0c;&#xff0c;则上式可化简为&#xff1a; 即 用Vene图也可以求出同样结果。 可是以…

DepthB2R靶机打靶记录

一、靶机介绍 下载地址&#xff1a;https://download.vulnhub.com/depth/DepthB2R.ova 二、信息收集 根据靶机主页显示&#xff0c;确认靶机ip为192.168.242.132 端口扫描 nmap -p- -A 192.168.242.132 发现只开放了8080端口 用dirsearch扫个目录 apt-get update apt-get …

胤娲科技:机械臂「叛逃」记——自由游走,再悄然合体

夜深人静&#xff0c;你正沉浸在梦乡的前奏&#xff0c;突然意识到房间的灯还亮着。此刻的你&#xff0c;是否幻想过有一只无形的手&#xff0c;轻盈地飘过&#xff0c;帮你熄灭那盏碍眼的灯&#xff1f; 又或者&#xff0c;你正窝在沙发上&#xff0c;享受电视剧的紧张刺激&am…

RKMEDIA画面质量调节-QP调节

QP是在视频采集编码过程中的量化参数&#xff0c;其值与画面质量成反比&#xff0c;即QP值越大画面质量越小&#xff0c;其具体调整方法如下&#xff1a; typedef struct rkVENC_RC_PARAM_S {RK_U32 u32ThrdI[RC_TEXTURE_THR_SIZE]; // [0, 255]RK_U32 u32ThrdP[RC_TEXTURE_TH…

一致性哈希算法解析

1. 哈希算法 想象我们的网络世界是一个巨大的环形摩天轮&#xff0c;上面有无数的座位&#xff0c;每个座位都代表了一个存储空间。现在&#xff0c;我们需要将三万张照片安排到这个摩天轮的三台机器上。这些机器我们可以想象成三个大车厢&#xff0c;每个车厢可以装载一部分照…

GIS专业的就业前景

地理信息系统&#xff08;GIS&#xff09;作为一门跨学科的领域&#xff0c;随着技术的发展和应用领域的拓宽&#xff0c;其就业前景日益广阔。GIS专业毕业生可以在多个行业中找到合适的职位&#xff0c;并且随着经验的积累&#xff0c;薪资和职业发展空间都相当可观。 1. 就业…