【C++/STL】:哈希 -- 线性探测哈希桶

news2024/9/20 11:30:36

目录

  • 💡前言
  • 一,unordered系列容器
  • 二,哈希
    • 2.1 哈希的概念
    • 2.2 哈希函数
    • 2.3 哈希冲突
  • 三,哈希冲突解决(重点)
    • 3.1 开放定址法
    • 3.2 哈希桶(重点)
  • 四,线性探测的实现
    • 4.1 线性探测的基本框架
    • 4.2 插入操作
    • 4.3 查找操作
    • 4.4 删除操作
  • 五,哈希桶的实现(重点)
    • 5.1 哈希桶的基本框架
    • 5.2 插入操作
    • 5.3 查找操作
    • 5.4 删除操作
  • 六,优化思考

点击跳转至文章: 【C++/STL】:set和map的介绍及基本使用

💡前言

在C++98中,STL提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 l o g 2 N log_2 N log2N,即最差情况下需要比较红黑树的高度次。

在C++11中,STL又提供了4个unordered系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是其底层结构不同,unordered系列的底层是哈希表

本篇文章的内容是学习unordered系列的容器,重点学习什么是哈希,知道什么是哈希冲突,并且掌握解决哈希冲突的两种常用方法:线性探测&哈希桶。

一,unordered系列容器

unordered系列容器有4个:
(1) unordered_map
(2) unordered_set
(3) unordered_multimap
(4) unordered_multiset

它们与 map/set 系列容器的核心功能的重叠度90%,使用方法基本类似,这里不再重复演示。最主要的区别是:遍历unordered系列容器中存储的数据是无序的,并且就性能而言,unordered系列容器在插入,查找,删除方面时间复杂度是 O(1)因此在平时,我们更加推荐使用unordered系列容器

二,哈希

2.1 哈希的概念

构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与元素之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素

该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)

2.2 哈希函数

这里介绍两种常见的哈希函数:直接定址法除留余数法

(1) 直接定址法:用 key 值直接在哈希表中映射一个绝对位置或相对位置
优点:快,无冲突
缺点:要事先知道 key 值的分步情况,并且适用于范围集中的数据

(2) 除留余数法:用 key 值模(%)表的大小 N,利用得到的余数(0 ~ N-1)把 key 值映射到表中的位置

这种方法适用任何类型的数据(一些非整形类型可以通过某种方法转换),但是这也会导致一个不可避免的问题:产生哈希冲突

2.3 哈希冲突

不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞

如果用上面的除留余数法来通俗的解释,就是不同的 key 值,取模后映射到了相同的位置

三,哈希冲突解决(重点)

解决哈希冲突两种常见的方法是:开放地址法哈希桶

3.1 开放定址法

当发生哈希冲突时(自己的位置被占了),如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把 key 值存放到冲突位置中的"下一个" 空位置中去

那如何寻找下一个空位置呢?
(1) 线性探测:从发生冲突的位置开始,按 +1,+2,+3……依次向后探测,直到寻找到下一个空位置为止

比如下表中现在需要插入元素44,先通过哈希函数计算出映射位置为4,但是4位置被占,所以要从这个位置开始,依次向后探测,找到8空位置。

在这里插入图片描述

线性探测优点:实现非常简单,
线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同 key 值占据了可利用的空位置,使得寻找某 key 值的位置需要许多次比较,导致搜索效率降低。如何缓解呢?

(2) 二次探测:从发生冲突的位置开始,按 +1 ^ 2 , + 2 ^ 2,+3 ^ 2……依次向后探测,直到寻找到下一个空位置为止。

3.2 哈希桶(重点)

哈希桶也叫拉链法,首先对 key 值用哈希函数计算出位置,具有相同位置的 key 值归于同一集合,每一个集合称为一个桶各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中

unordered系列的底层哈希表解决哈希冲突的结构就是哈希桶

从下图可以看出,哈希表中每个桶中放的都是发生哈希冲突的元素

在这里插入图片描述

四,线性探测的实现

4.1 线性探测的基本框架

哈希表中的每一个位置给一个状态标记。这样可以避免值存在但是由于冲突位置删除后置空导致查找不到的问题

//枚举三种状态
enum State
{
	EMPTY,  //空
	EXIST,  //存在元素
	DELETE  //元素删除
};

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

template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:
HashTable()
{
	_tables.resize(10); //初始化10个空间
}

	//插入,查找,删除等功能……

private:
	vector<HashData<K, V>> _tables;
	size_t _n = 0;  //表中的数据个数
};

4.2 插入操作

(1) 计算出映射下标,这里用除留余数法要除以数据的个数size,而不是容量的大小capacity,因为要用下标访问,如果除以容量用下标访问会越界。
(2) 插入时发生哈希冲突,用线性探测往后找空位置。
(3) 扩容负载因子:表中的元素个数 / 表的长度 ,一般控制在0.7~0.8
(4) 去重操作,插入时可以复用Find进行判断,值存在,插入失败。

bool Insert(const pair<K, V>& kv)
{
	//去冗余,值存在,插入失败
	if (Find(kv.first))
		return false;

	// 扩容
	//负载因子:表中的元素个数 / 表的长度 一般控制在0.7~0.8
	if (_n * 10 / _tables.size() >= 7)
	{
		HashTable<K, V, Hash> newHT;

		//不要在原表扩容,会破坏映射关系
		//_tables.resize(_tables.size() * 2); //err

		//重开一个新的哈希表
		newHT._tables.resize(_tables.size() * 2);

		//遍历旧表,复用Insert,(可避免重复写映射下标的逻辑)
		// 把旧表中存在的数据插入新表
		for (size_t i = 0; i < _tables.size(); i++)
		{
			if (_tables[i]._state == EXIST)
				newHT.Insert(_tables[i]._kv);
		}

		//交换
		_tables.swap(newHT._tables);
	}

	//计算出映射下标
	//这里用除留余数法。要除以数据的个数size,而不是容量的大小capacity
	//因为要用下标访问,如果除以容量用下标访问会越界
	Hash hs;
	size_t hashi = hs(kv.first) % _tables.size();

	//哈希冲突,线性探测继续找下一个位置
	while (_tables[hashi]._state == EXIST)
	{
		hashi++;
		hashi %= _tables.size(); //解决回绕
	}

	_tables[hashi]._kv = kv;
	_tables[hashi]._state = EXIST;
	_n++;

	return true;
}

4.3 查找操作

算出映射下标确定查找起点,再继续往后找,值相等且为存在状态就找到了,避免删除后还能找到

HashData<K, V>* Find(const K& key)
{
	Hash hs;
	size_t hashi = hs(key) % _tables.size();

	while (_tables[hashi]._state != EMPTY)
	{
		//值相等且为存在状态,避免删除后还能找到
		if (_tables[hashi]._kv.first == key
			&& _tables[hashi]._state == EXIST)
		{
			return &_tables[hashi];
		}

		hashi++;
		hashi %= _tables.size(); //解决回绕
	}

	return nullptr;
}

4.4 删除操作

复用Find,值存在就删除

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

五,哈希桶的实现(重点)

5.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 Hash = HashFunc<K>>
class HashBucket
{
	typedef HashNode<K, V> Node;
public:
	HashBucket()
	{
		_tables.resize(10, nullptr); //初始化10个空间
	}

	//插入,删除,查找等功能……

private:
	vector<Node*> _tables;  // 指针数组
	size_t _n = 0;          // 表中储存的数据个数
};

5.2 插入操作

(1) 核心操作就是链表的头插

(2) 扩容:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容

在这里插入图片描述

bool Insert(const pair<K, V>& kv)
{
	//去冗余,值存在,插入失败
	if (Find(kv.first))
		return false;
		
	//负载因子==1,扩容

	//方式1
	//if(_n == _tables.size())
	//{
	//	HashBucket<K, V> newHB;
	//	newHB._tables.resize(_tables.size() * 2);

	//	//遍历旧表,复用Insert,(可避免重复写映射下标的逻辑)
	//	// 把旧表中存在的数据插入新表
	//	for (size_t i = 0; i < _tables.size(); i++)
	//	{
	//		Node* cur = _tables[i];
	//		if (cur)
	//		{
	//			newHB.Insert(cur->_kv);
	//			cur = cur->_next;
	//		}
	//	}
	//	_tables.swap(newHB._tables);
	//}

	
	//方式1的扩容逻辑消耗的空间太多,假设有一万个节点,就要重新new一万个
	//又要销毁一万个,效率不高
	
	//方式2
	// 是在扩容后也重新利用旧表中的旧节点,把旧节点挪动到新表中
	Hash hs;
	if (_n == _tables.size())
	{
		vector<Node*> newtables(_tables.size() * 2, nullptr);
		for (size_t i = 0; i < _tables.size(); i++)
		{
			Node* cur = _tables[i];
			while (cur)
			{
				Node* next = cur->_next;

				//旧节点挪到新表的下标映射
				size_t hashi = hs(cur->_kv.first) % newtables.size();
				
				//头插到新表
				cur->_next = newtables[hashi];
				newtables[hashi] = cur;

				cur = next;
			}
			_tables[i] = nullptr;
		}
		_tables.swap(newtables);
	}

	size_t hashi = hs(kv.first) % _tables.size(); //计算映射下标

	//头插
	Node* newnode = new Node(kv);
	newnode->_next = _tables[hashi];
	_tables[hashi] = newnode;
	_n++;

	return true;
}

5.3 查找操作

核心操作就是链表的查找

Node* Find(const K& key)
{
	Hash hs;
	size_t hashi = hs(key) % _tables.size(); //计算映射下标
	Node* cur = _tables[hashi];
	while (cur)
	{
		if (cur->_kv.first == key)
		{
			return cur;
		}
		cur = cur->_next;
	}
	return nullptr;
}

5.4 删除操作

核心操作就是链表的删除
在这里插入图片描述

bool Erase(const K& key)
{
	Hash hs;
	size_t hashi = hs(key) % _tables.size(); //计算映射下标
	Node* cur = _tables[hashi];
	Node* prev = nullptr;

	while (cur)
	{
		if (cur->_kv.first == key)
		{
			//第一个节点
			if (prev == nullptr)
				_tables[hashi] = cur->_next;
			else
				//中间节点
				prev->_next = cur->_next;

			delete cur;
			_n--;
			return true;
		}
		//没找到,继续向下走
		prev = cur;
		cur = cur->_next;
	}
	return false;
}

六,优化思考

只能存储key为整形的元素,其他类型怎么解决?

需要支持转整形的仿函数:
(1) 当key为浮点数,负数,指针等可以直接强转为无符号整形的类型
(2) 当key为 string 类(频繁使用)

//当key为浮点数,负数,指针等时,强转成无符号整形,再进行映射
template<class K>
struct HashFunc
{
	size_t operator()(const K& key)
	{
		return (size_t)key;
	}
};

//用原模板对 string 进行特化
template<>
struct HashFunc<string>
{
	size_t operator()(const string& key)
	{
		size_t n = 0;

		for (auto& ch : key)
		{
			n *= 31; //尽量减少冲突
			n += ch;
		}
		return n;
	}
};

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

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

相关文章

【C++】类和对象——Lesson1

Hi~&#xff01;这里是奋斗的小羊&#xff0c;很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~~ &#x1f4a5;&#x1f4a5;个人主页&#xff1a;奋斗的小羊 &#x1f4a5;&#x1f4a5;所属专栏&#xff1a;C &#x1f680;本系列文章为个人学习笔记…

【Golang 面试 - 基础题】每日 5 题(十)

✍个人博客&#xff1a;Pandaconda-CSDN博客 &#x1f4e3;专栏地址&#xff1a;http://t.csdnimg.cn/UWz06 &#x1f4da;专栏简介&#xff1a;在这个专栏中&#xff0c;我将会分享 Golang 面试中常见的面试题给大家~ ❤️如果有收获的话&#xff0c;欢迎点赞&#x1f44d;收藏…

关联映射和缓存机制学习笔记

学习视频&#xff1a;4001 关联映射概述_哔哩哔哩_bilibili~4007 案例&#xff1a;商品的类别_哔哩哔哩_bilibili 目录 1.关联映射概述 1.1关联映射关系 一对一关系 一对多关系 多对多关系 Java对象如何描述事物之间的关系 1.2一对一查询 元素 a.嵌套查询方式 b.嵌套结果方…

Spring Cache常用注解

依赖代码如下&#xff1a; <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId></dependency> 常用注解详解 1. Cacheable 作用&#xff1a;主要用于配置方法&#xff0c;使其…

第九届全球渲染大赛来了!CG爱好者准备好了吗!

在CG界的日历上&#xff0c;二月和八月总是特别繁忙的月份。这两个月&#xff0c;全球CG艺术界最盛大的赛事——全球渲染大赛&#xff0c;都会开放报名&#xff0c;吸引着世界各地的CG艺术家和爱好者参与。备受期待的第九届全球渲染大赛&#xff0c;已经定于2024年8月3日在美国…

微信私域运营工具分享

解决微信多管理难的问题&#xff0c;多微信工作重复做&#xff0c;效率低的问题&#xff0c;防止飞单、删除客户&#xff0c;解决私域运营的难题

在双碳目标下,如何实现工厂的数字化改造升级

在"双碳"目标下&#xff0c;如何实现工厂的数字化改造升级 在“双碳”目标&#xff0c;即2030年前实现碳达峰、2060年前实现碳中和的宏伟蓝图下&#xff0c;企业作为经济社会活动的主体&#xff0c;其改造升级不仅是响应国家战略的必然要求&#xff0c;也是实现可持…

软件压力测试知识大揭秘,专业软件测评公司推荐

在信息技术迅猛发展的今天&#xff0c;软件已经成为各个行业运作的核心。有助于提升工作效率和管理水平的&#xff0c;软件的稳定性和性能也变得尤为重要。而软件压力测试&#xff0c;作为一种重要的测试手段&#xff0c;逐渐受到了更多企业的重视。 软件压力测试&#xff0c;…

【ROS 最简单教程 001/300】ROS 概念介绍

ROS&#xff1a;Robot Operating System 【适用于机器人的开源元操作系统】 ROS Plumbing Tools Capabilities Ecosystem 通讯 Plumbing ⭐ 实现ROS不同节点之间的交互工具 Tools ⭐ 工具软件包 (ROS中的开发和调试工具)&#xff0c;提供 仿真 功能&#xff1b;功能 Capabi…

图文好物和无人直播实操:定位/涨粉/养号/橱窗/作品/制作/剪辑/开播/等等

1.前言 各位小伙伴大家好&#xff0c;这里是天夏共创&#xff0c;免费分享副业/创业精品项目资源&#xff0c;打破互联网创业/副业信息壁垒&#xff0c;和您一起共享副业/创业项目资源&#xff0c;开启智能化创业/副业新时代&#xff01;致力于每天免费分享全网互联网精品VIP项…

React类组件生命周期与this关键字

类组件生命周期 参考链接 一图胜千言&#xff08;不常用的生命周期函数已隐藏&#xff09; 代码&#xff1a; //CC1.js import { Component } from "react";export default class CC1 extends Component {constructor(props) {super(props);console.log("con…

基于IDEA+Mysql+SpringBoot开发的社区养老服务管理系统

基于IDEAMysqlSpringBoot开发的社区养老服务管理系统 项目介绍&#x1f481;&#x1f3fb; node -version 14.21.3 在当前社会老龄化趋势日益加剧的背景下&#xff0c;构建一个高效、便捷的社区网养老服务管理系统显得尤为重要。本项目基于Spring Boot框架开发&#xff0c;旨…

网站打不开怎么办,收藏以备不时之需

DNS设置示范教程 部分地区有使用移动网络的小伙伴们吐槽无法访问部分网站的情况&#xff0c;同样的网站&#xff0c;使用电信和联通的用户就能正常访问。 这其实有很大几率是由于运营商的网络问题导致的&#xff0c;容易出现网站打不开的结果。 要解决移动网络无法访问的情况…

【React Hooks原理 - useTransition】

概述 在上一篇中我们介绍了useDeferredValue的基本原理&#xff0c;本文主要介绍一下useTransition这个Hook&#xff0c;之所以在这里提到useDeferredValue&#xff0c;是因为这两个Hook都是在React18引入的进行渲染优化的Hooks&#xff0c;在某些功能上是重叠的&#xff0c;主…

YOLO入门教程(一)——训练自己的模型【含教程源码 + 故障排查】

目录 引言前期准备Step1 打标训练Step2 格式转换Step3 整理训练集Step4 训练数据集4.1创建yaml文件4.2训练4.3故障排查4.3.1OpenCV版本故障&#xff0c;把OpenCV版本升级到4.0以上4.3.2NumPy版本故障&#xff0c;把NumPy降低版本到1.26.44.3.3没有安装ultralytics模块4.3.4Aria…

自闭症儿童上学指南:帮助孩子适应校园生活

在自闭症儿童成长的道路上&#xff0c;校园生活是他们融入社会、学习新知、发展社交技能的重要一步。作为星启帆自闭症儿童康复机构&#xff0c;我们深知这一过程对于孩子及其家庭而言既充满挑战也极具意义。 一、前期准备&#xff1a;建立坚实的支持体系 1. 深入了解孩子需求 …

【机器学习】梯度下降函数如何判断其收敛、学习率的调整以及特征缩放的Z-分数标准化

#引言 在机器学习中&#xff0c;特征缩放和学习率是两个非常重要的概念&#xff0c;它们对模型的性能和训练速度有显著影响。 特征缩放是指将数据集中的特征值缩放到一个固定的范围内&#xff0c;通常是在0到1之间或者标准化到均值为0、方差为1。特征缩放对于模型的训练至关重要…

Vmware安装openstack

安装虚拟机 创建完成后&#xff0c;点击开启虚拟机 稍等执行成功后 上传压缩包到指定目录。将yoga_patch.tar.gz包上传至/root目录下&#xff0c;将stack3_without_data.tar.gz包使用WinSCP上传至/opt目录下 vim run_yoga.sh #/bin/bash cd /root sudo apt-get update tar -xzv…

java面向对象总结

java面向对象篇到这里就已经结束了&#xff0c;有什么不懂的地方可以逐一进行重新观看。希望大家能够从入门到起飞。 Java面向对象基础篇综合训练&#xff08;附带全套源代码及逐语句分析&#xff09;-&#xff1e;基于javabeen Java面向对象进阶篇综合训练&#xff08;附带全…

如何跨专业通过软件设计师考试

链接&#xff1a;如何跨专业通过软件设计师-软件中级职称考试 (qq.com)