高阶数据结构之哈希表基础讲解与模拟实现
程序猿的读书历程:x语言入门—>x语言应用实践—>x语言高阶编程—>x语言的科学与艺术—>编程之美—>编程之道—>编程之禅—>颈椎病康复指南。
前言:
哈希表(Hash Table)是一种高效的键值对存储数据结构,广泛应用于各种需要快速查找的场景,如数据库索引、缓存系统、集合等。它的基本思想是通过哈希函数将键映射到哈希表中的一个位置,从而实现快速的数据插入、删除和查找操作。下面我们将详细介绍哈希表的工作原理、实现方式、优缺点以及应用场景。
一、哈希概念
哈希是一种思想,普遍是通过一个哈希数组来存储数据的。学哈希思想,最重要的就是抓住映射两个字,它是一个无序的数据结构,所以想要找到存储的数据,就必须通过相对应的哈希关系来寻找。

这样,我们就能通过这个映射关系,可以不经过任何比较,一次直接从表中得到要搜索的元素。
二、哈希冲突
但是通过上面的介绍,相信不少童鞋已经发现了,一个下标只能存储一个数据,如果我们有两个数,转换后的下标相同呢?
即:不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
倘若数据中发生了哈希冲突,我们应该怎么做呢?
常见的哈希函数主要是有两种,一种是直接定址法:
1、闭散列
2、开散列

三、其他数据类型的存储问题
哈希函数采用处理余数法,被模的key必须要为整形才可以处理,我们之前的思路只能解决int类型的存储问题,如果那个值是string,是char,我们又应该怎么解决呢?
string与char类型不能被取余,我们想到,那就把它转化为int类型不就可以了吗。
由于字符串长度我们不能确定,但abcd与acbd两个字符串的ASCII码值确实一样,如果光是ASCII码值之和来计算,难免会出现比较离谱的存储结果。据此,通过研究,我们可以通过一些条件来减少ASCII码值的巧合:
class Str_to_Int
{
public:size_t operator()(const string& s){const char* str = s.c_str();unsigned int seed = 131; // 31 131 1313 13131 131313unsigned int hash = 0;while (*str){hash = hash * seed + (*str++);}return (hash & 0x7FFFFFFF);}
};
通过这种处理,就能明显减少巧合的发生,将其分配到正确的地址上。
四、哈希表闭散列线性探测实现
我们先写一个简单的哈希表的闭散列实现来理解一下哈希表的底层逻辑。
#pragma once
#include<vector>// 哈希函数采用除留余数法
template<class K>
struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};// 哈希表中支持字符串的操作
template<>//这是对前面模板HashFunc的string特化类型
struct HashFunc<string>
{size_t operator()(const string& key){size_t hash = 0;for (auto e : key){hash *= 31;//防止abcd与dcba的ASCII码值之和相同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 Hash = HashFunc<K>>class HashTable{public:HashTable(){_tables.resize(10);}bool Insert(const pair<K, V>& kv){if (Find(kv.first))//如果以前插入过相同键值{return false;}if ((_n * 10) / _tables.size() >= 7)//扩容{HashTable<K, V, Hash>newh;newh._tables.resize(2 * _tables.size());for (int i = 0; i < _tables.size(); ++i){if (_tables[i]._state == EXIST){newh.Insert(_tables[i]._kv);}}_tables.swap(newh._tables);}Hash h;size_t index = h(kv.first) % _tables.size();//确定插入下标while (_tables[index]._state == EXIST){++index;index = index % _tables.size();}_tables[index]._state = EXIST;_tables[index]._kv = kv;++_n;return true;}HashData<K, V>* Find(const K& key){Hash h;size_t index = h(key) % _tables.size();//确定查找下标while (_tables[index]._state != EMPTY){if ( key == _tables[index]._kv.first){return &_tables[index];}++index;index %= _tables.size();}return nullptr;}bool Erase(const K& key){HashData<K, V>* ret = find(key);if (ret){ret->_state = DELETE;return true;}else{return false;}}private:vector<HashData<K, V>> _tables;size_t _n = 0; // 表中存储数据个数};
}
慢慢看这层代码。
我们用K代表key值,V代表Value值,用Hash来代表一个模板函数,这个函数是为了实现我们的转化key值的作用(就是string类型的key转化为int值)。
我们首先实现了哈希函数的模板,让任意类型的K值得以转化为int类型的参数。注意:对于能够转化为int类型的内置类型,我们直接使用强制转化就行,但是对于经常常用到的string,却又不能直接转换为int,我们就可以写一个特化,要求当K为string时直接调用我们的特化函数就行了。
随后在我们的作用于中定义一个枚举类型,代表上面说的三个状态:存在,空,删除。
寻常的内置类型自然不会包含我们才定义的枚举状态,自然就需要定义一个自定义类型。于是HashData出世了。
随后就是平常的接口的编写:
对于find接口,如果我们找到了对应的值,就需要返回这个值的指针HashData<K, V>*,如果没找到,就返回空指针。而查找就是先通过Hash,来找到初始的键值处,开始线性查找直到找到或者为空找不到。
对于insert插入接口,我们先判断是否已经插入过相同键值,然后在判断是否达到扩容标准,如果达到了,就进行扩容操作(创建一个新的哈希数组,随后复用insert进行插入,最后交换两个哈希数组就行,新创建的会自动进行销毁)。扩容后,也是先通过Hash,来找到初始的键值,但我们这次应该通过线性探测来查找空位置或者删除的位置。
对于erase接口,我们可以先复用find找到相应的位置,随后把其的_state属性改为delete就行,不必进行数据内容上的修改。我们访问任意一个地址,都是先判断其state属性是否满足条件。
五、哈希表开散列哈希桶的实现
先看代码:
#pragma once
#include<vector>template<class K>
struct HashFunc//哈希函数,把K类型转化为int
{size_t operator()(const K& key){return (size_t)key;}
};
template<>
struct HashFunc<string>//当K类型时string时的特化函数
{size_t operator()(const string& s){size_t ret = 0;for (auto &it : s)//我们这里对string的每个字母采用乘以31再相加的方法{ret *= 31;ret += it;}return ret;}
};namespace hash_bucket
{template<class T>struct HashNode//哈希桶存储的单链表的节点结构{T _data;HashNode<T>* next;HashNode(const T&data):_data(data),next(nullptr){}};template<class K,class T,class Hash=HashFunc<K>>class HashTable{struct keyofT//我这里的实现方法有些特殊,多增加了一个keyofT函数,这个函数时为了后面用哈希桶实现unordered_map与unordered_set//而实现的,由于那个时候哈希桶才是底层,所以现在只使用底层代码就会变得奇怪{const K& operator()(const T&kv)//传递一个string ,int类型的参数就得//HashTable<string, pair<string, int>>hash;{return kv.first;}};public:typedef HashNode<T> Node;HashTable():_n(0){_tables.resize(10);}~HashTable(){for (size_t i = 0; i < _tables.size(); i++){Node* cur = _tables[i];while (cur){Node* next = cur->next;delete cur;cur = next;}_tables[i] = nullptr;}}bool insert(const T& data){Hash h;if (_n >= _tables.size())//扩容{vector<Node*> newtables(_tables.size() * 2, nullptr);for (int i = 0; i < _tables.size(); ++i){Node* cur = _tables[i];while (cur){Node* next = cur->next;size_t newindex = h(keyofT()(cur->_data)) % newtables.size();cur->next = newtables[newindex];newtables[newindex] = cur;cur = next; }_tables[i] = nullptr;}_tables.swap(newtables);}Node* newnode = new Node(data);size_t index = h(keyofT()(data)) % _tables.size();newnode->next = _tables[index];_tables[index] = newnode;++_n;return true;}Node* find(const K& key){Hash h;size_t index = h(key) % _tables.size();Node* cur = _tables[index];while (cur){if (cur->_data.first == key){return cur;}else{cur = cur->next;}}return nullptr;}bool erase(const K& key){Hash h;size_t index = h(key) % _tables.size();Node* prev = nullptr;Node* cur = _tables[index];while (cur){if (cur->_data.first == key){if (prev == nullptr){_tables[index] = cur->next;}else{prev->next = cur->next;}delete cur;cur = nullptr;return true;}else{prev = cur;cur = cur->next;}}return false;}private:vector<Node*>_tables;size_t _n;};
};
相较于闭散列,stl库里实现unordered_map与unordered_set两个容器时底层都用的开散列,所以我这里的开散列实现的有些奇怪,增加的keyofT函数更有利于后续的封装容器。
但是大体结构仍然没有改变,同样用到了Hash来解决不同类型转化为int的问题。唯一值得一提的就是由于我们的节点是指针的链接方式,所以扩容时,我们不需要再赋值节点,只需要把每个节点指针插入到新的哈希table里进行交换就行。
六、哈希表性能分析
哈希表的性能主要取决于哈希函数的设计和哈希冲突的处理方式。哈希表在最理想的情况下,即哈希函数将元素均匀分布到哈希表中时,查找、插入、删除操作的时间复杂度为 O(1)O(1)O(1)。但当发生大量哈希冲突时,时间复杂度可能退化到 O(n)O(n)O(n),这是最坏情况。为了优化性能,我们可以从以下几个方面着手:
-
设计良好的哈希函数:哈希函数应尽可能均匀地将元素分布到哈希表中,避免哈希冲突。对数据的特性进行分析,选择合适的哈希函数,如前文提到的直接定址法、除留余数法等。
-
扩容:当哈希表中存储的元素个数接近表容量时,哈希冲突的概率会增加,因此需要动态扩容,保持较低的装载因子(如装载因子不超过0.7)。
-
合理选择哈希冲突解决策略:开散列(链地址法)通常比闭散列(开放定址法)表现更好,尤其是在高装载因子的情况下,链表法通过链表的结构减少了冲突对性能的影响。
七、哈希表应用场景
哈希表作为一种高效的数据结构,应用非常广泛,特别是在需要快速查找的场景中。例如:
-
数据库索引:哈希表在数据库系统中用于索引结构,能够快速查找数据。
-
缓存系统:例如Redis等内存缓存系统广泛使用哈希表存储键值对,实现高效的数据存取。
-
集合类操作:哈希表在语言标准库中的实现,如C++的
unordered_map
、unordered_set
,用于高效的查找和去重操作。 -
字典查找:哈希表是构建字典和符号表的基础,广泛用于自然语言处理、编译器等场景。
八、哈希表的优缺点
优点:
- 查找、插入、删除操作在理想情况下的时间复杂度为 O(1)O(1)O(1),性能非常高效。
- 实现简单,适合键值对的快速存储和检索。
缺点:
- 在发生大量哈希冲突的情况下,性能可能退化到 O(n)O(n)O(n)。
- 哈希函数的设计需要谨慎,容易出现偏斜分布,从而影响性能。
- 哈希表无法保证元素的顺序,适用于无序集合或字典的应用场景。
九、总结
哈希表作为一种重要的数据结构,提供了高效的查找、插入和删除操作。通过设计良好的哈希函数和适当的冲突解决策略,可以最大化哈希表的性能。了解哈希表的工作原理和实现方式,有助于在实际应用中选择合适的解决方案,并有效提升系统的性能。
希望本篇文章对大家有所帮助!
相关文章:

高阶数据结构之哈希表基础讲解与模拟实现
程序猿的读书历程:x语言入门—>x语言应用实践—>x语言高阶编程—>x语言的科学与艺术—>编程之美—>编程之道—>编程之禅—>颈椎病康复指南。 前言: 哈希表(Hash Table)是一种高效的键值对存储数据结构&…...

基于STM32设计的智能货架(华为云IOT)(225)
文章目录 一、前言1.1 项目介绍【1】项目背景【2】项目支持的功能【3】项目硬件模块组成【4】ESP8266工作模式配置【5】Android手机APP开发思路【6】项目模块划分1.2 项目开发背景【1】选题来源与背景【2】国内外研究现状【3】课题研究的目的和内容【4】参考文献【5】研究内容【…...

JDBC API详解一
DriverManager 驱动管理类,作用:1,注册驱动;2,获取数据库连接 1,注册驱动 Class.forName("com.mysql.cj.jdbc.Driver"); 查看Driver类源码 static{try{DriverManager.registerDriver(newDrive…...

工厂安灯系统在设备管理中的重要性
在现代制造业中,设备管理是确保生产效率和产品质量的关键环节。随着工业4.0的推进,越来越多的企业开始采用智能化的设备管理系统,其中安灯系统作为一种有效的管理工具,逐渐受到重视。安灯系统最初源于日本的丰田生产方式ÿ…...

【LabVIEW学习篇 - 23】:简单状态机
文章目录 简单状态机状态机的创建和了解状态机实现红绿灯 简单状态机 一个优秀的应用程序离不开好的程序框架,不仅要很好满足用户的功能需求,还要考虑到系统的稳定性、实时性、可扩展性、可维护性,执行效率等方面。借用一些成熟的设计框架&a…...

【CSS】 Grid布局:现代网页设计的基石
引言 最近接到一个网页布局比较复杂的页面,看了半天还是决定用grid布局来写,记录一下 布局是构建用户界面的关键部分。CSS Grid布局提供了一种简单而强大的方式来创建复杂的网格布局,它让设计师和开发者能够更直观、更灵活地控制网页的结构。…...

jQuery UI API 文档
关于《jQuery UI API 文档》,我找到了一些有用的信息。jQuery UI 是建立在 jQuery JavaScript 库上的一组用户界面交互、特效、小部件及主题。如果您是 jQuery 新手,建议您先查看 jQuery 教程。目前,我找到的资料主要是关于 jQuery UI 1.10 版…...

盘点2024年大家都在用的录屏工具
现在录屏工具的使用范围越来越广了。我的深切体验是有很多人愿意为知识付费了,但是到线下培训的话很多人时间不一定能协调的来,这就导致涌现了不少的录屏课程。这次我们来探讨下要怎么录屏才能呈现更好的效果。 1.福昕录屏大师 链接达达:ww…...

【大数据】探索怎么从一段话中解析关键信息(寄件人相关信息)
本文由ChatGPT生成,主要用于学习,大家有疑问请及时提出。 使用NLP实现文本信息解析功能:以提取姓名、地址和电话号码为例 在这个博客中,我们将通过自然语言处理(NLP)技术来实现一个简单的文本信息解析功能…...

初学者指南:MyBatis 入门教程
主要介绍了Mybatis的基本使用、JDBC、数据库连接池、lombok注解! 文章目录 前言 什么是Mybatis? 快速入门 使用Mybatis查询所有的用户信息 配置SQL提示 JDBC介绍 Mybatis 数据库连接池 lombok 总结 前言 主要介绍了Mybatis的基本使用、JDBC、数据库连接…...

reader-lm:小模型 html转markdown
参考: https://huggingface.co/jinaai/reader-lm-0.5b 在线demo: https://colab.research.google.com/drive/1wXWyj5hOxEHY6WeHbOwEzYAC0WB1I5uA#scrollTo0mG9ISzHOuKK 输入网址:https://www.galaxy-geely.com/E5 结果: 代码…...

进击J6:ResNeXt-50实战
🍨 本文为🔗365天深度学习训练营 中的学习记录博客🍖 原作者:K同学啊 一、实验目的: 阅读ResNeXt论文,了解作者的构建思路对比之前介绍的ResNet50V2、DenseNet算法使用ResNeXt-50算法完成猴痘病识别 二、实…...

新代机床采集数据
新代集團1995年成立於台灣新竹,事業版圖遍布全球,以台灣為中心向外發展,據點橫跨歐洲、美洲、亞洲三大洲。新代長期深耕於機床控制器的軟體及硬體技術研發,專注於運動控制領域,目前已成為亞太市場中深具影響力的控制器領導品牌之一。主營產品包括:機床數控系統、伺服驅動…...

景联文科技:专业数据标注公司,推动AI技术革新
数据标注作为AI技术发展的重要支撑,对于训练高质量的机器学习模型以及推动应用领域的创新具有不可替代的作用。 景联文科技作为专业的数据标注公司,致力于提供专业的数据标注服务,帮助客户解决AI链条中的数据处理难题,共同推动人工…...

k8s以及prometheus
#生成控制器文件并建立控制器 [rootk8s-master ~]# kubectl create deployment bwmis --image timinglee/myapp:v1 --replicas 2 --dry-runclient -o yaml > bwmis.yaml [rootk8s-master ~]# kubectl expose deployment bwmis --port 80 --target-port 80 --dry-runclient…...

android 权限说明
1. 权限的定义语法 注: 任何应用都可以定义权限 <permission 标签是定义权限 <uses-permission 标签是使用权限。 <permission android:description"string resource"android:icon"drawable resource"android:label"string res…...

<winsock>重叠IO模型
基于事件判断io完成 send程序 #include <stdio.h> #include <winsock2.h>#pragma comment(lib, "Ws2_32.lib") #pragma warning(disable : 4996)int main() {WSADATA wsaData;if (WSAStartup(MAKEWORD(2, 2), &wsaData) ! 0){printf("WSAStart…...

Android Tools | 如何使用Draw.io助力Android开发:从UI设计到流程优化
Android Tools | 如何使用Draw.io助力Android开发:从UI设计到流程优化 1. 引言 在Android开发中,视觉化设计与流程管理至关重要。虽然开发工具如Android Studio强大,但它并不适用于所有设计场景。Draw.io是一款免费的在线绘图工具ÿ…...

Java 每日一刊(第5期):变量守护者
前言 这里是分享 Java 相关内容的专刊,每日一更。 本期将为大家带来以下内容: 量子数据宇宙的变量守护者第一章:能源错配与基本数据类型第二章:引用类型与通讯网络的崩溃第三章:作用域冲突与系统崩溃终章࿱…...

【C++二分查找】2517. 礼盒的最大甜蜜度
本文涉及的基础知识点 C二分查找 贪心(决策包容性) LeetCode 2517. 礼盒的最大甜蜜度 给你一个正整数数组 price ,其中 price[i] 表示第 i 类糖果的价格,另给你一个正整数 k 。 商店组合 k 类 不同 糖果打包成礼盒出售。礼盒的 甜蜜度 是礼…...

【详解】数据库E-R图——医院计算机管理系统
题目 某医院病房计算机管理中需要如下信息: 科室:科室名,科室地址,科室电话,医生姓名 病房:病房号,床位号,所属科室名 医生:工作证号,姓名,性别&a…...

分类预测|基于改进的灰狼IGWO优化支持向量机SVM的数据分类预测matlab程序 改进策略:Cat混沌与高斯变异
分类预测|基于改进的灰狼IGWO优化支持向量机SVM的数据分类预测matlab程序 改进策略:Cat混沌与高斯变异 文章目录 一、基本原理原理流程1. **定义目标函数**2. **初始化GWO**3. **评估适应度**4. **更新狼的位置**5. **更新狼的等级**6. **重复迭代**7. **选择最佳解…...

圆锥曲线练习
设 A ( x 1 , y 1 ) , B ( x 2 , y 2 ) A\left( x_{1}, y_{1} \right), B\left( x_{2}, y_{2} \right) A(x1,y1),B(x2,y2) l : y k ( x 2 ) l: y k\left( x2 \right) l:yk(x2) 显然 y 0 y0 y0符合题意 当 k ≠ 0 k\neq 0 k0 联立 l l l和 C C C ( k 2 1 2 ) x…...

STM32时钟树
1 什么是时钟 2 时钟数简图...

NX—UI界面生成的文件在VS上的设置
UI界面保存生成的三个文件 打开VS创建项目,删除自动生成的cpp文件,将生成的hpp和cpp文件拷贝到项目的目录下,并且在VS项目中添加现有项目。 修改VS的输出路径,项目右键选择属性,链接器中的常规,文件路径D:…...

Wine容器内程序执行sh脚本问题研究
问题背景 wpf程序在wine环境执行sh脚本,不能等待脚本执行完成自动退出的问题进行了研究,需求很简单,在wpf程序使用cmd,或者bat ,又或者是直接执行sh脚本,想到脚本执行完成才处理后面的逻辑。但是实际验证过…...

《深度学习》OpenCV轮廓检测 模版匹配 解析及实现
目录 一、模型匹配 1、什么是模型匹配 2、步骤 1)提取模型的特征 2)在图像中查找特征点 3)进行特征匹配 4)模型匹配 3、参数及用法 1、用法 2、参数 1)image:待搜索对象 2)templ&am…...

Java XML
1、XML文件介绍 配置文件:用来保存设置的一些东西。 拿IDEA来举例,比如设置的背景图片,字体信息,字号信息和主题信息等等。 (1)以前是用txt保存的,没有任何优点,而且不利于阅读&a…...

好用的视频压缩工具有哪些?这4款千万不要错过
视频压缩的方法有很多种,像我们手机里的视频剪辑工具,手机和电脑自带的压缩功能,在线压缩网站,专业压缩软件压缩等等。不同的场景和需求下大家可以选择不同的工具,但是如果碰到需要大量和经常压缩视频的话,…...

【Python爬虫系列】_016.关于登录和验证码
我 的 个 人 主 页:👉👉 失心疯的个人主页 👈👈 入 门 教 程 推 荐 :👉👉 Python零基础入门教程合集 👈👈 虚 拟 环 境 搭 建 :👉&…...