哈希表之开散列的实现

news2025/1/19 8:22:47

在这里插入图片描述

回顾与引出

我们在上一节用闭散列的开放定址法实现了哈希表。不难看出这种方法有明显的缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同 关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。

为了解决数据堆积导致寻找位置的次数增加造成的效率下降,我们这里讲解实现哈希表的另一种方法:拉链法(哈希桶)。

在这里插入图片描述

数组中的每一个位置不再存储有效的数据,而是存储一个指针,指向一个有效的元素。如果冲突的元素很多,结果就是哈希桶很长。哈希桶太长查找效率同样也会下降,因此在用这种方式实现哈希表的时候,也会维护指针数组的扩容逻辑,以保证哈希表的整体效率。

基本结构

  • 成员变量

    哈希表中的每一个元素是通过链表链接起来的,每个链表的头结点与数组的位置关联。因此,你可能会在 HashTable z中使用 vector<list> 作为类的成员来维护哈希表。这样做完全没有任何问题,但是对于哈希表来说还是有点冗余了,因为拉链法(哈希桶)中的链表只是单链表,在插入一个新元素的时候头插就可以了。但是 list 中有 prev 指针,用 list 效率反而会下降。

    size_t _n 用来记录哈希表中存储了多少个有效的数据,这与哈希表的扩容逻辑相关。类比开散列实现哈希表的 size_t _n

  • 哈希表节点

    节点存储的数据是一个 key-value 的结构,因为后面我们要用哈希表封装 unordered_mapunordered_set 先这么写。

    节点中需要包含一个节点的指针,用来指向单链表中的下一个节点。

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
	{
	public:
		typedef HashNode<K, V> Node;

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

构造函数

在构造函数中,我们需要给哈希表申请一些空间,并初始化为 nullptrnullptr 表示数组中的这个位置没有存储元素。当然在构造函数中,你也可以不给哈希表申请空间,只不过在 Insert 函数中就必须特殊判断 _tablesize 是否为 0。这么对比下来,还是在构造函数中给哈希表提前申请一些空间比较香。

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

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

插入元素之前,我们需要通过哈希函数找到新插入元素应该插入到数组的哪一个位置。哈希函数还是选择除留余数法,用 kv.first % _table.size() 找到新元素在数组中的位置之后。我们就要将新元素插入到单链表中:那我们应该是头插还是尾插呢?毫无疑问当然是头插哈。尾插需要找尾,效率比较低,就算维护尾节点的指针,也会有空间消耗,不如头插来得快。

size_t hashi = kv.first % _table.size();
//头插
Node* newNode = new Node(kv);
newNode->_next = _table[hashi];
_table[hashi] = newNode;

上面就是头插的核心代码啦!如果你不理解,可以尝试举一个例子:

在这里插入图片描述

如果我们只管向哈希表中插入数据,不扩容,某些桶越来越长,效率就得不到保障。想闭闭散列实现的哈希表,开散列的哈希表可以将负载因子适当放大,我们就取 1 吧。平均下来每个桶一个数据。查找的效率就是接近 O(1) 啦!扩容之后,每个元素映射到数组中的下标也可能会改变,因此扩容还有降低桶长度的效果!

扩容之后原来的数据怎么映射到新的哈希表中呢?你可能会想到复用 Insert 函数。和闭散列实现的哈希表不同,开散列的 Insert 函数中会在堆区申请节点。如果直接复用 Insert 接口,就需要释放原来的节点,重新开辟新的节点插入到新的哈希表中。仔细想想,我们既然已经有原哈希表中的节点了,为何不直接将他链接到新的哈希表中呢?既不需要释放节点,也不需要开辟新的节点。

bool Insert(const pair<K, V>& kv)
{
	if (_n >= _table.size())
	{
		size_t newSize = _table.size() * 2;
		HashTable<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 % newSize;
				cur->_next = newTable[i];
				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;
}

将旧表的节点插入到新的哈希表:首先遍历旧表,如果某个下标的值不为 nullptr 说明这个下标存储了有效数据。然后遍历该下标对应的哈希桶,计算新的 hashi 之后映射到新的哈希表即可。

Node* Find(const K& key)

这个函数用于在哈希表中查找一个元素。在开散列实现的哈希表中,就相当于在链表中查找一个元素。比较简单!

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

这就是 Find 函数的实现,我们这里实现的哈希表是不允许插入两个相同的元素的。因此,你还需要在 Insert 函数的开头加上判断,使得 Insert 不能插入两个相同的元素。

在这里插入图片描述

bool Erase(const K& key)

Erase 函数用来在哈希表中除值为 key 的元素。首先我们需要根据除留余数法找到 key 对应的下标。然后,在这个下标的位置进行一个单链表的删除就行啦!初始化 prev 指针为 nullptr,用 cur 指针遍历哈希桶(单链表),如果说,cur 节点存储的 key 值就是待删除的元素,令 prevnext 指向 curnext 就行啦。最后记得释放掉要删除的节点哦!

我们来看一个特殊的例子:如果要删除的节点就是单链表的头结点,如下图中的 4 。此时 prevnullptr 在用 prev->next 链接 cur->next 就会发生空指针的解引用。因此需要特殊判断一下,删除的是不是单链表(哈希桶)的头结点。

在这里插入图片描述

bool Erase(const K& key)
{
    size_t hashi = 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;
}

析构函数

我们实现的哈希表是开散列实现的,存储数据的节点都是在堆上申请的。不同于闭散列实现的哈希表,开散列实现的哈希表需要我们自己写析构函数。

析构函数的写法:遍历 _table 数组,将每个哈希桶(单链表)逐一释放即可!

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

支持字符串作为哈希表存储的数据

解决方法与开散列实现的哈希表是一样的。因为字符串是不能直接对整数取模的,因此我们需要通过字符串的哈希算法将字符串转换为整数。然后对字符串转换出来的整形取模就可以啦!关于哈希函数可以自行百度搜索。

key 值,我们用仿函数套一层,这样当传入 string 类型的时候,就能通过调用仿函数获得正确的 key 值了!

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

template<>
struct DefaultHashFunc<string>
{
	size_t operator()(const string& str)
	{
		// BKDR
		size_t hash = 0;
		for (auto ch : str)
		{
			hash *= 131;
			hash += ch;
		}

		return hash;
	}
};

DefaultHashFunc 这个类中,我们重载了圆括号运算符。对于那些可以进行取模运算的类型,我们直接将 key 返回即可,对于不能进行取模运算的 string 通过模板的特化,进行特殊处理即可。这里使用到的字符串哈希算法是:BKDR 哈希算法。是一种比较常用的字符串哈希算法。通过哈希算法,将字符串转化为可以取模的整数。

部分修改

既然要同时适配 string 作为哈希表的存储类型,在之前实现哈希表的代码中,就不能直接对 key 进行取模啦!而是要传入 key 值,通过仿函数来获取正确的 key 值。

完整代码:

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 HashFunc = DefaultHashFunc<K>>
	class HashTable
	{
		typedef HashNode<K, V> Node;
	public:
		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;
			}
		}

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

			HashFunc hf;

			// 负载因子到1就扩容
			if (_n == _table.size())
			{
				// 16:03继续
				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[i] = nullptr;
				}

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

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

		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");
			}
			cout << endl;
		}

	private:
		vector<Node*> _table; // 指针数组
		size_t _n = 0; // 存储了多少个有效数据
	};
}

在这里插入图片描述

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

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

相关文章

【Mysql】[Err] 1293 - Incorrect table definition;

基本情况 SQL文件描述 /* Navicat MySQL Data TransferSource Server : cm4生产-200 Source Server Version : 50725 Source Host : 192.168.1.200:3306 Source Database : db_wmsTarget Server Type : MYSQL Target Server Version : 50725 File…

4G5G智能执法记录仪在保险公司车辆保险远程定损中的应用

4G智能执法记录仪&#xff1a;汽车保险定损的**利器 随着科技的不断进步&#xff0c;越来越多的智能设备应用到日常生活中。而在车辆保险定损领域&#xff0c;4G智能执法记录仪的出现无疑是一大**。它不仅可以实现远程定损&#xff0c;还能实现可视化操作、打印保单以及数据融…

10年经验之谈 —— 如何做接口测试呢?接口测试有哪些工具?

回想入职测试已经10年时间了&#xff0c;初入职场的我对于接口测试茫然不知。后来因为业务需要&#xff0c;开始慢慢接触接口测试。从最开始使用工具进行接口测试到编写代码实现接口自动化&#xff0c;到最后的测试平台开发。回想这一路走来感触颇深&#xff0c;因此为了避免打…

几个西门子PLC常见通讯问题的解决方法

1台200SMART 如何控制2台步进电机&#xff1f; S7-200SMART CPU最多可输出3路高速脉冲&#xff08;除ST20外&#xff09;&#xff0c;这意味着可同时控制最多3个步进电机&#xff0c;通过运动向导可配置相应的运动控制子程序&#xff0c;然后通过调用子程序编程可实现对步进电…

error: ‘ui/ui_uimainwindow.h‘ file not found

问题&#xff1a;在刚好创建的Qt Designer Form Class类中&#xff0c;发现类的.cpp文件中有ui头文件未找到 原因&#xff1a;.ui文件没有被识别到&#xff0c;或者.ui文件不存在&#xff0c;导致ui头文件未创建而报错。 解决&#xff1a;若修改了.ui文件&#xff0c;随手ctrls…

ILI9225 TFT显示屏16位并口方式驱动

所用屏及资料如后图&#xff1a; ILI9225&#xff0c;176*220&#xff0c;8位或16位并口屏&#xff0c;IM0接GND&#xff0c;电源及背光接3.3v 主控&#xff1a;CH32V307驱动&#xff08;库文件和STM32基本一样&#xff09; 一、源码 ILI9225.c #include "ILI9225.h&quo…

innoDB的缓冲池(Buffer Pool)的工作原理

数据存在磁盘了&#xff0c;总不能次次和磁盘交互吧&#xff0c;所以innoDB有一个缓冲池&#xff08;Buffer Pool&#xff09;&#xff0c;有了缓冲池后&#xff0c;读写就优先在缓冲池了。读先在缓冲池读&#xff0c;没有再去磁盘加载进缓冲池&#xff1b;写也是先写缓冲池&am…

学习MySQL先有全局观,细说其发展历程及特点

学习MySQL先有全局观&#xff0c;细说其发展历程及特点 一、枝繁叶茂的MySQL家族1. 发展历程2. 分支版本 二、特点分析1. 常用数据库2. 选型角度及场景 三、三大组成部分四、总结 相信很多同学在接触编程之初&#xff0c;就接触过数据库&#xff0c;而对于其中关系型数据库中的…

云备份——初步认识及环境搭建

文章目录 整体功能简介云备份功能实现目标服务器程序负责功能细分服务端模块划分客户端功能细分客户端模块划分 环境搭建gcc安装 jsoncppbundle库 与 httplib库安装 整体功能简介 云备份功能 自动将本地计算机上指定文件夹中需要备份的文件上传备份到服务器中 并且能够通过浏…

__int128类型movaps指令crash

结论 在使用__int128时&#xff0c;如果__int128类型的内存起始地址不是按16字节对齐的话&#xff0c;有些汇编指令会抛出SIGSEGV使程序crash。 malloc在64位系统中申请的内存地址&#xff0c;是按16字节对齐的&#xff0c;但一般使用时经常会申请一块内存自己切割使用&#…

小程序泄露腾讯地图apikey

今天挖小程序时测了很久&#xff0c;一直没有头绪&#xff0c;后来想要测试一下支付漏洞&#xff0c;但是这里却出问题了 添加地址时我发现&#xff0c;当我添加一个地址时&#xff0c;他会显示腾讯地图的logo和一部分小图&#xff0c;那时候我就在想&#xff0c;既然这里可以调…

独立版求职招聘平台小程序开发

小程序招聘系统开发 我们开发了一款高效、便捷的互联网招聘平台。在这里&#xff0c;可以轻松实现企业入驻、职位发布、在线求职、精准匹配职位和人才&#xff0c;以及参与招聘会等功能。目标是为求职者和企业搭建一个连接彼此的桥梁&#xff0c;帮助您更快地找到满意的工作&…

高斯Filter 和 Bilateral Filter

参考链接&#xff1a; Python | Bilateral Filtering - GeeksforGeeks 高斯Filter&#xff1a; 高斯模糊后的图像中的每个像素的强度是由它周围的像素的加权平均得到的&#xff0c;这个权重就是高斯函数的值&#xff0c;它取决于像素之间的距离。具体来说&#xff1a; 通常会导…

论文阅读 Forecasting at Scale (二)

最近在看时间序列的文章&#xff0c;回顾下经典 论文地址 项目地址 Forecasting at Scale 3.2、季节性 3.3、假日和活动事件3.4、模型拟合3.5、分析师参与的循环建模4、自动化预测评估4.1、使用基线预测4.2、建模预测准确性4.3、模拟历史预测4.4、识别大的预测误差 5、结论6、致…

软件系统运维方案

1.项目情况 2.服务简述 2.1服务内容 2.2服务方式 2.3服务要求 2.4服务流程 2.5工作流程 2.6业务关系 2.7培训 3.资源提供 3.1项目组成员 3.2服务保障 点击获取所有软件开发资料&#xff1a;点我获取

使用paddleocr进行OCR文字识别

1 OCR介绍 OCR&#xff08;Optical Character Recognition&#xff09;即光学字符识别&#xff0c;是一种将不同类型的文档&#xff08;如扫描的纸质文件、PDF文件或图像文件中的文本&#xff09;转换成可编辑和可搜索的数据的技术。OCR技术能够识别和转换印刷或手写文字&…

商务俄语学习,柯桥基础入门教学,千万别小看俄语中的“что”

1、что до (чего) 至于 例&#xff1a; что до меня, то я не могу согласиться 至于我&#xff0c;我不能同意。 А что до зимовки... Ты приедешь в этом году? 说到冬天和过冬…你今年回来吗…

Leetcode—13.罗马数字转整数【简单】

2023每日刷题&#xff08;三十七&#xff09; Leetcode—13.罗马数字转整数 算法思想 当前位置的元素比下个位置的元素小&#xff0c;就减去当前值&#xff0c;否则加上当前值 实现代码 int getValue(char c) {switch(c) {case I:return 1;case V:return 5;case X:return 1…

DDoS攻击频发,科普防御DDoS攻击的几大有效方法

谈到目前最凶猛、频率高&#xff0c;且令人深恶痛绝的网络攻击&#xff0c;DDoS攻击无疑能在榜上占有一席之地。各种规模的企业报包括组织机构都可能受到影响&#xff0c;它能使企业宕机数小时以上&#xff0c;给整个互联网造成无数损失。可以说&#xff0c;怎样防御DDoS攻击是…

算法刷题-动态规划2

算法刷题-动态规划2 珠宝的最高价值下降路径最小和 珠宝的最高价值 题目 大佬思路 多开一行使得代码更加的简洁 移动到右侧和下侧 dp[ i ][ j ]有两种情况&#xff1a; 第一种是从上面来的礼物最大价值&#xff1a;dp[ i ][ j ] dp[ i - 1 ][ j ] g[ i ][ j ] 第二种是从左…