哈希表之闭散列的实现

在这里插入图片描述

闭散列实现哈希表

在闭散列实现哈希表中,我们选择线性探测法来解决哈希冲突。在哈希表的简介部分,我们已经介绍过线性探测法啦!

线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

基本结构定义

在哈希表的简介部分,我们知道闭散列中删除一个元素是通过标记的方式删除的,因此在实现闭散列中我们需要定义一个变量用来标记哈希表中每一个位置的状态。这种情况可以使用枚举(enum)来表示哈希表中每个位置的状态。

enum STATE
{
	EXIST, //表示这个位置已经存放数据了
	EMPTY, //表示这个位置没有存放数据
	DELETE //表示这个位置的数据已经被删除
};

哈希表是 unordered_mapunordered_set 的底层数据结,HashTable 存储的数据依旧是 key-value 的形式,也就是 pair 啦!类比 mapset 就行啦!

template<class K, class V>
struct HashData
{
	pair<K, V> _kv; //存储数据
	STATE _state; //哈希表每一个位置的状态
};

哈希表就是一个数组,只不过具有特定的插入,删除数据的方式。比可以类比priority_queue(堆),他的底层也是一个数组,但是也具有插入删除数据的规则。

template<class K, class V>
class HashTable
{
public:

private:
	vector<HashData<K, V>> _table;
	size_t _n = 0; //哈希表中有效元素的个数
};

这里为什么需要一个 _n 来存储哈希表中有效元素的个数呢?在哈希表的简介部分我们已经知道,闭散列解决哈希冲突的方式可能会发生数据堆叠,降低哈希表的效率。因此,我们需要维护 _n 来管理哈希表的扩容,以此来提高效率。具体的做法会在哈希表的 Insert 函数中讲解。

构造函数

我们可以一开始就给哈希表开一段空间,原因后面再讲。

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

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

在哈希表的简介部分我们了解到很多哈希函数,我们选择比较常见的除留余数法作为实现哈希表的哈希函数。当我们要插入一个新的元素时,我们就拿他的 key 对数组的 size 取模,这样就能得到新元素对应的下标位置,如果这个下标对应的标识符不是 EMPTY 就找下一个位置,直到找到一个标识符为 DELETE 或者 EMPTY 的位置,然后插入元素就可以啦。

不可以对数组的 capacity 取模哦!

在这里插入图片描述

为了方便,哈希表中的数组一般都是 sizecapacity 一样大的,即使不一样大,对 size 取模都不会错。

为了确保 sizecapacity 一样大,在 HashTable 的构造函数调用 vectorresize 函数就可以!

bool Insert(const pair<K, V>& kv)
{
    size_t hashi = kv.first % _table.size();
    //找到一个空位置
    while (_table[hashi]._state != EXIST)
    {
        ++hashi;
        hashi %= _table.size();
    }

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

    return true;
}

于是我们顺利写出了这样的代码,显然这是不够的,因为我们并没有考虑哈希表扩容的问题。那扩容逻辑应该怎么写呢?我们先不着急,来看看哈希表的载荷因子是个什么东东。

哈希表的载荷因子定义为: α \alpha α = 填入表中的元素个数 / 哈希表的长度

α \alpha α 是散列表装满程度的标志因子。由于表长是定值, α \alpha α 与 “填入表中的元素个数” 成正比,所以, α \alpha α 越大,表明填入表中的元素越多,产生冲突的可能性越大,但是空间利用率越大;反之, α \alpha α 越小,表明填入表中的元素越少,产生冲突的可能性就越小,但是空间利用率就变低了。实际上,哈希表的平均查找长度是载荷因子 α \alpha α 的函数,只是不同处理冲突的方法有不同的函数。

对于开放定址法,载荷因子是特别重要的因素,应严格限制在 0.7 - 0.8 以下。超多 0.8 查询时 CPU 缓存不明中按照指数曲线上升。因此,一些采用开放定址法的 hash 库,如 Java 的系统库限制了载荷因子为 0.75,超过这个值就要对哈希表进行扩容处理。

我们实现,选择载荷因子大于等于 0.7 之后扩容哈!假设我们一开始没有给哈希表初始化空间,在扩容判断的时候就比较麻烦,因为一开始哈希表的 size 为 0,就需要特殊判断了! α = n / t a b l e . s i z e ( ) \alpha = n / table.size() α=n/table.size(),n 为哈希表中有效元素的个数,显然 table.size() 作为除数是不能为 0 的,因此需要特殊判断。

在扩容之后,因为 HashTable 的大小发生改变了,原来哈希表中的元素都需要重新映射。

不可以直接 resize(newSize) 就完事儿了,必须重新映射!

在计算载荷因子时有如下计算方法:

1 : n ∗ 10 / t a b l e . s i z e ( ) ≥ 7 1:n * 10 / table.size() \geq 7 1n10/table.size()7

2 : ( d o u b l e ) n / t a b l e . s i z e ( ) ≥ 0.7 2:(double)n / table.size() \geq 0.7 2(double)n/table.size()0.7

看你喜欢哪一种了!

bool Insert(const pair<K, V>& kv)
{
	if (_n * 10 / _table.size() >= 7)
	{
		size_t newSize = _table.size() * 2;
		//遍历旧表,重新映射到新表
		HashTable<K, V> newHT;
		newHT._table.resize(newSize);

		for (int i = 0; i < _table.size(); i++)
		{
			if (_table[i]._state == EXIST)
			{
				newHT.Insert(_table[i]._kv);
			}
		}

		_table.swap(newHT._table);
	}

	size_t hashi = 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)

查找比较简单,根据 key 算出下标,然后向后查找,如果找到和 key 值相等的元素并且该元素的标志位不是 DELETE,返回即可;如果直到找到标志位为空的元素还没找到,那么返回 nullptr 就可以。


HashData<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 (HashData<const K, V>*)&_table[hashi];
		}
		++hashi; //继续到下一个位置寻找
		hashi %= _table.size();
	}
	return nullptr; //哈希表中找不到这个元素
}

bool Erase(const K& key)

这个函数就更简单了,如果说要删除的元素在哈希表中存在的话,将对应的标志位改为 DELETE 就行啦!

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

解决 string 作为 key 的情况

像那些整形,指针啥的都可以直接取模,然后计算他在哈希表中的位置。但是对于字符串呢,他是无法取模的。因此,怎么解决这个问题呢?又得靠我们的仿函数了,当 key 值为 string 类型的时候,通过模板的特化,调用处理 string 的那个 operator() 就可以啦!

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

这是一个对于可以取模的 key 准备的 operator() 我们啥也不需要做,返回传入的 key 就行啦。返回值强转为 size_t 能够解决负数作为 key 值的问题。

模板特化的知识点已经讲过了,忘记了可以复习复习:21天学会C++:Day14----模板_姬如祎的博客-CSDN博客

这里还有一个问题就是,如何让字符串能够对整数取模呢?这里就涉及到字符串的哈希了!字符串的哈希算法有很多,你可以在网上搜索。我们这里选择一个比较常见的字符串哈希算法:BKDR Hash算法。

在这里插入图片描述

模板特化:

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

最终代码:

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

	bool Insert(const pair<K, V>& kv)
	{
		if (_n * 10 / _table.size() >= 7)
		{
			size_t newSize = _table.size() * 2;
			//遍历旧表,重新映射到新表
			HashTable<K, V> newHT;
			newHT._table.resize(newSize);

			for (int 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<const K, V>* ret = Find(key);
		if (ret)
		{
			ret->_state = DELETE;
			--_n;
			return true;
		}
		return false;
	}


private:
	vector<HashData<K, V>> _table;
	size_t _n = 0; //哈希表中有效元素的个数
};

好啦,这就是开散列实现哈希表的全部过程啦!下一讲我们会探索闭散列实现哈希表的方式。

在这里插入图片描述

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

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

相关文章

Carla之语义分割及BoundingBox验证模型

参考&#xff1a; Carla系列——4.Cara模拟器添加语义分割相机&#xff08;Semantic segmentation camera&#xff09; Carla自动驾驶仿真五&#xff1a;opencv绘制运动车辆的boudingbox&#xff08;代码详解&#xff09; Carla官网Bounding Boxes Carla官网创建自定义语义标签…

【离散数学必刷题】谓词逻辑(第二章 左孝凌版)刷完包过!

专栏&#xff1a;离散数学必刷题 本章需要掌握的重要知识&#xff1a; 1.利用谓词表达式表示命题 2.变元的约束 3.谓词公式的定义、谓词公式的赋值 4.谓词公式的翻译&#xff08;注意在全总个体域时使用特性谓词&#xff09; 5.有限论域上量词的消去 6.谓词公式中关于量词的等价…

软件工程——名词解释

适用多种类型的软件工程教材&#xff0c;有关名词释义的总结较为齐全~ 目录 1. 软件 2. 软件危机 3. 软件工程 4. 软件生存周期 5. 软件复用 6. 质量 7. 质量策划 8. 质量改进 9. 质量控制 10. 质量保证 11. 软件质量 12. 正式技术复审 13. ISO 14. ISO9000 15.…

思维模型 暗示效应

本系列文章 主要是 分享 思维模型&#xff0c;涉及各个领域&#xff0c;重在提升认知。无形中引导他人的思想和行为。 1 暗示效应的应用 1.1 暗示效应在商业品牌树立中的应用 可口可乐的品牌形象&#xff1a;可口可乐通过广告、包装和营销活动&#xff0c;向消费者传递了一种…

macOS使用conda初体会

最近在扫盲测序的一些知识 其中需要安装一些软件进行练习&#xff0c;如质控的fastqc&#xff0c;然后需要用conda来配置环境变量和安装软件。记录一下方便后续查阅学习 1.安装miniconda 由于我的电脑之前已经安装了brew&#xff0c;所以我就直接用brew安装了 brew install …

基于STC12C5A60S2系列1T 8051单片机定时器/计数器应用

基于STC12C5A60S2系列1T 8051单片机定时器/计数器应用 STC12C5A60S2系列1T 8051单片机管脚图STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式及配置STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式介绍STC12C5A60S2系列1T 8051单片机定时器/计数器介绍STC12C5A60S2系…

BGP基本配置实验

目录 一、实验拓扑 二、实验需求 三、实验步骤 1、IP地址配置 2、内部OSPF互通&#xff0c;配置OSPF协议 3、BGP建立邻居关系 4、R1和R5上把业务网段宣告进BGP 5、消除路由黑洞&#xff0c;在R2、R4上做路由引入 6、业务网段互通 一、实验拓扑 二、实验需求 1、按照图…

Gold-YOLO:基于收集-分配机制的高效目标检测器

文章目录 摘要1、简介2、相关工作2.1、实时目标检测器2.2、基于Transformer的目标检测2.3、用于目标检测的多尺度特征 3、方法3.1、预备知识3.2、低级收集和分发分支3.3、高阶段收集和分发分支3.4、增强的跨层信息流3.5、遮罩图像建模预训练 4、实验4.1、设置4.2、比较4.3.2、 …

Android14前台服务适配指南

Android14前台服务适配指南 Android 10引入了android:foregroundServiceType属性&#xff0c;用于帮助开发者更有目的地定义前台服务。这个属性在Android 14中被强制要求&#xff0c;必须指定适当的前台服务类型。以下是可选择的前台服务类型&#xff1a; camera: 相机应用。…

Git之分支与版本

&#x1f3ac; 艳艳耶✌️&#xff1a;个人主页 &#x1f525; 个人专栏 &#xff1a;《Spring与Mybatis集成整合》《Vue.js使用》 ⛺️ 越努力 &#xff0c;越幸运。 1.开发测试上线git的使用 1.1. 环境讲述 当软件从开发到正式环境部署的过程中&#xff0c;不同环境的作用…

docker搭建etcd集群

最近用到etcd&#xff0c;就打算用docker搭建一套&#xff0c;学习整理了一下。记录在此&#xff0c;抛砖引玉。 文中的配置、代码见于https://gitee.com/bbjg001/darcy_common/tree/master/docker_compose_etcd 搭建一个单节点 docker run -d --name etcdx \-p 2379:2379 \…

matlab Silink PID 手动调参

&#xff08;业余&#xff09;PID即比例积分微分&#xff0c;它将给定值r(t)与实际输出值y(t)的偏差的比例(P)、积分(I)、微分(D)通过线性组合形成控制量&#xff0c;对被控对象进行控制。我们先用matlab Silink弄一个简易的PID例子&#xff1a; 中间三条就是比例&#xff0c;积…

Django中简单的增删改查

用户列表展示 建立列表 views.py def userlist(request):return render(request,userlist.html) urls.py urlpatterns [path(admin/, admin.site.urls),path(userlist/, views.userlist), ]templates----userlist.html <!DOCTYPE html> <html lang"en">…

大数据可视化数据大屏可视化模板【可视化项目案例-05】

🎉🎊🎉 你的技术旅程将在这里启航! 🚀🚀 本文选自专栏:可视化技术专栏100例 可视化技术专栏100例,包括但不限于大屏可视化、图表可视化等等。订阅专栏用户在文章底部可下载对应案例源码以供大家深入的学习研究。 🎓 每一个案例都会提供完整代码和详细的讲解,不…

matlab直线一级倒立摆lqr控制

1、内容简介 略 16-可以交流、咨询、答疑 matlab直线一级倒立摆lqr控制 2、内容说明 倒立摆是一个开环不稳定的强非线性系统&#xff0c;其控制策略与杂技运动员顶杆平衡表演的技巧有异曲同工之处&#xff0c;目的在于使得摆杆处于临界稳定状态&#xff0c;是进行控制理论研…

MYSQL字符串函数详解和实战(字符串函数大全,内含示例)

MySQL提供了许多字符串函数&#xff0c;用于处理和操作字符串数据。以下是一些常用的MYSQL字符串函数。 建议收藏以备后续用到查阅参考。 目录 一、CONCAT 拼接字符串 二、CONCAT_WS 拼接字符串 三、SUBSTR 取子字符串 四、SUBSTRING 取子字符串 五、SUBSTRING_INDEX 取子…

Linux 程序开发流程 / 基本开发工具 / Vim / GCC工具链 / Make 工具 / Makefile 模板

编辑整理 by Staok。 本文部分内容摘自 “100ask imx6ull” 开发板的配套资料&#xff08;如 百问网的《嵌入式Linux应用开发完全手册》&#xff0c;在 百问网 imx6ull pro 开发板 页面 中的《2.1 100ASK_IMX6ULL_PRO&#xff1a;开发板资料》或《2.2 全系列Linux教程&#xf…

文生图模型测评之HPS v2

文章目录 1. 简介2. HPD v22.1 相关数据集介绍2.2 HPD v2 的构建2.2.1 prompt collection2.2.2 image collection2.2.3 preference annotation3. Human Preference Score v23.1 构建模型3.2 实验结果4. 结论及局限性论文链接:Human Preference Score v2: A Solid Benchmark fo…

Java通过JNI技术调用C++动态链接库的helloword测试

JNI调用原理 原理就不细说了&#xff0c;其实就是写个库给Java调&#xff0c;可以百度一下Java JNI&#xff0c;下面是HelloWorld代码测试 编写一个本地测试类 package com.my.study.cpp_jni;/*** 测试Java调用C库* <p>使用命令javac -h . NativeTest.java自动生成C头…

Technology Strategy Patterns 学习笔记8- Communicating the Strategy-Decks(ppt模板)

1 Ghost Deck/Blank Deck 1.1 It’s a special way of making an initial deck that has a certain purpose 1.2 you’re making sure you have figured out what all the important shots are before incurring the major expense of shooting them 1.3 需要从技术、战略、产…