哈希表之开散列的实现

在这里插入图片描述

回顾与引出

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

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

在这里插入图片描述

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

基本结构

  • 成员变量

    哈希表中的每一个元素是通过链表链接起来的,每个链表的头结点与数组的位置关联。因此,你可能会在 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; // 存储了多少个有效数据
	};
}

在这里插入图片描述

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

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

相关文章

秋招如何准备?有什么建议?

秋招&#xff0c;是毕业生最好的求职渠道&#xff0c;没有之一。尽管还有春招&#xff0c;社招......都不如秋招重要&#xff0c;因为秋招的机会更多..... 如何准备秋招&#xff1f; 1、简历很重要 一个好的简历&#xff0c;就是敲门砖&#xff0c;这是你跟企业HR的第一次亲…

如何使用SD-WAN提升物流供应链网络效率

案例背景 本次分享的物流供应链企业是一家国际性的大型企业&#xff0c;专注于提供全球范围内的物流和供应链解决方案。案例用户在不同国家和地区均设有多个分支机构和办公地点&#xff0c;以支持客户需求和业务运营。 在过去&#xff0c;该企业用户使用传统的MPLS网络来连接各…

【grep】从html表格中快速定位某个数据

文章目录 1 背景2 参考知识2.1 grep2.2 HTML基础语言标签 3 解决方案 1 背景 在html中是一堆表格、图片、文字什么的&#xff0c;想从表格中提取关键词为“GJC”后对应的数字&#xff0c;怎么办呢&#xff1f; 逐个打开html文件&#xff0c;“ctrlF”搜一下&#xff0c;然后复…

直线导轨在自动锁螺丝机的作用及注意事项

直线导轨在自动锁螺丝机中具有重要作用&#xff0c;可以提供精确的导向&#xff0c;使滑块能够沿固定轨迹移动&#xff0c;确保螺丝准确无误地进入螺丝孔并被锁定&#xff0c;因此&#xff0c;选择高品质的直线导轨对于自动锁螺丝机的性能和精度至关重要&#xff01;那么&#…

拿下!这些证书可以帮你职场晋升!(PMP/CSPM/NPDP)

PMP证书为项目管理道路打好基础&#xff0c;建立规划思维&#xff0c;整合思维&#xff0c;提高解决问题效率。中国也有自己的项目管理认证CSPM&#xff0c;与PMP相比难度较小&#xff0c;可用已获得的证书免考。NPDP认证拓宽视野&#xff0c;帮助项目经理提升技能。 01PMP为项…

常见树种(贵州省):006栎类

摘要&#xff1a;本专栏树种介绍图片来源于PPBC中国植物图像库&#xff08;下附网址&#xff09;&#xff0c;本文整理仅做交流学习使用&#xff0c;同时便于查找&#xff0c;如有侵权请联系删除。 图片网址&#xff1a;PPBC中国植物图像库——最大的植物分类图片库 一、麻栎 …

【狂神说】CSS3详解

目录 CSS概述什么是CSSCSS发展史快速入门CSS的三种导入方式 2 选择器2.1 基本选择器标签选择器类选择器id选择器 2.2 层次选择器2.3 结构伪类选择器2.4 属性选择器&#xff08;常用&#xff09; 3 美化网页元素3.1 为什么要美化网页3.2 字体样式3.3 文本样式 视频课程见链接&am…

口碑好的猫罐头有哪些?宠物店受欢迎的5款猫罐头推荐!

快到双十二啦&#xff01;铲屎官们是时候给家里猫主子囤猫罐头了。许多铲屎官看大促的各种品牌宣传&#xff0c;看到眼花缭乱&#xff0c;不知道选哪些猫罐头好&#xff0c;胡乱选又怕踩坑。 口碑好的猫罐头有哪些&#xff1f;作为一个经营宠物店7年的老板&#xff0c;活动期间…

c语言编程(模考2)

简答题1 从键盘输入10个数&#xff0c;统计非正数的个数&#xff0c;并且计算非正数的和 #include<stdio.h> int main() {int i,n0,sum0;int a[10];printf("请输入10个数&#xff1a;");for(i0;i<10;i){scanf("%d",&a[i]);}for(i0;i<10…

Android使用Kotlin利用Gson解析多层嵌套Json数据

文章目录 1、依赖2、解析 1、依赖 build.gradle(app)中加入 dependencies { implementation com.google.code.gson:gson:2.8.9 }2、解析 假设这是要解析Json数据 var responseStr "{"code": 200,"message": "操作成功","data&quo…

vue3 iconify 图标几种使用 并加载本地 svg 图标

iconify iconify 与 iconify/vue 使用 下载 pnpm add iconify/vue -D使用 import { Icon } from "iconify/vue";<template><Icon icon"mdi-light:home" style"color: red; font-size: 43px" /><Icon icon"mdi:home-flo…

11.6AOP

一.AOP是什么 是面向切面编程,是对某一类事情的集中处理. 二.解决的问题 三.AOP的组成 四.实现步骤 1.添加依赖(版本要对应): maven仓库链接 2.添加两个注解 3.定义切点 4.通知 5.环绕通知 五.excution表达式 六.AOP原理 1.建立在动态代理的基础上,对方法级别的拦截. 2. …

python实现鼠标实时坐标监测

python实现鼠标实时坐标监测 一、说明 使用了以下技术和库&#xff1a; tkinter&#xff1a;用于创建GUI界面。pyperclip&#xff1a;用于复制文本到剪贴板。pynput.mouse&#xff1a;用于监听鼠标事件&#xff0c;包括移动和点击。threading&#xff1a;用于创建多线程&…

深入浅出 Linux 中的 ARM IOMMU SMMU I

Linux 系统下的 SMMU 介绍 在计算机系统架构中&#xff0c;与传统的用于 CPU 访问内存的管理的 MMU 类似&#xff0c;IOMMU (Input Output Memory Management Unit) 将来自系统 I/O 设备的 DMA 请求传递到系统互连之前&#xff0c;它会先转换请求的地址&#xff0c;并对系统 I…

[MICROSAR Adaptive] --- Persistency

1 persistency 概念介绍 percistency持久化,上一集我们介绍过 从一个应用程序的角度来看 它能使用的API可以分为三类,ara::per的API就属于这里的第二类direct API,只需要编译链接相应的库就可以直接使用了。我们先来了解ara::per的主要功能,ara::per提供持久化存储相关的,…

云流量回溯主要作用是哪些?

云流量回溯&#xff0c;作为网络运营中的一项关键技术&#xff0c;具有重要的作用&#xff0c;为企业提供了更加精准、高效的网络管理手段。本文将探讨云流量回溯的主要作用以及其在网络优化中的关键性。 1. 实时监测与分析&#xff1a;云流量回溯通过实时监测网络流量&#xf…

[Linux] shell脚本之循环

一、循环定义 一组被重复执行的语句称之为 循环体,能否继续重复,决定循环的终止条件。 循环语句 是由循环体及循环的终止条件两部分组成的。 二、for循环 2.1 带列表循环 语法 for 变量名 in 取值列表do 命令序列 done 花括号用法&#xff1a; 花括号{ }和seq在for循环…

虚拟摇杆OnJoystickMove未被调用,角色不移动

更改interaction type 为 event notification

spring-boot-admin-starter-server监控springboot项目

文章目录 场景实现具体操作展示 场景 监控三件套Prometheus、Grafana、Alertmanager 部署起来太复杂,如果公司没有运维而且项目很小就可以使用spring-boot-admin-starter-server替代。这个包使用起来还是很简单的, 下面就实现一个对springCloud项目的监控 实现 参考 项目 具体操…

表格制作软件排行榜,热门做表格的软件推荐

在数字化时代&#xff0c;表格不仅仅是企业管理和数据整理的重要工具&#xff0c;更是学术研究、项目规划以及日常生活中必不可少的一部分。为了更高效地进行表格制作&#xff0c;选择一款优秀的表格制作软件是至关重要的。在众多的软件中&#xff0c;我们特别推荐一款备受好评…