【C++】哈希闭散列

一.哈希的概念

在前面学习了二叉搜索树、AVL树、红黑树之后,我们得知顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须经过关键码的多次比较。顺序查找的时间复杂度为 O(N),平衡树中为树的高度,即(logN),搜索的效率取决于搜索过程中元素的比较次数。

理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素。

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

当向该结构中:

  • 插入元素
    根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。

  • 搜索元素
    对元素的关键码进行同样的计算,把求得的函数当作元素的存储位置,在结构中按此位置取元素进行比较,若关键码相等,则搜索成功。

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

在这里插入图片描述

使用以上方法插入直接进行映射插入,搜索时不必进行关键码的比较,因为搜索速度非常快。

1. 哈希冲突

对于两个数据元素的关键字,其 key1 != key2,但是存在 key1 % p == key2 % p,此时 key1 和key2 就会被映射到 hash Table 中相同的位置。

在这里插入图片描述
假设我们将需要存的数n,存的索引值 = n % 10
现在需要将20存入该表中,计算出的键值为0,但是该位置已经有数据了,即发生了哈希冲突.

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

把具有不同关键码而具有相同哈希地址的数据元素称为 “同义词” 。
发生哈希冲突该如何处理呢?

2. 哈希函数

引起哈希冲突的一个原因在于:哈希函数设计的不够合理。

哈希函数的设计原则:

  • 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有 m 个地址时,其值域必须在 0 到 m-1 之间。
  • 哈希函数计算出来的地址能均匀的分布在整个空间中。
  • 哈希函数设计应足够简单。

常见的两种哈希函数

① 直接定址法

取关键字的某个线性函数为散列地址:hash(key) = A * Key + B

优点:简单、均匀

缺点:需要事先知道关键字的分布情况

使用场景:适合查找比较小且连续的情况。

题目举例:387. 字符串中的第一个唯一字符

②除留余数法(常用)

设散列表中允许的地址数为 m ,取一个不大于 m ,但最接近或者等于 m 的质数 p 作为除数,按照哈希函数:hash(key) = key % p(p<= m),将关键码转化为哈希地址。

就是以下这种方法

在这里插入图片描述

3. 负载因子

如果hash的插入,进行映射后如果找不到空位就要一直往后面检测查找空位,所以如果当哈希表中只有一个空位时,插入一个数据的时间复杂度很可能就变成了O(N),所以说再这种情况发生前我们就要对其进行扩容。

那什么情况下进行扩容呢?应该括多大呢?如果是除留余数法,那质数 p又应该是多少呢?

散列表的负载因子定义为:a = 填入表中的元素个数 / 散列表的长度

在这里插入图片描述

负载因子越小,冲突的概率越小,空间利用率低
负载因子越大,冲突的概率越大,空间利用率高。

而库中的做法是,负载因子如果大于0.7,就进行扩容。

二.闭散列

闭散列:也叫开放地址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把 key 存放到冲突位置中的"下一个"空位置中去。

寻找下一个空位置方式有线性探测和二次探测,接下来我们结合理论并实现一下。

1. 线性探测

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

结构定义:

首先我们定义结构,我们定义每个位置有三种状态{存在,空位,删除},一个位置要么是已经有数据了,要么是没有数据,为什么有一个删除状态呢?

如果出现下面这种情况,20在3的后面进行插入,将3删除后,20便无法查找到,所以我们会再添加一个删除状态,防止以下这种情况发生。

在这里插入图片描述

使用一个数组进行存储,其中每一个位置位HashDate类型,该类型中会记录当前位置的状态。再添加一个size变量,用于记录当前存储的有效数据个数。结构如下:

enum State {EMPTY,EXIST,DELETE};   //每个位置有三种状态template<class K,class V>
struct HashDate
{pair<K, V> _kv;State _state;
};template<class K,class V>
class HashTable
{
public://成员函数:
private:vector<HashDate<K, V>> _table;size_t _size=0;				 
};

2. Insert 插入

  • 通过哈希函数(除留余数法)获取待插入元素在哈希表中的位置
  • 如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,使用线性探测找到下一个空位置,插入新元素。

在这里插入图片描述

注意事项:

  1. 确定质数p,我们应该取vector的size()还是capacity()作为质数?
    使用size(),因为capacity()是vector开辟的总空间,超过size()的部分是不能直接使用的,只能使用size()以内的空间,而size要通过我们插入数据或resize进行改变。简而言之,vector中超过size(),小于capacity()的部分我们是不能直接访问的,尽管已经开辟。

  2. 如果线性探测一直探测到 i 下标超过hash_table.size(),我们应该如何做。
    如果一直探测超过数组的下标,应该绕回数组的开始处,所以每次 i++ 后,我们可以继续进行取模,如果超过了size(),会自动从数组0下标处开始探测;当然,使用if判断 i 超过size(),超过就置0也是可以的。

  3. 当装载因子超过0.7之后,我们应该怎么做。
    即_size / _table.size() >=7 时,我们要进行扩容,创建一个新哈希表,然后将旧表中的数据拷贝到新表中,此时我们可以复用 Insert 函数,因为新表是不存在扩容问题的,所以会使用 Insert 中插入逻辑的代码,然后将数据全部插入到新表中,最后我们将新表与旧表的进行swap一下,就将新表扩容指向的内容交换给了临时变量,临时变量调用析构函数,自动释放,这样,扩容问题就得到了解决。

bool Insert(const pair<K, V>& kv)
{//如果 size==0 或装载因子 >=0.7 进行扩容if (_table.size() == 0 || 10 * _size / _table.size() >= 7){size_t newSize = _table.size() == 0 ? 10 : _table.size() * 2;HashTable<K, V> newHash;newHash._table.resize(newSize);//将旧表中的数据拷贝到新表中  --- 复用Insert继续拷贝数据for (auto e : _table){if (e._state == EXIST){newHash.Insert(e._kv);}}//进行交换  newHash自动调用其析构函数_table.swap(newHash._table);}size_t hashi = kv.first % _table.size();while (_table[hashi]._state == EXIST)   //如果存在数据就一直往后找{hashi++;//如果hashi++超过size(),需要绕回数组的开始hashi %= _table.size();}//找到位置,插入数据_table[hashi]._kv = kv;_table[hashi]._state = EXIST;++_size;return  true;
}

注意,如果插入的是负数,会发生整形提升,int类型会转变为我们的size_t 类型,此时负数再进行取模,就可以得到一个合法的映射位置,也可以被查找的。

接下来还有一个问题,如果数据发生冗余怎么办。就是如果插入的是已经存在的值,应该如何处理呢?

3. Find 查找

所以我们可以在插入之前编写一个find函数,如果该数据存在,则不进行插入

HashDate<K, V>* Find(const K& key)
{//判断表为空的情况if (_table.size() == 0)return nullptr;size_t hashi = key % _table.size();while (_table[hashi]._state != EMPTY){//如果找到key了,并且状态不是DELETEif (_table[hashi]._kv.first == key && _table[hashi]._state != DELETE){return &_table[hashi];}hashi++;//如果超过表的长度,则除以表的大小,让其回到表头。hashi %= _table.size();}return nullptr;
}

此时有一个问题, 我们的循环是_state !=EMPT,如果遍历重回到起点,这些遍历到的数据_state都为EMPTY,就可能导致死循环,所以我们还要保存起始位置的状态,如果重回起点则也返回false(当然,这是一种非常极端的情况,但是会出现)。

4. Erase删除

删除的思路非常简单,如果find查找到该值,直接将其对应的state改为DELETE即可。

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

5. 插入复杂类型

那如果我们想实现一个统计次数的哈希表,则 key 值是string类型的怎么办呢?string类型或字符串类型是无法被取模的。那再如果我们想插入一个自己定义的复杂类型呢?

我们先来看看STL库中是如何解决这个问题的。

在这里插入图片描述
所以我们要编写默认的hash取key的仿函数作为缺省参数。

在这里插入图片描述

但是在库中的unordered_map并不需要我们自己传入仿函数,因为string是一个挺常见的类型,库中使用了模板的特化,对string类型进行了特殊处理,我们接下来也将其进行改动为特化的形式。

template<>
struct HashFunc<string>
{size_t operator()(const string& key){size_t val = 0;for (auto ch : key)val = val * 131 + ch;return val;}
};

6. 二次探测

二次探测不是指探测两次,而是 i 的指数方进行探测。

如下是使用线性探测和二次探测插入同一组数据的插入结果,如下:

在这里插入图片描述

然后我们在线性探测的方式上进行改动:

在这里插入图片描述

三.源码与测试用例

1. hash

enum State { EMPTY, EXIST, DELETE };   //每个位置有三种状态template<class K, class V>
struct HashDate
{pair<K, V> _kv;State _state= EMPTY;
};template<class K>
struct HashFunc
{size_t operator()(const K& key){return (size_t)key;}
};template<>
struct HashFunc<string>
{size_t operator()(const string& key){size_t val = 0;for (auto ch : key)val = val * 131 + ch;return val;}
};template<class K, class V,class Hash=HashFunc<K>>
class HashTable
{
public:bool Insert(const pair<K, V>& kv){//如果表中已经存在该数据if (Find(kv.first))	return false;//如果 size==0 或装载因子 >=0.7 进行扩容if (_table.size() == 0 || 10 * _size / _table.size() >= 7){size_t newSize = _table.size() == 0 ? 10 : _table.size() * 2;HashTable<K, V, Hash> newHash;newHash._table.resize(newSize);//将旧表中的数据拷贝到新表中  --- 复用Insert继续拷贝数据for (auto e : _table){if (e._state == EXIST){newHash.Insert(e._kv);}}//进行交换  newHash自动调用其析构函数_table.swap(newHash._table);}Hash hash;size_t hashi = hash(kv.first) % _table.size();while (_table[hashi]._state == EXIST)   //如果存在数据就一直往后找{hashi++;//如果hashi++超过size(),需要绕回数组的开始hashi %= _table.size();}//找到位置,插入数据_table[hashi]._kv = kv;_table[hashi]._state = EXIST;++_size;return  true;}HashDate<K,V>*  Find(const K& key){//判断表为空的情况if (_table.size() == 0)return nullptr;Hash hash;size_t hashi = hash(key) % _table.size();while (_table[hashi]._state != EMPTY){//如果找到key了,并且状态不是DELETEif (_table[hashi]._kv.first == key && _table[hashi]._state!=DELETE){return &_table[hashi];}hashi++;//如果超过表的长度,则除以表的大小,让其回到表头。hashi %= _table.size();}return nullptr;}bool Erase(const K& key){HashDate<K, V>* ret = Find(key);if (ret){ret->_state = DELETE;--_size;return true;}return false;}void Print(){for(int i=0;i< _table.size();i++){if (_table[i]._state == EXIST)cout <<"i:" <<i<<" [" << _table[i]._kv.first << " " << _table[i]._kv.second <<"]" << endl;}}private:vector<HashDate<K, V>> _table;size_t _size=0;
};

2. 测试用例

void test_hash01()
{HashTable<int, int> Hash;int a[] = { 1,11,4,15,26,7};for (auto e : a){Hash.Insert(make_pair(e, e));}Hash.Print();cout << endl;
}
void test_hash02()
{HashTable<int, int> Hash;int a[] = { 1,11,4,15,26,7,13,5,34,9 };for (auto e : a){Hash.Insert(make_pair(e, e));}Hash.Print();cout << endl;
}void test_hash03()
{HashTable<int, int> Hash;int a[] = { 1,11,4,15,26,7,13,5,34,9 };for (auto e : a){Hash.Insert(make_pair(e, e));}Hash.Print();cout << endl<<"find:"<<endl;cout << (Hash.Find(11)->_kv).first << endl;cout << (Hash.Find(4)->_kv).first << endl;cout << (Hash.Find(5)->_kv).first << endl;cout << (Hash.Find(34)->_kv).first << endl;cout << "Erase:" << endl;Hash.Erase(11);cout << Hash.Find(11) << endl;
}void test_hash04_string()
{string arr[] = { "苹果","西瓜","菠萝","草莓","菠萝","草莓" ,"菠萝","草莓" , "西瓜", "菠萝", "草莓", "西瓜", "菠萝", "草莓","苹果" };HashTable<string,int> countHT;for (auto& str : arr){auto ptr = countHT.Find(str);if (ptr)ptr->_kv.second++;elsecountHT.Insert({ str,1 });}countHT.Print();
}void test_hash05_string()
{HashFunc<string> hash;cout << hash({ "abc" }) << endl;cout << hash({ "bac" }) << endl;cout << hash({ "cba" }) << endl;cout << hash({ "bbb" }) << endl;
}

本文到此结束, 码文不易, 还请多多支持哦 ! ! !

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

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

相关文章

【MapGIS精品教程】010:空间叠置分析案例教程

文章目录 一、叠置分析介绍(一) 什么是叠加分析(二)叠加分析的分类二、叠加分析操作一、叠置分析介绍 (一) 什么是叠加分析 叠加分析是依靠把分散在不同层上的空间属性信息按相同的空间位置加到一起,合为新的一层。该层的属性由被叠加层各自的属性组合而成,这种组合可…

Pytest测试框架3

目录&#xff1a; pytest结合数据驱动-yamlpytest结合数据驱动-excelpytest结合数据驱动-csvpytest结合数据驱动-jsonpytest测试用例生命周期管理&#xff08;一&#xff09;pytest测试用例生命周期管理&#xff08;二&#xff09;pytest测试用例生命周期管理&#xff08;三&a…

【java安全】原生反序列化利用链JDK7u21

文章目录 【java安全】原生反序列化利用链JDK7u21前言原理equalsImpl()如何调用equalsImpl()&#xff1f;HashSet通过反序列化间接执行equals()方法如何使hash相等&#xff1f; 思路整理POCGadget为什么在HashSet#add()前要将HashMap的value设为其他值&#xff1f; 【java安全】…

轮足机器人硬件总结

简介 本文主要根据“轮腿机器人Hyun”总结的硬件部分。 轮腿机器人Hyun开源地址&#xff1a;https://github.com/HuGuoXuang/Hyun 1 电源部分 1.1 78M05 78M05是一款三端稳压器芯片&#xff0c;它可以将输入电压稳定输出为5V直流电压. 1.2 AMS1117-3.3 AMS1117-3.3是一种输…

【2.1】Java微服务:详解Hystrix

✅作者简介&#xff1a;大家好&#xff0c;我是 Meteors., 向往着更加简洁高效的代码写法与编程方式&#xff0c;持续分享Java技术内容。 &#x1f34e;个人主页&#xff1a;Meteors.的博客 &#x1f49e;当前专栏&#xff1a; 深度学习 ✨特色专栏&#xff1a; 知识分享 &…

pytorch实战-图像分类(二)(模型训练及验证)(基于迁移学习(理解+代码))

目录 1.迁移学习概念 2.数据预处理 3.训练模型&#xff08;基于迁移学习&#xff09; 3.1选择网络&#xff0c;这里用resnet 3.2如果用GPU训练&#xff0c;需要加入以下代码 3.3卷积层冻结模块 3.4加载resnet152模 3.5解释initialize_model函数 3.6迁移学习网络搭建 3.…

DAY03_Spring—SpringAOPAOP切入点表达式AOP通知类型Spring事务管理

目录 一 AOP1 AOP简介问题导入1.1 AOP简介和作用1.2 AOP中的核心概念 2 AOP入门案例问题导入2.1 AOP入门案例思路分析2.2 AOP入门案例实现【第一步】导入aop相关坐标【第二步】定义dao接口与实现类【第三步】定义通知类&#xff0c;制作通知方法【第四步】定义切入点表达式、配…

服务蓝图:提升和改善服务系统的工具

服务蓝图&#xff1a;提升和改善服务系统的工具 Service Blueprint 翻译成服务提供计划比较恰当 趣讲大白话&#xff1a;精细耕耘&#xff0c;才有好体验 【趣讲信息科技249期】 **************************** 西方擅长的是工具和方法 把一件事情透过工具和方法做到人人能懂 日…

TypeScript 中【class类】与 【 接口 Interfaces】的联合搭配使用解读

导读&#xff1a; 前面章节&#xff0c;我们讲到过 接口&#xff08;Interface&#xff09;可以用于对「对象的形状&#xff08;Shape&#xff09;」进行描述。 本章节主要介绍接口的另一个用途&#xff0c;对类的一部分行为进行抽象。 类配合实现接口 实现&#xff08;impleme…

<van-empty description=““ /> 滚动条bug

使用 <van-empty description"" /> 时&#xff0c;图片出现了个滚动条&#xff0c;图片可以上下滑动。 代码如下&#xff1a; <block wx:if"{{courseList.length < 0}}"><van-empty description"" /> </block> <…

使用toad库进行机器学习评分卡全流程

1 加载数据 导入模块 import pandas as pd from sklearn.metrics import roc_auc_score,roc_curve,auc from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression import numpy as np import math import xgboost as xgb …

Windows同时安装两个版本的JDK并随时切换,以JDK6和JDK8为例,并解决相关存在的问题(亲测有效)

Windows同时安装两个版本的JDK并随时切换&#xff0c;以JDK6和JDK8为例&#xff0c;并解决相关存在的问题&#xff08;亲测有效&#xff09; 1.下载不同版本JDK 这里给出JDK6和JDK的百度网盘地址&#xff0c;具体安装过程&#xff0c;傻瓜式安装即可。 链接&#xff1a;http…