闭散列哈希表

一、什么是 哈希

1.1 哈希概念 与 哈希冲突

在正式介绍闭散列哈希之前,我们需要明确 哈希 的概念。

哈希 :构造一种数据存储结构,通过函数 HashFunc() ,使 元素的存储位置其对应的键值 建立一一映射关系,在此基础上,可以只凭借 O(1) 的时间复杂度查找到目标元素。

举一个过去我们常见的例子:

在统计字符串中各小写字母出现的次数时,我们通常创建 int count[26] = { 0 }; 的这样一个数组,'a' 与 下标为 0 的位置映射,'b' 与 下标为 1 的位置映射,以此类推。

通过以上举例可见,我们对 哈希 其实并不陌生,但是由此衍生出两个问题:

  1. 在数据范围集中时,我们可以通过开一定大小的空间实现下标与元素的一一映射;但如果出现这样一组分散的数据 1, 2, 12, 99, 10000, 6 呢?

  2. 提前把第一个问题的答案告诉各位: 除留余数法 可以解决问题 —— 开一个大小为 10 的数组,每个数据 % 10 后再存进数组中。

    但,如何避免 “哈希冲突” —— 不同的键值计算出相同的哈希地址 呢?比如:2 % 10 == 12 % 10 == 2,如何规避二者冲突的问题?

1.2 哈希函数

引起哈希冲突的原因很可能是:哈希函数设计的不够合理 —— 哈希函数最好能保证所有元素均匀地分布在整个哈希空间中

常见的哈希函数:

  1. 直接定址法。比如:小写字母次数映射。

  2. 除留余数法。

二、闭散列

闭散列:开放定址法 —— 如果发生了 “哈希冲突” 且当前的哈希空间并未被“填满”,此时,把新插入的冲突元素存到 “下一个”空位置 去。

2.1 线性探测

2 % 10 == 12 % 10 == 2 发生了哈希冲突,同时 下标为 2 的下一个位置 —— 下标为 3 为空,就把 12 放在这里;

如果 下标为 3 位置也已经存了元素,就一直往后找 —— 在哈希空间中循环查找,直到找到一个空位置,再把元素插入其中。

通过上面的解释,相信大家已经明了 线性探测 的基本要义,下面再给出它的定义。

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

2.2 引入状态量

假定我们要将 2 删除,同时插入 32 —— 拷贝一张新的哈希表,再将除了 2 以外的其他数据插入新表,这种做法显然太低效;

如果把 2 以后的每个元素往前移,则改变了元素与哈希地址的映射关系。

因此,我们需要在每个哈希地址增加一个状态量 —— EMPTY(空),EXIST(存在),DELETE(删除),默认构造把所有位置初始化为 EMPTY ,插入元素的同时将 EMPTY 改为 EXIST ,删除元素再将 EXIST 改为 DELETE

通过每个哈希位置的状态量,判断此处是否为空,是否可以插入元素等等。

2.3 闭散列的框架搭建
  • 枚举状态量

    enum State{EMPTY,EXIST,DELETE};
  • HashData

    template<class K, class V>struct HashData{pair<K, V> _kv;State _state = EMPTY; // 默认初始化为空};    
  • HashTable

    template<class K, class V>class HashTable{public:HashTable(size_t n = 10){_tables.resize(n);// resize() 可以保证 size == capacity}private:vector<HashData<K, V>> _tables;size_t _n = 0;// 当前哈希表中的元素个数};
2.4 Insert() 及引入 HashFunc()

解释一个概念 :负载因子 = 哈希表中所存元素的个数 / 表的大小

    // 先给出一个基本的 Insert 函数bool Insert(const pair<K, V>& kv){if (Find(kv.first)) // 未实现的 Find(),找到了返回映射的哈希位置指针,没找到返回空{return false; // 已经存在,插入失败}// 扩容逻辑if ((double)_n / _tables.size() >= 0.7) // 将 负载因子 控制在 0.7{// 创建一个空间为 原表两倍大小 的表HashTable<K, V> newTable(2 * _tables.size()); for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]._state == EXIST)newTable.Insert(_tables[i]._kv); }_tables.swap(newTable._tables);}// 插入逻辑size_t hashi = kv.first % _tables.size(); // 计算相应的 哈希地址while (_tables[hashi]._state == EXIST)// 线性探测{hashi++;hashi %= _tables.size();}// 代码运行到这里则必然找到了一个可以插入的位置_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST; // 修改对应状态_n++;return true;}
​void Test_Insert1(){int arr[] = { 1, 4, 24, 34, 7, 44, 17, 20 };HashTable<int, int> ht;for (auto e : arr){ht.Insert(make_pair(e, e));}}

扩容逻辑中复用 Insert() 的部分确实精妙绝伦,newTable 的 size 是原表的 2 倍,因此在插入过程中,不会出现重复扩容进而死循环的状态。

以上的 Insert() 看上去似乎没什么问题,可是,一旦我们把传入两个 string —— HashTable<string, string> ,再 Insert(make_pair<"sort", "排序">) 就出问题了 —— string 类型不支持 size_t hashi = kv.first % _tables.size(); 的方式计算哈希地址!

所以我们需要一个哈希函数 —— HashFunc()(仿函数) ,用于将任意长度的输入数据映射到固定长度输出值(哈希值或散列值)

    template<class K>struct HashFunc{size_t operator()(const K& key){size_t ret = key;return ret;}};
​// 为 string 写一个特化版本template<>struct HashFunc<string>{size_t operator()(const string& s){size_t hash = 0;for (auto& e : s){hash = hash * 131 + e; // 131 是前辈用大量数据测试得到的值,可以尽大程度避免哈希冲突}return hash;}};

有了 HashFunc,我们再对以上的内容做一下改造:

    template<class K, class V, class Hash = HashFunc<K>>class HashTable{public:HashTable(size_t n = 10){_tables.resize(n);}bool Insert(const pair<K, V>& kv){Hash hs;if (Find(kv.first)) {return false;}
​// 扩容逻辑if ((double)_n / _tables.size() >= 0.7) {HashTable<K, V> newTable(2 * _tables.size()); for (size_t i = 0; i < _tables.size(); i++){if (_tables[i]._state == EXIST)newTable.Insert(_tables[i]._kv); }_tables.swap(newTable._tables);}
​// 插入逻辑size_t hashi = hs(kv.first) % _tables.size(); // hs(kv.first) 利用哈希函数计算 映射的哈希地址while (_tables[hashi]._state == EXIST){hashi++;hashi %= _tables.size();}_tables[hashi]._kv = kv;_tables[hashi]._state = EXIST; _n++;return true;}private:vector<HashData<K, V>> _tables;size_t _n = 0;};

再运行一下:

    void Test_Insert2(){HashTable<string, string> ht;ht.Insert(make_pair("sort", "排序"));ht.Insert(make_pair("iterator", "迭代器"));
​}

就不会出错啦!

Hash 是一个模板接口,当自定义类型不支持仿函数模板推演的时候,你可以传入自己的 HashFunc 完成对应功能!

2.5 Find()Erase()
    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;}

中间过程,有些值可能被删除 —— 状态为 DELETE。

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

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

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

相关文章

GA-CNN-LSTM多输入分类|遗传算法-卷积-长短期神经网络|Matlab

目录 一、程序及算法内容介绍&#xff1a; 基本内容&#xff1a; 亮点与优势&#xff1a; 二、实际运行效果&#xff1a; 三、算法介绍&#xff1a; 四、完整程序下载&#xff1a; 一、程序及算法内容介绍&#xff1a; 基本内容&#xff1a; 本代码基于Matlab平台编译&am…

el-checkbox选中后的值为id,组件显示为label中文

直接上代码 方法一 <el-checkbox v-for"item in list" :key"item.id" :label"item.id">{{中文}} </el-checkbox> 方法二 <el-checkbox-group class"flex_check" v-model"rkStatusList" v-for"item…

css 案例 横向滚动渐变

效果 完整代码&#xff1a; <template><view class"content"><view class"tab"><view class"tab-item" v-for"(item,index) in tab" :key"index" click"handlerTab(index)":class"ind…

简单的神经网络

一、softmax的基本概念 我们之前学过sigmoid、relu、tanh等等激活函数&#xff0c;今天我们来看一下softmax。 先简单回顾一些其他激活函数&#xff1a; Sigmoid激活函数&#xff1a;Sigmoid函数&#xff08;也称为Logistic函数&#xff09;是一种常见的激活函数&#xff0c…

Ubuntu20.04 设置路由器

1. 网络拓扑图 2. 查看网卡信息 ip a得出如下网卡信息&#xff0c;enp1s0和enp2s0为两个网卡名称&#xff0c;以及相关两个网卡的详细信息&#xff0c;不同设备的网卡名称可能不一样 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group defaul…

【UE】制作自己的插件

步骤 1. 打开插件面板&#xff0c;在界面左上角点击“添加”按钮 2. 在“新插件”界面先点击“纯内容”然后点击“创建插件” 此时在项目工程目录下的“Plugins”文件夹中就可以看到我们创建的插件 3. 如果想在自己创建的插件中添加功能&#xff0c;我们可以在项目浏览器中的“…

SSM【Spring SpringMVC Mybatis】——Mybatis(二)

如果对一些基础理论感兴趣可以看这一期&#x1f447; SSM【Spring SpringMVC Mybatis】——Mybatis 目录 1、Mybatis中参数传递问题 1.1 单个普通参数 1.2 多个普通参数 1.3 命名参数 1.4 POJO参数 1.5 Map参数 1.6 Collection|List|Array等参数 2、Mybatis参数传递【#与…

14:java基础-Tomcat-Web容器

文章目录 面试题Web 容器是什么&#xff1f;HTTP 的本质 面试题 Web 容器是什么&#xff1f; 让我们先来简单回顾一下 Web 技术的发展历史&#xff0c;可以帮助你理解 Web 容器的由来。早期的 Web 应用主要用于浏览新闻等静态页面&#xff0c;HTTP 服务器&#xff08;比如Apa…

ArcGIS10.2能用了10.2.2不行了(解决)

前两天我们的推文介绍了 ArcGIS10.2系列许可到期解决方案-CSDN博客文章浏览阅读2次。本文手机码字&#xff0c;不排版了。 昨晚&#xff08;2021\12\17&#xff09;12点后&#xff0c;收到很多学员反馈 ArcGIS10.2系列软件突然崩溃。更有的&#xff0c;今天全单位崩溃。​提示许…

社交媒体数据恢复:九信极速版

九信极速版是一款功能强大的社交软件&#xff0c;但在使用过程中&#xff0c;您可能会遇到数据丢失的问题&#xff0c;如聊天记录、好友等。不用担心&#xff0c;九信极速版提供了数据恢复功能&#xff0c;帮助您找回丢失的数据。以下是详细的恢复步骤&#xff1a; 一、恢复聊…

Spring如何控制Bean的加载顺序

前言 正常情况下&#xff0c;Spring 容器加载 Bean 的顺序是不确定的&#xff0c;那么我们如果需要按顺序加载 Bean 时应如何操作&#xff1f;本文将详细讲述我们如何才能控制 Bean 的加载顺序。 场景 我创建了 4 个 Class 文件&#xff0c;分别命名为 FirstInitialization Se…

(docker)进入容器后如何使用本机gpu

首次创建容器&#xff0c;不能直接使用本机gpu 在系统终端进行如下配置&#xff1a; 1.安装NVIDIA Container Toolkit 进入Nvidia官网Installing the NVIDIA Container Toolkit — NVIDIA Container Toolkit 1.15.0 documentation&#xff0c;安装NVIDIA Container Toolkit …