闭散列哈希表

一、什么是 哈希

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;
        }
        else
            return false;
    }

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Spring】Spring 整合 Junit、MyBatis

一、 Spring 整合 Junit <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache…

云端地球联动大疆机场,支撑矿山高效巡检与智能监测

矿产资源是我国的重要战略性资源。近年来&#xff0c;随着矿山开采深度的逐渐增加&#xff0c;露天矿山边坡滑落等灾害频繁发生&#xff0c;威胁人民群众生命与财产安全。因此&#xff0c;对露天矿边坡进行快速、实时、有效的形变监测和预警已成为当前我国矿山防灾与安全生产的…

【CCF-CSP】202403-4 十滴水

题目描述 十滴水是一个非常经典的小游戏。 小 C 正在玩一个一维版本的十滴水游戏。我们通过一个例子描述游戏的基本规则。 游戏在一个 1c 的网格上进行&#xff0c;格子用整数x(1≤x≤c) 编号&#xff0c;编号从左往右依次递增。网格内 m 个格子里有 1∼41∼4 滴水&#xff0…

Python:如何将嵌套列表展平成单层列表

在Python中&#xff0c;我们经常会遇到需要处理列表的情况&#xff0c;尤其是嵌套列表。嵌套列表就是一个列表中包含另一个或多个列表。有时&#xff0c;我们需要将这些嵌套的列表展平成一个单层的列表&#xff0c;以便于进一步的处理和分析。本文将介绍如何使用Python将嵌套列…

【架构】MVC架构模式 三层架构

1 不使用MVC架构模式完成银行账户转账 <% page contentType"text/html;charsetUTF-8" language"java" %> <html><head><base href"${pageContext.request.scheme}://${pageContext.request.serverName}:${pageContext.request.…

Redis加入系统服务,开机自启

vi /etc/systemd/system/redis.service i :wq #加载服务配置文件 systemctl daemon-reload #启动redis systemctl start redis #设置开机自启 systemctl enable redis #查看启动状态 systemctl status redis

每日一练 2024.5.10

题目&#xff1a; 给定一个非负整数数组 nums&#xff0c; nums 中一半整数是 奇数 &#xff0c;一半整数是 偶数 。 对数组进行排序&#xff0c;以便当 nums[i] 为奇数时&#xff0c;i 也是 奇数 &#xff1b;当 nums[i] 为偶数时&#xff0c; i 也是 偶数 。 示例 1&#…

Day 29 MySQL的主从复制集群

一&#xff1a;主从复制 1.主从复制概念 什么是主从复制&#xff1a; ​ 主从复制&#xff0c;是用来建立一个和主数据库完全一样的数据库环境&#xff0c;称为从数据库&#xff1b;主数据库一般是准实时的业务数据库 主从复制的作用&#xff1a; ​ 做数据的热备&#xf…

【Python 下载大量品牌网站的图片(一)】关于图片的处理和下载,吃满带宽,可多开窗口下载多个网站,DOS窗口类型

写作日期&#xff1a;2024.05.11 使用工具&#xff1a;Python 可修改功能&#xff1a;线程量、UA、Cookie、代理、存储目录、间隔时间、超时时间、图片压缩、图片缩放 默认功能&#xff1a;图片转换、断续下载、图片检测、路径处理、存储文件 GUI&#xff1a;DOS窗口 类型&…

K8s源码分析(二)-K8s调度队列介绍

本文首发在个人博客上&#xff0c;欢迎来踩&#xff01; 本次分析参考的K8s版本是 文章目录 调度队列简介调度队列源代码分析队列初始化QueuedPodInfo元素介绍ActiveQ源代码介绍UnschedulableQ源代码介绍**BackoffQ**源代码介绍队列弹出待调度的Pod队列增加新的待调度的Podpod调…

C++:week3:数据结构与算法

文章目录 (十一) 常用数据结构1.动态数组(1)模型(2).h与.c(3)实现 2.链表(1)模型(2)分类(3)基本操作(API)(4)实现(5)链表常见面试题(6)空间与时间 3.栈(1)模型(2)基本操作(3)实现(4)栈的应用 4.队列(1)模型(2)基本操作(API)(3)实现(4)队列的应用 5.哈希表(1)哈希表的提出原因(2…

支付宝小程序如何去除页面下拉回弹

描述&#xff1a;支付宝小程序页面下拉时会产生回弹&#xff0c;如果页面上有拖拽功能&#xff0c;会有影响 解决方法&#xff1a; 页面xx.config.js中设置&#xff1a;allowsBounceVertical: “NO” 官方文档&#xff1a;https://opensupport.alipay.com/support/FAQ/7110b5d…

什么是Jetpack

Jetpack Jetpack 是一套组件库、工具&#xff0c;可帮助开发人员遵循最佳做法&#xff0c;减少样板代码并编写可在 Android 版本和设备上一致工作的代码&#xff0c;以便开发人员可以专注于他们关心的代码 组成 主要包含四部分&#xff1a;架构&#xff08;Architecture&…

手写一个SPI FLASH 读写擦除控制器(未完)

文章目录 flash读写数据的特点1. 扇擦除SE&#xff08;Sector Erase&#xff09;1.1 flash_se 模块设计1.1.1 信号连接示意图&#xff1a;1.1.2 SE状态机1.1.3 波形图设计&#xff1a;1.1.4 代码 2. 页写PP(Page Program)2.1 flash_pp模块设计2.1.1 信号连接示意图&#xff1a;…

社交媒体数据恢复:快手、快手极速版

一、备份快手数据 登录快手账号&#xff1a;首先&#xff0c;打开快手APP&#xff0c;登录您的快手账号。 进入设置&#xff1a;在快手首页点击右上角的三条横线图标&#xff0c;进入设置页面。 数据备份&#xff1a;在设置页面找到“备份与恢复”选项&#xff0c;点击进入。…

【机器学习】 人工智能和机器学习辅助决策在空战中的未来选择

&#x1f680;传送门 &#x1f680;文章引言&#x1f512;技术层面&#x1f4d5;作战结构&#x1f308;替代决策选项&#x1f3ac;选项 1&#xff1a;超级战争&#xff08;Hyperwar&#xff09;&#x1f320;选项 2&#xff1a;超越OODA&#x1f302;选项 3&#xff1a;阻止其他…

【漏洞复现】用友U8-Cloud XChangeServlet XXE漏洞

0x01 产品简介 用友U8Cloud是用友推出的新一代云ERP,主要聚焦成长型、创新型企业,提供企业级云ERP整体解决方案。 0x02 漏洞概述 用友U8 cloud /service/XChangeServlet接口存在XXE漏洞,未授权的攻击者可通过此漏洞获取数据库敏感信息,从而盗取服务器数据,造成服务器信…

API接口开发实现一键智能化自动抓取电商平台热销商品数据支持高并发免费接入示例

为了实现一键智能化自动抓取电商平台热销商品数据支持高并发免费接入&#xff0c;你可以使用Python编程语言和相关库&#xff08;如requests、BeautifulSoup等&#xff09;来开发一个API接口&#xff0c;也可以使用封装好的api接口获取&#xff0c;注册一个api账号获取key和sec…

代码审计平台sonarqube的安装及使用

docker搭建代码审计平台sonarqube 一、代码审计关注的质量指标二、静态分析技术分类三、使用sonarqube的目的四、sonarqube流程五、docker快速搭建sonarqube六、sonarqube scanner的安装和使用七、sonarqube对maven项目进行分析八、sonarqube分析报告解析九、代码扫描规则定制十…

使用 AI Assistant for Observability 和组织的运行手册增强 SRE 故障排除

作者&#xff1a;Almudena Sanz Oliv, Katrin Freihofner, Tom Grabowski 通过本指南&#xff0c;你的 SRE 团队可以实现增强的警报修复和事件管理。 可观测性 AI 助手可帮助用户使用自然语言界面探索和分析可观测性数据&#xff0c;利用自动函数调用来请求、分析和可视化数据…