哈希表的底层实现(1)---C++版

news2024/11/22 19:30:51

目录

哈希表的基本原理

哈希表的优点

哈希表的缺点

应用场景

闭散列法

开散列法

开放定值法Open Addressing——线性探测的模拟实现

超大重点部分评析

链地址法Separate Chaining——哈希桶的模拟实现


哈希表(Hash Table)是一种数据结构,它通过将键(Key)映射到值(Value)的方式来实现快速的数据存储与查找。哈希表的核心概念是哈希函数,该函数用于将输入的键转换为哈希值(通常是一个整数),并根据这个哈希值将数据存储在数组中的特定位置。

哈希表的基本原理

  1. 哈希函数:这是哈希表的关键部分,它接收一个键并返回一个整数(哈希值)。理想情况下,哈希函数会将不同的键均匀地分布到数组的不同位置上。

  2. 数组(桶,Bucket):哈希表通常是基于数组实现的,哈希函数的输出决定了键值对应该存储在数组中的哪个位置。

  3. 处理冲突:由于不同的键可能会生成相同的哈希值(称为哈希冲突),需要有机制来处理这些冲突。常见的处理方式包括:

    • 链地址法(Separate Chaining):每个数组位置存储一个链表,所有哈希值相同的键值对都存储在同一个链表中。
    • 开放地址法(Open Addressing):当冲突发生时,查找数组中的下一个空闲位置存储键值对,常见的方式包括线性探测、二次探测和双重散列。
  4. 查找和插入:哈希表允许以 O(1) 的时间复杂度进行查找和插入操作(在理想情况下),即只需一次哈希函数计算和一次数组访问即可找到或插入数据。

哈希表的优点

  • 快速的查找、插入和删除:在平均情况下,哈希表的查找、插入和删除操作都是常数时间(O(1)),这使其非常高效。
  • 简单实现:哈希表相对容易实现,且不需要复杂的数据结构操作。

哈希表的缺点

  • 空间浪费:如果哈希表的负载因子(存储的元素数量与数组大小的比值)较高,容易导致冲突增加,从而降低性能。为避免冲突,通常会使用更大的数组,这会导致空间浪费。
  • 无法有序遍历:哈希表中的数据是无序的,如果需要顺序访问数据,需要额外的处理。
  • 哈希函数质量:哈希表的性能高度依赖于哈希函数的质量,差劲的哈希函数可能导致较多的冲突。

应用场景

哈希表在许多场景中广泛使用,包括但不限于:

  • 字典实现:例如,Python 中的字典(dict)就是通过哈希表实现的。
  • 缓存:哈希表常用于实现缓存机制,如 LRU 缓存。
  • 集合操作:例如,在查找某个元素是否在集合中时,哈希表可以显著提高效率。
  • 位图:
  • 布隆过滤器:

通过哈希表,可以在大型数据集上快速执行查找、插入和删除操作,这使得它在实际应用中非常实用。

所以依照哈希表的基本原理可以看出哈希表的底层结构可以分为以下两种,这两种方法的本质都是除留余数法:

闭散列法

闭散列法也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有 空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置 呢? 比如2.1中的场景,现在需要插入元素44,先通过哈希函数计算哈希地址,hashAddr为4, 因此44理论上应该插在该位置,但是该位置已经放了值为4的元素,即发生哈希冲突。从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。

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

删除:采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素 会影响其他元素的搜索。比如删除元素4,如果直接删除掉,44查找起来可能会受影 响。因此线性探测采用标记的伪删除法来删除一个元素。

上面有一个名词为线性探测,就是当待插入元素出现哈希冲突时,一个一个往下寻找空位置称为线性探测,每次n的固定次方倍往下找称为二次探测,两种探测的本质解决的问题完全一致,所有我们就只使用线性探测进行实现。

开散列法

开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地 址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链 接起来,各链表的头结点存储在哈希表中。代码总体实现的功能和上面的闭散列法差不多。

从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。

开放定值法Open Addressing——线性探测的模拟实现

#include<iostream>
#include<vector>
using namespace std;

//namespace hashbucket//包一个命名空间就可以调用了
//{

    template<class K>
    struct Hashfunc
    {
        size_t operator()(const K& key)
        {
            return (size_t)key;
//由于像当K为string时不可以直接强制转换成整型,所以如果哈希表需要插入string类型的数据就需要写string的特化版本,如下
        }
    };
    
//struct StringHashFunc
    //{
    //    size_t operator()(const string& s)
    //    {
    //        size_t hash = 0;
    //        for (auto e : s)
    //        {
    //            hash *= 31;
    //            hash += e;
    //        }
    //
    //        return hash;
    //    }
    //};

    template<>
    struct Hashfunc<string>
//可以直接在这里写,模板的特化前提是前面有写过了,且名称一样,遇到和特化吻合的会优先匹配特化版本
    {
        size_t operator()(const string& key)
        {
            size_t hash = 0;
            for (auto e : key)
            {
                hash += e;
                hash *= 31;
//根据某篇文章可得多乘31可以有效避免很多重复
            }
            return hash;
        }
    };
    enum status
    {
        EXIST,
        EMPTY,
        DELETE
//三个状态,这里定义了哈希表中的数据的多个状态,在红黑树的模拟实现中有用过,这样方便判断此位置的数据状态,方便以后的线性探测和删除数据等
    };

    template<class K, class V>
    struct Hashdata
    {
        pair<K, V> _kv;
        status s = EMPTY;
//刚开始时所有空位的状态都是EMPTY
    };


    template<class K, class V, class Hash = Hashfunc<K>>
    class Hashtable
    {
    public:
        Hashtable()
        {
            _table.resize(10);
//直接在初始化时开10个空间
        }
        Hash ha;
//仿函数
        bool insert(const pair<K, V>& kv)
        {
            if (Find(kv.first))
            {
                return false;
            }
            if (10 * n / _table.size() >= 7)
            {
                
//vector<HashData<K, V>> newTables(_tables.size() * 2);
                 遍历旧表, 将所有数据映射到新表
                 ...
                //_tables.swap(newTables);
                //以上方法无法复用insert

                Hashtable<K, V, Hash> newht;//是KV
                newht._table.resize(2 * _table.size());//_table肯定是类里的,不然要指定的
                for (int i = 0; i < _table.size(); i++)
                {
                    newht.insert(_table[i]._kv);
//复用
                }
                _table.swap(newht._table);
//交换
            }
            size_t hashi = ha(kv.first) % _table.size();
//非整型不能取%,所以使用仿函树转换一下
            while (_table[hashi].s == EXIST)
            {
                hashi++;
                hashi %= _table.size();
//hashi不断的++的时候有可能会超出整个vector的数据的长度,所以需要再%_table.size()使其处在最后时下一步回到数据的开头,所以可以看出hashi是不断循环的。
            }
            _table[hashi]._kv = kv;
            _table[hashi].s = EXIST;
            n++;
//每成功插入一个数据,其负载因子就加1
            return true;
        }
        vector<Hashdata<K, V>>* begin() const
        {
            return &_table[0];
        }

        vector<Hashdata<K, V>>* end() const
        {
            return &_table[_table.size()];
        }
        Hashdata<K, V>* Find(const K& key)
        {
            size_t hashi = ha(key) % _table.size();
            size_t n = hashi;
            while (_table[hashi].s != DELETE)
            {
                if (_table[hashi]._kv.first == key && _table[hashi].s == EXIST)
//由于下面的删除逻辑为不删除数据只改名数据的状态所以当找到相等时,这个数据有可能会有两种状态:EXIST和DELETE
                {
                    return &_table[hashi];
                }
                hashi++;
                hashi = hashi % _table.size();
                if (hashi == n)
                {
                    return nullptr;
                }
            }
            return nullptr;
        }

        bool Erase(const K& key)//只要传一个K就可以了
        {
            Hashdata<K, V>* ret = Find(key);
            if (ret)
            {
                ret->s == DELETE;
                return true;
            }
            return false;
        }
    private:
        vector<Hashdata<K, V>> _table;
        size_t n = 0;
//表中存储数据个数, n为负载因子
    };

    
//}

超大重点部分评析

这边说一下扩容的逻辑,由于插入逻辑里面hashi是不断循环的,所以当哈希表里面数据都占满时,hashi会陷入死循环,但是就算没有全部占满,如果已占数据过多就会使得下一个数据在插入时哈希冲突过多使时间复杂度变大,所以为了避免这种情况,引入负载因子,当插入数据占比>=百分之70时,也就是负载因子/空间数据容纳最大个数 == 0.7时,将容器的容量扩大成原来的双倍。

再观察上面的扩容代码,为什么不能直接再原空间之间扩容呢,需要另开一个空间再交换回去,因为如果之间在原空间扩容,会使得size(数据最大个数)变大使得原本的数据的映射乱了。像现在这种扩容写法交换swap之后,由于新创建的临时变量newht出函数体就会销毁了,也就会顺带释放掉swap交给它的原空间,这样扩容之后的空间就保留下来了,所以不需要考虑旧表没有被释放的问题。

最后注意一下这个Erase的写法,很多人可能会认为删除某个元素需要像之前顺序表的那种写法(毕竟每个数据就在顺序表里面),删除了这个数据,让这个数据后面的数据依次往前移,时间复杂度就是O(n),这里不是不可以,但是如果你这么写就麻烦了,因为每个数据都外带了一个数据状态,按顺序表那么写的话就忽略的状态这个东西。

链地址法Separate Chaining——哈希桶的模拟实现

预知后事如何,请持续关注本系列内容!!!

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

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

相关文章

STM32G070 CubeMX配置多通道/单通道ADC+DMA流程 LL库

基础配置不再赘述&#xff0c;时钟这些根据硬件来配置 多通道ADCDMA配置图&#xff1a; 程序配置&#xff1a; 调试查看内存数据&#xff0c;硬件上将PA1接到GND&#xff0c;PA2接到3V3 采集的数据会循环覆盖内存 问题&#xff1a;代码里先初始化ADC_IN1&#xff0c;再初…

Spring扩展点系列-ApplicationContextAwareProcessor

文章目录 简介源码分析示例代码示例一&#xff1a;扩展点的执行顺序运行示例一 示例二&#xff1a;获取配置文件值配置文件application.properties内容定义工具类ConfigUtilcontroller测试调用运行示例二 示例三&#xff1a;实现ResourceLoaderAware读取文件ExtendResourceLoad…

CleanClip - 「CleanClip」是一款专为 Mac 设计的桌面剪贴板工具

官方介绍 欢迎使用 CleanClip —— Mac 上最简洁高效的剪贴板管理工具。CleanClip 专为追求简约操作体验的用户设计&#xff0c;它帮助用户记录系统剪贴板上的内容&#xff0c;并提供强大的分类管理能力&#xff0c;帮助你整理复制的内容&#xff0c;提高办公效率。 智能简洁&…

MAVEN如何导入项目

工作中经常需要导入他人的项目&#xff0c;那么如何导入呢&#xff1f; 1&#xff0c; 选择Maven面板&#xff0c;点 2&#xff0c;选中对应项目的pom.xml&#xff0c;双击即可 3&#xff0c;如果没有maven面板&#xff0c;可以选择view->Appearnce->Tool Window Bars…

HTML5元素定位

1.元素定位 为了实现网页整体布局&#xff0c;我们先要知道&#xff0c;一个元素&#xff0c;是如何定位到页面上的某个位置的&#xff0c;这就是元素定位。 元素定位有四种&#xff0c;可以使用position样式来设置元素定位&#xff0c;所以此属性值有四种&#xff1a; stat…

MybatisPlus新增数据时怎么返回新增数据的id

问&#xff1a;MybatisPlus新增数据时怎么返回新增数据的id&#xff1f;答&#xff1a;当插入操作执行后&#xff0c;MyBatis Plus会自动获取生成的ID并将其设置到传入的实体类对象的id属性中。当然&#xff0c;这需要你的表字段ID是自增的 实体类代码 public class Sites {p…

东风德纳携手纷享销客打造汽车零部件行业营销数智化新标杆

为进一步提升数字化经营管理水平&#xff0c;加速数字化转型&#xff0c;推进“品牌向上”战略落实落地&#xff0c;9月2日&#xff0c;东风德纳车桥有限公司召开CRM项目启动会&#xff0c;携手纷享销客&#xff0c;打造汽车零部件行业营销数智化标杆工程。东风德纳车桥总经理陆…

高效Flutter应用开发:GetX状态管理实战技巧

探索GetX状态管理的使用 前言 在之前的文章中&#xff0c;我们详细介绍了 Flutter 应用中的状态管理&#xff0c;setState、Provider库以及Bloc的使用。 本篇我们继续介绍另一个实现状态管理的方式&#xff1a;GetX。 一、GetX状态管理 基础介绍 GetX 是一个在 Flutter 中…

【原创】【总结】【C++类的设计要点】一道十分典型的含继承与虚函数的类设计题

设计类时的要点 1构造函数与析构函数&#xff1a;先在public中写上构造函数与析构函数 2成员函数&#xff1a;根据题目要求在public中声明成员函数&#xff1b;成员函数的实现在类内类外均可&#xff0c;注意若在类外实现时用::符号表明是哪个类的函数 3数据成员&#xff1a;关…

STM32L051K8U6-HAL-串口中断控制灯闪烁速度

HAL三步法&#xff1a; 1、配置下载线 2、配置晶振 3、配置时钟 4、 配置灯引脚属性为输出模式。并设置标签为LED 5、配置串口1 串口常用函数说明&#xff1a; 需要实现的伪代码&#xff1a; 示例&#xff1a;链接&#xff1a;https://pan.baidu.com/s/1u6FamKgZhvcEsFAdgGeaw…

Realsense D455 imu 数据不输出?

现象 realsense_viewer 可以可视化查看imu数据, 但是realsense-ros 查看/camera/accel/sample和/camera/gyro/sample没有数据输出 背景 realsense_viewer 安装: sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-key F6E65AC044F831AC80A06380C8B3A55A6F3EFCDE…

移动通信为啥要用双极化天线?

❝本文简单介绍下移动通信为啥要用双极化天线及其简单概述。 移动通信为啥要用双极化天线&#xff1f; - RFASK射频问问❝本文简单介绍下移动通信为啥要用双极化天线及其简单概述。什么是极化&#xff1f;电磁波的极化通常是用其电场矢量的空间指向来描述&#xff1a;在空间某…

Leetcode 字母异位词分组

这道题目的意思就是&#xff1a;把包含字母字符相同的单词分到同一组。 算法思路&#xff1a; 使用哈希表来解决。 首先将每个字符串进行排序&#xff0c;将排序之后的字符串作为 key&#xff0c;然后将用 key 所对应的异位词组 作为value。然后我们使用 std::pair 来遍历 键…

Vue的学习(三)

目录 一、for循环中key的作用 1‌.提高性能‌&#xff1a; ‌2.优化用户体验‌&#xff1a; ‌3.辅助Vue进行列表渲染‌&#xff1a; 4‌.方便可复用组件的使用‌&#xff1a; 二、methods及computed及wacth的区别 三、过滤器 1.Vue 2 过滤器简介 定义过滤器 使用过滤…

八、适配器模式

适配器模式&#xff08;Adapter Pattern&#xff09;是一种结构型设计模式&#xff0c;它允许不兼容的接口之间进行合作。适配器模式通过创建一个适配器类来转换一个接口的接口&#xff0c;使得原本由于接口不兼容无法一起工作的类可以一起工作。 主要组成部分&#xff1a; 目标…

CUDA-中值滤波算法

作者&#xff1a;翟天保Steven 版权声明&#xff1a;著作权归作者所有&#xff0c;商业转载请联系作者获得授权&#xff0c;非商业转载请注明出处 实现原理 中值滤波是一种常用的图像处理方法&#xff0c;特别适用于去除图像中的脉冲噪声&#xff08;如椒盐噪声&#xff09;。…

基于IOT的供电房监控系统(实物)

aliyun_mqtt.cpp 本次设计利用ESP8266作为系统主控&#xff0c;利用超声波检测门的状态&#xff0c;利用DHT11检测环境温湿度、烟雾传感器检测空气中的气体浓度&#xff0c;利用火焰报警器模块检测火焰状态&#xff0c;使用OLED进行可视化显示&#xff0c;系统显示传感器数据&a…

同相放大器电路设计

1 简介 同相放大电路输入阻抗为运放的极高输入阻抗&#xff08;GΩ级&#xff09;&#xff0c;因此可处理高阻抗输入源信号。同相放大器的共模电压等于输入信号。 2 设计目标 2.1 输入 2.2 输出 2.3 频率 2.4 电源 3 电路设计 根据设计目标&#xff0c;最终设计的电路结构…

python-确定进制

题目描述 6 942 对于十进制来说是错误的&#xff0c;但是对于 13 进制来说是正确的。即 6(13)​ 9(13)​42(13)​&#xff0c;而 42(13)​4 13^12 13^054(10)​。 你的任务是写一段程序读入三个整数 p,q 和 r&#xff0c;然后确定一个进制 B(2≤B≤16) 使得 p qr 。如果 B 有…

Vue3: 使用ref自动补齐.value

目录 一.老版本&#xff08;已经弃用TypeScript Vue Plugin (Volar)&#xff09; 二.新版本&#xff08;Vue - Official&#xff09; 三.勾选后重启VScode 四.效果 VScode中搜索Vue - Official插件 一.老版本&#xff08;已经弃用TypeScript Vue Plugin (Volar)&#xff0…