『 C++ 入门到放弃 』- 哈希表

一、哈希的概念

哈希,也称「 散列 」是一种用来进行高效查找的数据结构,查找的时间复杂度平均为O(1),其本质就是依赖哈希函数这个算法来将 key 和该 key 存储位置建立一个映射关系。

而因为是有着映射关系,所以哈希的事件复杂度为 O (1)

key => hash function => position

采用哈希处理时,一般所需空间都会比元素个数多,否则产生冲突的概率就比较大,影响哈希的性能 => 因此哈希是以空间的代价换取较高的效率

二、相关名词解释

  1. 哈希冲突:也称哈希碰撞,也就是不同的key映射到了同一个位置

    实际上任何哈希函数都可能发生哈希冲突,我们能做的就是设计出一个好的哈希函数尽可能的减少哈希冲突发生的概率

  2. 负载因子:假设哈希表中已经映射存储了N个值,哈希表的⼤⼩为M,则负载因子为 NM{\frac{N}{M}}MN

    负载因子的值越大,意味着哈希冲突的发生率越高,空间使用率高。反之负载因子越小,哈希冲突的发生率越低,空间使用率低。因此负载因子也是后续模拟实现当中用来判断是否要扩容的一个标准

三、哈希函数的定义

3.1 直接定址法

直接定址法适用于 key ( 或称关键字 => 非C++中定义的关键字 ) 范围较集中的情况

直接定址法本质就是⽤ key 计算出⼀个绝对位置或者相对位置

leetcode-387 题目描述如下
在这里插入图片描述

我们可以手动模拟哈希

class Solution {public:int firstUniqChar(string s) {int hash[26] = {0}; // 26 对应26个字母for(int i = 0;i<s.size();++i){hash[s[i] - 'a']++; // 直接通过 s 中每一个字符的值 去映射一个位置}for(int i = 0;i<s.size();++i){if(hash[s[i] - 'a'] == 1){return i;}}return -1;}
};

3.2 除法散列法

除法散列法也叫做除留余数法,假设哈希表的⼤⼩为M,那么通过key除以M的余数作为映射位置的下标

也就是哈希函数为:h(key) = key % M

在使用除法散列法时应避免 M 的大小取 2 的幂次或 10 的幂次

如果 M 取的大小是 2x{2^x}2x 那么key % 2x{2^x}2x 本质相当于保留key的后X位,那么后x位相同的值,计算出的哈希值都是⼀样的就冲突了。

如:{63 , 31}看起来没有关联的值,如果M是16,那么计算出的哈希值都是15

因为63的⼆进制后4位是 1111,31的⼆进制后4位是 1111。如果是10x{10^x}10x,就更明显了,保留的都是
10进值的后x位,如:{112, 12312},如果M是100,那么计算出的哈希值都是12

为了避免前面提到的问题,当使用除法散列法时建议M取不太接近2的整数次幂的⼀个质数

散列函数有一个共同性质,即函数值应按同等概率取其值域的每一个值

四、哈希碰撞的解决方法

4.1 开放定址法

在开放定址法中所有的元素都放到哈希表⾥,当⼀个关键字key⽤哈希函数计算出的位置冲突了

则按照某种规则找到⼀个没有存储数据的位置进⾏存储,开放定址法中负载因⼦⼀定是⼩于的

这⾥有三种:线性探测、⼆次探测、双重探测

4.1.1 线性探测

从发⽣冲突的位置开始,依次线性向后探测,直到寻找到下⼀个没有存储数据的位置为⽌,如果⾛
到哈希表尾,则回绕到哈希表头的位置

采用线性探测法处理散列时的冲突,当从哈希表删除一个记录时,不应将这个记录的所在位置置空,因为这会影响以后的查找

4.1.2 二次探测

从发⽣冲突的位置开始,依次左右按⼆次⽅跳跃式探测,直到寻找到下⼀个没有存储数据的位置为
⽌,如果往右⾛到哈希表尾,则回绕到哈希表头的位置;如果往左⾛到哈希表头,则回绕到哈希表
尾的位置

4.1.3 开放定址法代码实现

开放定址法在实践中,不如链地址法,因为开放定址法解决冲突不管使⽤哪种⽅法,占⽤的都是哈希表中的空间

始终存在互相影响的问题。所以开放定址法,我们简单选择线性探测进行实现

#include <iostream>
using namespace std;namespace OpenAddress {// 仿函数template <class K> struct HashFunc {size_t operator()(const K &key) {return (size_t)key; // 把 key 转成整数 ( 为了处理 key 非整的情况 =>// 因为要取模,非整没办法取模 )// 如果是自定义类型,也可以将赅仿函数进行特化 来处理自定义类型转整的问题}};// 特化 处理stringtemplate <> struct HashFunc<string> {size_t operator()(const string &key) {size_t hash = 0;for (auto ch : key) {hash = hash * 131 + ch;}return hash;}};enum State { EMPTY, EXISTED, DELETED };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:// 这里提供sgi版本的哈希表使⽤的⽅法,给了⼀个近似2倍的质数表,每次去质数表获取扩容后的⼤⼩inline unsigned long __stl_next_prime(unsigned long n) {// Note: assumes long is at least 32 bits.static const int __stl_num_primes = 28;static const unsigned long __stl_prime_list[__stl_num_primes] = {53,        97,         193,        389,       769,       1543,3079,      6151,       12289,      24593,     49157,     98317,196613,    393241,     786433,     1572869,   3145739,   6291469,12582917,  25165843,   50331653,   100663319, 201326611, 402653189,805306457, 1610612741, 3221225473, 4294967291};const unsigned long *first = __stl_prime_list;const unsigned long *last = __stl_prime_list + __stl_num_primes;const unsigned long *pos = lower_bound(first, last, n);// 如果 n 的值超出 [first,last) 的最大值 => return 最后一个元素的下一个位置;// 如果小于 return 第一个元素的位置return pos == last ? *(last - 1) : *pos;}HashTable() { _table.resize(__stl_next_prime(0)); }bool insert(const pair<K, V> &kv) {// 插入时先确定该值是否已存在表中if (find(kv.first)) {return false;}// 先判断当前的负载因子大小( N(数据个数) / M(表大小) )if (_n * 10 / _table.size() >= 7) { // 这里我们将负载因子控制在0.7// 扩容的逻辑就是先开空间 把元素 _table 中的元素插入到新开的空间中// 再将新的空间和旧的空间交换HashTable<K, V, Hash> newht;newht._table.resize(__stl_next_prime(_table.size() + 1));for (size_t i = 0; i < _table.size(); ++i) {if (_table[i]._state == EXISTED) {newht.insert(_table[i]._kv);}}_table.swap(newht._table);}// 如果负载因子小于0.7 则直接插入Hash hash;size_t hash0 = hash(kv.first) % _table.size();size_t hashi = hash0;size_t i = 1;while (_table[hashi]._state == EXISTED) {// 线性探测hashi = (hash0 + i) % _table.size();// 如果是二次探测 就改成 +- i^2++i;}_table[hashi]._kv = kv;_table[hashi]._state = EXISTED;++_n;return true;}HashData<K, V> *find(const K &key) {Hash hash;size_t hash0 = hash(key) % _table.size();size_t hashi = hash0;size_t i = 1;while (_table[hashi]._state != EMPTY) {// 如果找到if (_table[hashi]._state == EXISTED && _table[hashi]._kv.first == key) {return &_table[hashi];}// 如果当前的 index 不是目标值 ( 可能是原本就被占用了,所以要往后找// 直到找完整个 _table) => 所以其实开放定址法的效率不及链定址法hashi = (hash0 + i) % _table.size();++i;}// while 结束代表没有找到目标值return nullptr;}bool erase(const K &key) {HashData<K, V> ret = find(key);if (ret == nullptr) {return false;}ret->_state = DELETED;--_n;return true;}private:vector<HashData<K, V>> _table;size_t _n = 0; // 哈希表中存儲的數據個數};void HashTest1() {HashTable<int, int> ht;ht.insert(make_pair(1, 1));ht.insert(make_pair(2, 2));ht.insert(make_pair(3, 3));ht.insert(make_pair(4, 4));ht.insert(make_pair(5, 5));ht.insert(make_pair(6, 6));ht.erase(2);ht.erase(3);}
} 

4.2 链定址法

链定址法的思想:开放定址法中所有的元素都放到哈希表⾥,链地址法中所有的数据不再直接存储在哈希表中

哈希表中存储⼀个指针,没有数据映射这个位置时,这个指针为空,有多个数据映射到这个位置时**

把这些冲突的数据链接成⼀个链表,挂在哈希表这个位置下⾯,链地址法也叫做拉链法或者哈希桶
在这里插入图片描述

namespace HashBucket {
// 仿函数
template <class K> struct HashFunc {size_t operator()(const K &key) { return (size_t)key; }
};
template <class K, class V> struct HashData {pair<K, V> _kv;HashData<K, V> *_next;HashData(const pair<K, V> &kv) : _kv(kv), _next(nullptr) {}
};template <class K, class V, class Hash = HashFunc<K>> class HashTable {typedef HashData<K, V> Node;public:inline unsigned long __stl_next_prime(unsigned long n) {// Note: assumes long is at least 32 bits.static const int __stl_num_primes = 28;static const unsigned long __stl_prime_list[__stl_num_primes] = {53,        97,         193,        389,       769,       1543,3079,      6151,       12289,      24593,     49157,     98317,196613,    393241,     786433,     1572869,   3145739,   6291469,12582917,  25165843,   50331653,   100663319, 201326611, 402653189,805306457, 1610612741, 3221225473, 4294967291};const unsigned long *first = __stl_prime_list;const unsigned long *last = __stl_prime_list + __stl_num_primes;const unsigned long *pos = lower_bound(first, last, n);// 如果 n 的值超出 [first,last) 的最大值 => return 最後一個元素的下一個位置;// 如果小於 return 第一個元素的位置return pos == last ? *(last - 1) : *pos;}// 初始化HashTable() { _table.resize(__stl_next_prime(0)); }~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) {Hash hs;size_t hashi = hs(kv.first) % _table.size();// 扩容// 这路就不用判断负载因子了,因为相同的位置都会像链表一样串接起来if (_n == _table.size()) {// 怎么扩?vector<Node *> newht(__stl_next_prime(_table.size() + 1), nullptr);for (size_t i = 0; i < _table.size(); ++i) {// 挪动数据Node *cur = _table[i];while (cur) {// 先保留_next;Node *next = cur->_next;// 将旧表的数据挪动到新表size_t hashi = hs(cur->_kv.first) % newht.size();cur->_next = newht[hashi];newht[hashi] = cur;cur = next;}_table[i] = nullptr;}_table.swap(newht);}// 如果有其他数据 => 直接头插Node *newnode = new Node(kv);newnode->_next = _table[hashi];_table[hashi] = newnode;++_n;return true;}Node *find(const K &key) {Hash hs;size_t hash0 = hs(key) % _table.size();Node *cur = _table[hash0];while (cur) {if (cur->_kv.first == key) {return cur;}cur = cur->_next;}return nullptr;}bool erase(const K &key) {Hash hs;size_t hash0 = hs(key) % _table.size();Node *cur = _table[hash0];Node *prev = nullptr;while (cur) {if (cur->_kv.first == key) {if (prev == nullptr) { // prev 为空 代表删除的是第一个数据_table[hash0] = cur->_next;} else {prev->_next = cur->_next;}delete cur;--_n;return true;}prev = cur;cur = cur->_next;}return false;}private:vector<Node *> _table;size_t _n = 0; // 数据个数
};
void HashBucketTest1() {HashTable<int, int> ht;ht.insert(make_pair(1, 1));ht.insert(make_pair(2, 2));ht.insert(make_pair(3, 3));ht.insert(make_pair(4, 4));ht.insert(make_pair(5, 5));ht.insert(make_pair(6, 6));ht.insert(make_pair(7, 7));ht.insert(make_pair(8, 8));ht.insert(make_pair(9, 9));ht.insert(make_pair(10, 10));
}
void HashBucketTest2() {HashTable<int, int> ht;ht.insert(make_pair(1, 1));ht.insert(make_pair(2, 2));ht.insert(make_pair(3, 3));ht.insert(make_pair(4, 4));ht.insert(make_pair(5, 5));ht.erase(2);ht.erase(3);ht.erase(4);ht.erase(5);ht.erase(6);ht.erase(7);ht.erase(8);ht.erase(9);
}
} // namespace HashBucket

五、选择题解析

  1. 已知有一个关键字序列:(19,14,23,1,68,20,84,27,55,11,10,79)散列存储在一个哈希表中

    若散列函数为H(key)=key%7,并采用链地址法来解决冲突,则在等概率情况下查找成功的平均查找长度为?

    其呈现如上图

    14,1,23,10,11,19,20 比较 1 次就可以找到

    84,79,68,27 2次

    55 3 次

    总共:1 * 7 + 2 * 4 + 3 * 1 = 18 数据个数:12

    18 / 12 =1.5

  2. 已知某个哈希表的n个关键字具有相同的哈希值,如果使用二次探测再散列法将这n个关键字存入哈希表,至少要进行几次探测

    有 n 个数据,第一个数据 1 次,第二个数据 2 次,。。。第 n 个数据 n 次

    总共:1 + 2 + 。。。 + n = n∗(n+1)2{\frac{n*(n+1)}{2}}2n(n+1)

  3. 用哈希(散列)方法处理冲突(碰撞)时可能出现堆积(聚集)现象,会受堆积现象直接影响的是平均查找長度

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

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

相关文章

零售收银系统开源代码全解析:连锁门店一体化解决方案(含POS+进销存+商城)

过去10年&#xff0c;收银系统技术经历了从单机版到云服务、从单纯结算到全渠道整合的快速演进。面对连锁多门店、AI称重、智能分账、跨店库存同步等新需求&#xff0c;很多企业的现有传统saas系统已显乏力。本文将梳理收银系统关键技术指标&#xff0c;助您在系统升级时做出明…

能源高效利用如何实现?楼宇自控系统智能化监管建筑设备

随着全球能源危机日益严峻和“双碳”目标的持续推进&#xff0c;建筑领域作为能耗大户&#xff08;约占社会总能耗的40%&#xff09;&#xff0c;其节能潜力备受关注。楼宇自控系统&#xff08;Building Automation System&#xff0c;简称BAS&#xff09;作为建筑智能化的核心…

校园二手交易小程序的设计与实现

文章目录前言详细视频演示具体实现截图后端框架SpringBoot微信小程序持久层框架MyBaits成功系统案例&#xff1a;参考代码数据库源码获取前言 博主介绍:CSDN特邀作者、985高校计算机专业毕业、现任某互联网大厂高级全栈开发工程师、Gitee/掘金/华为云/阿里云/GitHub等平台持续…

Redis(二):Redis高级特性和应用(慢查询、Pipeline、事务)

Redis的慢查询 许多存储系统&#xff08;例如 MySQL)提供慢查询日志帮助开发和运维人员定位系统存在的慢操作。所谓慢查询日志就是系统在命令执行前后计算每条命令的执行时间&#xff0c;当超过预设阀值,就将这条命令的相关信息&#xff08;例如:发生时间&#xff0c;耗时&…

如何为你的WordPress网站选择合适的安全插件

在管理WordPress网站时&#xff0c;安全因素至关重要。由于WordPress的广泛使用&#xff0c;它也成为了黑客攻击的首要目标。为了避免潜在的安全风险&#xff0c;选择合适的安全插件至关重要。而Wordfence和iThemes&#xff0c;作为两款颇具人气的WordPress安全插件&#xff0c…

我们使用Rust开发的AI知识库应用

这段时间陆陆续续的开发了2个AI知识库应用&#xff0c;一个面向企业&#xff0c;一个面向C端用户。 飞树智库&#xff1a;一个安全高效的面向 企业的知识库平台&#xff08;https://fskb.coderbox.cn/&#xff09;。 小飞树&#xff1a;一个专注于个人知识管理的AI应用&#…

自动化测试实战篇

目录 1. 自动化实施步骤 1.1 编写web测试用例 1.2 自动化测试脚本开发 1.3 将自动化测试补充至测试报告 1. 自动化实施步骤 1.1 编写web测试用例 1.2 自动化测试脚本开发 TestDevelopment: 测试用例 - Gitee.comhttps://gitee.com/Axurea/test-development/tree/master/2…

idea 服务器Debug端口启动设置

一&#xff1a;在阿里云服务器安全组已经设置了端口授权对象&#xff1a;正确命令&#xff1a;nohup java -Xdebug -Xrunjdwp:transportdt_socket,servery,suspendn,address9998 -jar -Duser.timezoneGMT08 -Xms256m -Xmx256m /opt/projects/*/*/*-starter-1.0-SNAPSHOT.jar -…

大模型量化004

Bert P-tuning BertPET、BertP-Tuning Chain of Thought Few shot Cot Auto-COT 解决手动编写高质量CoT示例麻烦耗时的问题 Auto COT 自动思维链生成器 1.业务场景&#xff1a; 每天收到很多反馈&#xff0c;之前需要人工整理&#xff0c;找到重点&#xff0c;做判断那些需要立…

C#(基本语法)

数据类型C#是一种强类型语言&#xff0c;变量必须声明类型。基本数据类型包括整型&#xff08;int、long&#xff09;、浮点型&#xff08;float、double&#xff09;、布尔型&#xff08;bool&#xff09;、字符型&#xff08;char&#xff09;和字符串型&#xff08;string&a…

ARM-I2C软实现

开发流程引脚初始化引脚功能定义实现读操作实现写操作GD32F4软件I2C初始化void SoftI2C_init() {// 时钟配置rcu_periph_clock_enable(SCL_RCU);// 设置输出模式gpio_mode_set(SCL_PORT, GPIO_MODE_OUTPUT, GPIO_PUPD_NONE, SCL_PIN);gpio_output_options_set(SCL_PORT, GPIO_O…

防水医用无人机市场报告:现状、趋势与洞察

市场规模与增长趋势在全球医疗科技快速发展的当下&#xff0c;防水医用无人机市场正崭露头角&#xff0c;展现出强劲的发展势头。据 QYR统计&#xff0c;2023 年全球医用无人机市场销售额达到 1.9 亿美元&#xff0c;预计到 2030 年将飙升至 8.5 亿美元&#xff0c;年复合增长率…

haproxy代理

一.负载均衡 1.1.什么是负载均衡 负载均衡&#xff1a;Load Balance&#xff0c;简称LB&#xff0c;是一种服务或基于硬件设备等实现的高可用反向代理技术&#xff0c;负载均 衡将特定的业务(web服务、网络流量等)分担给指定的一个或多个后端特定的服务器或设备&#xff0c;…

【面试】软件测试面试题

1. 测试用例如何编写 2. bug的生命周期 项目有多少人&#xff1f;多少条测试用例&#xff1f;多少bug&#xff1f;自己发现的第一条&#xff1f;&#xff08;是不是bug&#xff09; 3. 缺陷管理工具 包括Jira, PingCode, 禅道&#xff0c;BugZilla&#xff0c;Redmine, TAPD&am…

HbuilderX开发小程序

1.打卡HbuilderX&#xff0c;选择文件—新建—项目2.创建项目3.在HbuilderX中运行前要确定微信开发这工具的服务端口号是打开的4.HbuilderX中点击预览可以实时预览5.在微信开发者中进行本地测试点击后自动跳转到微信开发者工具中运行项目

Netty中FastThreadLocal解读

io.netty.util.concurrent.FastThreadLocal 是 Netty 中提供的高性能线程局部存储&#xff08;Thread-Local Storage&#xff09;实现&#xff0c;位于 io.netty.util.concurrent 包。它是 Java 标准库 ThreadLocal 的替代品&#xff0c;旨在优化性能&#xff0c;减少内存分配和…

上海迪士尼游玩攻略 小铁寄存柜让你轻松畅玩

去上海迪士尼玩最烦带一堆行李&#xff0c;其实有小铁寄存柜帮忙就能轻装上阵&#xff0c;各个关键位置都有分布&#xff0c;玩起来特别省心。​刚到迪士尼的时候&#xff0c;要是坐地铁到上海国际旅游度假区站&#xff0c;1/2 号口安检区就有小铁柜&#xff0c;行李箱、大背包…

飞算科技重磅出品:飞算 JavaAI 重构 Java 开发效率新标杆

在 Java 开发领域&#xff0c;一款由国家级高新技术企业自主研发的智能工具正引发行业关注 —— 飞算 JavaAI 不仅承载着中国原创技术的创新基因&#xff0c;更以贴合实际开发场景的功能设计&#xff0c;成为众多企业提升 Java 开发效率的核心助力。​作为飞算数智科技&#xf…

python案例:基于python 神经网络cnn和LDA主题分析的旅游景点满意度分析

1&#xff0e;绪论1.1研究背景与意义1.1.1研究背景随着旅游业的快速发展&#xff0c;满意度分析成为评估旅游景点质量和提升游客体验的重要手段。作为中国的旅游城市之一&#xff0c;其旅游景点吸引了大量游客。然而&#xff0c;如何科学评估和提升旅游景点的满意度&#xff0c…

Git快速入门,完整的git项目管理工具教程,git入门到精通!

Git的下载与安装&#xff1a; 直接去官网下载即可&#xff1b; 或者查看这个博客学会下载:Git 详细安装教程&#xff08;详解 Git 安装过程的每一个步骤&#xff09;_git安装-CSDN博客 注意&#xff1a;一个文件夹下只能有一个本地仓库(就是一个.git) 细节操作