闭散列哈希表

news2024/11/18 1:38:40

一、什么是 哈希

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;
    }

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

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

相关文章

Qt Excel读写 - QXlsx读取Excel文件显示到QTableWidget

Qt Excel读写 - QXlsx读取Excel文件显示到QTableWidget 引言一、设计思路二、核心源码三、其他参考链接 引言 QXlsx官方显示的例子中&#xff0c;有一个XlsxFactory可以Load xlsx file and display on Qt widgets.但是其包含商业许可…自己写了一个简化版本&#xff1a;可以读取…

深度学习--生成对抗网络GAN

GAN简介 让我们先来简单了解一下GAN GAN的全称是Generative Adversarial Networks&#xff0c;中文称为“生成对抗网络”&#xff0c;是一种在深度学习领域广泛使用的无监督学习方法。 GAN主要由两部分组成&#xff1a;生成器和判别器。生成器的目标是尽可能地生成真实的样本数…

深度学习之前馈神经网络

1.导入常用工具包 #在终端中输入以下命令就可以安装工具包 pip install numpy pip install pandas Pip install matplotlib注&#xff1a; numpy是科学计算基础包 pandas能方便处理结构化数据和函数 matplotlib主要用于绘制图表。 #导包的代码&#xff1a; import numpy as n…

java对象互换工具类

1:将Object类型转成json字符串 /*** 将对象转为字符串* param obj* return*/public static String toString(Object obj) {if(obj null) {return null;}if ("".equals(obj.toString())) {return null;}if (obj instanceof String) {return obj.toString();}try {Ob…

c#教程——索引器

前言&#xff1a; 索引器&#xff08;Indexer&#xff09;可以像操作数组一样来访问对象的元素。它允许你使用索引来访问对象中的元素&#xff0c;就像使用数组索引一样。在C#中&#xff0c;索引器的定义方式类似于属性&#xff0c;但具有类似数组的访问方式。 索引器&#x…

2010-2030年GHS-POP数据集下载

扫描文末二维码&#xff0c;关注微信公众号&#xff1a;ThsPool 后台回复 g008&#xff0c;领取 2010-2030年100m分辨率GHS-POP 数据集 &#x1f4ca; GHS Population Grid (R2023)&#xff1a;全球人口分布的精准视图与深度应用 &#x1f310; 在全球化和快速城市化的今天&am…

2024-05-10 Ubuntu上面使用libyuv,用于转换、缩放、旋转和其他操作YUV图像数据,测试实例使用I420ToRGB24

一、简介&#xff1a;libyuv 最初是由Google开发的&#xff0c;主要是为了支持WebRTC项目中的视频处理需求。用于处理YUV格式图像数据的开源库。它提供了一系列的函数&#xff0c;用于转换、缩放、旋转和其他操作YUV图像数据。 二、执行下面的命令下载和安装libyuv。 git clo…

【全开源】JAVA台球助教台球教练多端系统源码支持微信小程序+微信公众号+H5+APP

功能介绍 球厅端&#xff1a;球厅认证、教练人数、教练的位置记录、助教申请、我的项目、签到记录、我的钱包、数据统计 教练端&#xff1a;我的页面&#xff0c;数据统计、订单详情、保证金、实名认证、服务管理、紧急求助、签到功能 用户端&#xff1a;精准分类、我的助教…

FonePaw Data Recovery for Mac:轻松恢复丢失数据

FonePaw Data Recovery for Mac是一款功能强大的数据恢复软件&#xff0c;专为Mac用户设计&#xff0c;帮助用户轻松恢复因各种原因丢失的数据。该软件支持从硬盘驱动器、存储卡、闪存驱动器等存储介质中恢复丢失或删除的文件&#xff0c;包括照片、视频、文档、电子邮件、音频…

2024最新版付费进群系统源码全开源

源码说明&#xff1a; 下 载 地 址 &#xff1a; runruncode.com/php/19758.html 2024最新修复版独立付费进群系统源码全开源&#xff0c;基于ThinkPHP框架开发。 1、修复SQL表 2、修复支付文件 3、修复支付图标不显示 4、修复定位、分销逻辑、抽成逻辑 5、新增支持源支…

AVL树的旋转

目录 1.平衡因子 2.旋转 a.节点定义 b.插入 插入 平衡因子更新 旋转 左单旋 右单旋 右左双旋 左右双旋 3.AVL树的验证 1.平衡因子 我们知道搜索二叉树有缺陷&#xff0c;就是不平衡&#xff0c;比如下面的树 什么是搜索树的平衡&#xff1f;就是每个节点的左右子树的…

Web实时通信的学习之旅:轮询、WebSocket、SSE的区别以及优缺点

文章目录 一、通信机制1、轮询1.1、短轮询1.2、长轮询 2、Websocket3、Server-Sent Events 二、区别1、连接方式2、协议3、兼容性4、安全性5、优缺点5.1、WebSocket 的优点&#xff1a;5.2、WebSocket 的缺点&#xff1a;5.3、SSE 的优点&#xff1a;5.4、SSE 的缺点&#xff1…

刷代码随想录有感(62):修建二叉搜索树

题干&#xff1a; 代码&#xff1a; class Solution { public:TreeNode* traversal(TreeNode* root, int low, int high){if(root NULL)return NULL;if(root->val < low)return traversal(root->right, low, high);if(root->val > high)return traversal(ro…

新版文件同步工具(Python编写,其中同时加入了多进程计算MD5、多线程复制大文件、多协程复制小文件、彩色输出消息、日志功能)

两个月前&#xff0c;接到一个粉丝的要求&#xff0c;说希望在我之前编写的一个python编写的文件同步脚本(Python编写的简易文件同步工具(已解决大文件同步时内存溢出问题)https://blog.csdn.net/donglxd/article/details/131225175)上加入多线程复制文件的功能&#xff0c;前段…

Aapache Tomcat AJP 文件包含漏洞(CVE-2020-1938)

1 漏洞描述 CVE-2020-1938 是 Apache Tomcat 中的一个严重安全漏洞&#xff0c;该漏洞涉及到 Tomcat 的 AJP&#xff08;Apache JServ Protocol&#xff09;连接器。由于 AJP 协议在处理请求时存在缺陷&#xff0c;攻击者可以利用此漏洞读取服务器上的任意文件&#xff0c;甚至…

ALV Color-颜色

目录 前言 实战 列颜色 行颜色 单元格颜色 前言 在ABAP ALV中&#xff0c;Color颜色设置是一种增强列表显示效果的重要手段&#xff0c;可以用来突出显示特定行、列或单元格&#xff0c;以吸引用户注意或传达数据的特定状态。 颜色设置中有优先级顺序&#xff0c;他们是单元格…

Mac电脑安装打开APP显示问题已损坏 问题解决

当MAC电脑安装完软件打开时&#xff0c;显示文件已损坏&#xff0c;无法打开。搜了很多教程终于找到解决方案&#xff0c;记录下方便以后再用。 我的mac电脑是intel芯片的&#xff0c;如果你遇到这个问题&#xff0c;可以参考我的这个方案。 1.首先当打开软件后出现 “xx软件已…

HTTPS 原理和 TLS 握手机制

HTTPS的概述与重要性 在当今数字化时代&#xff0c;网络安全问题日益凸显&#xff0c;数据在传输过程中的安全性备受关注。HTTPS 作为一种重要的网络通信协议&#xff0c;为数据的传输提供了强有力的安全保障。它是在 HTTP 的基础上发展而来&#xff0c;通过引入数据加密机制&a…

C++ BuilderXE 计算程序运行时间精确到毫秒

#include <time.h> // //计算时间 clock_t start,end,dtStart; startclock(); // ProgressBar1->Percent0; // // ProgressBar1->Percenti/DDnum*100; // Application->ProcessMessages(); // //操作完成计时 …

干货分享:AI知识库-从认识到搭建

随着知识库的出现&#xff0c;人工智能也逐渐加入进来&#xff0c;形成了“AI知识库”。也许将AI和知识库拆开&#xff0c;你能理解是什么意思&#xff0c;但是当两个词结合在一起时&#xff0c;你又真的能理解它是做什么的吗&#xff1f;这就是今天我们要来聊的话题&#xff0…