hashmap C++实现分析

2023-05-16

一、简介

Map 是 Key-Value 对映射的抽象接口,该映射不包括重复的键,即一个键对应一个值。

在HashMap中,其会根据hash算法来计算key-value的存储位置并进行快速存取。

本文介绍的C++ hashmap,是一个缓存用的hash_map,实现模仿自Java的HashMap,做了一些改造和精简。

特点:无读锁, 低写锁, 不删除只添加/更新, 桶不扩容, 按经验值初始化桶大小, 性能取决于hash算法和桶大小。

二、数据结构

1、哈希

Hash 就是把任意长度的输入,通过哈希算法,变换成固定长度的输出(通常是整型),该输出就是哈希值。

这种转换是一种压缩映射,也就是说,散列值的空间通常远小于输入的空间。不同的输入可能会散列成相同的输出,因此不能从散列值来唯一地确定输入值。

简单的说,哈希就是一种将任意长度的消息压缩到某一固定长度的信息摘要函数。

2、哈希表

哈希表有多种不同的实现,其核心问题是如何解决冲突,即不同输入产生相同输出时,应该如何存储。

最经典的一种实现方法就是拉链法,它的数据结构是链表的数组:

拉链法

数组的特点是:寻址容易,插入和删除困难。

链表的特点是:寻址困难,插入和删除容易。

对于某个元素,我们通过哈希算法,根据元素特征计算元素在数组中的下标,从而将元素分配、插入到不同的链表中去。在查找时,我们同样通过元素特征找到正确的链表,再从链表中找出正确的元素。

3、HashMap 的数据结构             

HashMap 实际上就是一个链表的数组,对于每个 key-value对元素,根据其key的哈希,该元素被分配到某个桶当中,桶使用链表实现,链表的节点包含了一个key,一个value,以及一个指向下一个节点的指针。

三、几个核心问题

1. 找下标:如何高效运算以及减少碰撞

当我们拿到一个hashCode之后,需要将整型的hashCode转换成链表数组中的下标,比如数组大小为n,则下标为:

index = hashCode % n;

这里的取模运算效率较低,如果能够使用位运算(&)来代替取模运算(%),效率将有所提升。位运算直接对内存数据进行操作,不需要转成十进制,处理速度非常快。

我们可以使用以下方法来实现:

index = hashCode & (n-1);

hashCode 与 n-1 进行按位与操作,得到的结果必定是小于n的。

但是,以上按位与的操作跟取模运算并不等价,这可能会带来index分布不均匀问题。

举个例子,假设数组大小为15,则hash值在与14(即 1110)进行&运算时,得到的结果最后一位永远都是0,即 0001、0011、0101、0111、1001、1011、1101、1111位置处是不可能存储数据的。这样,空间的减少会导致碰撞几率的进一步增加,从而就会导致查询速度慢。

如果能够保证按位与的操作跟取模运算是等价的,那么不同的hash值发生碰撞的概率比较小,这样就会使得数据在table数组中分布较均匀,查询速度也较快。

那么,为什么如何实现位运算(&)跟取模运算(%)的等价呢?我们看以下等式:

X % 2^n = X & (2^n1)

2^n表示2的n次方,也就是说,一个数对2^n取模 == 一个数和(2^n – 1)做按位与运算 。

假设n为3,则2^3 = 8,表示成二进制就是1000。2^3-1 = 7 ,表示成二进制就是0111。

此时X & (2^3 – 1) 就相当于取X的二进制的最后三位数。

从二进制角度来看,X / 2^n 相当于 X >> n,即把X右移n位,被移掉的部分(后n位),则是X % 2^n,也就是余数。

因此,计算 X % 2^n,实际上就是要获取 X 的后n位

我们注意到,2^n 的后 n+1 位都是1,其余为0,于是 2^n-1 的后 n 位都是1,其余为0。

因此,X 跟 2^n-1 做按位与运算,将得到X 的后n位。

所以,只要保证数组的大小是2^n,就可以使用位运算来替代取模运算了。

因此,当拿到一个用户指定的数组大小时,我们总是会再做一层处理,以保证实际的数组大小为 2^n:

size_t getTableSize(size_t capacity) {
    // 计算超过 capacity 的最小 2^n 
    size_t ssize = 1;
    while (ssize < capacity) {
        ssize <<= 1;
    }
    return ssize;
}

2. 哈希策略:如何将元素均匀地分配到各个桶内

由于我们将使用key的hashCode来计算该元素在数组中的下标,所以我们希望hashCode是一个size_t类型。所以我们的哈希函数最首要的就是要把各种类型的key转换成size_t类型,以下是代码实现:

#ifndef cache_hash_func_H__
#define cache_hash_func_H__

#include <string>

namespace HashMap {

/**
 * hash算法仿函数
 */
template<class KeyType>
struct cache_hash_func {
};

inline std::size_t cache_hash_string(const char* __s) {
    unsigned long __h = 0;
    for (; *__s; ++__s)
        __h = 5 * __h + *__s;
    return std::size_t(__h);
}

template<>
struct cache_hash_func<std::string> {
    std::size_t operator()(const std::string & __s) const {
        return cache_hash_string(__s.c_str());
    }
};

template<>
struct cache_hash_func<char*> {
    std::size_t operator()(const char* __s) const {
        return cache_hash_string(__s);
    }
};

template<>
struct cache_hash_func<const char*> {
    std::size_t operator()(const char* __s) const {
        return cache_hash_string(__s);
    }
};

template<>
struct cache_hash_func<char> {
    std::size_t operator()(char __x) const {
        return __x;
    }
};

template<>
struct cache_hash_func<unsigned char> {
    std::size_t operator()(unsigned char __x) const {
        return __x;
    }
};

template<>
struct cache_hash_func<signed char> {
    std::size_t operator()(unsigned char __x) const {
        return __x;
    }
};

template<>
struct cache_hash_func<short> {
    std::size_t operator()(short __x) const {
        return __x;
    }
};

template<>
struct cache_hash_func<unsigned short> {
    std::size_t operator()(unsigned short __x) const {
        return __x;
    }
};

template<>
struct cache_hash_func<int> {
    std::size_t operator()(int __x) const {
        return __x;
    }
};

template<>
struct cache_hash_func<unsigned int> {
    std::size_t operator()(unsigned int __x) const {
        return __x;
    }
};

template<>
struct cache_hash_func<long> {
    std::size_t operator()(long __x) const {
        return __x ^ (__x >> 32);
    }
};

template<>
struct cache_hash_func<unsigned long> {
    std::size_t operator()(unsigned long __x) const {
        return __x ^ (__x >> 32);
    }
};

}

#endif /* cache_hash_func_H__ */

可以看到,上面实现的hash函数比较随意,难以产生较为均匀(即冲突少)的hashCode。

为了防止质量低下的hashCode()函数实现,我们使用getHash()方法对一个对象的hashCode进行重新计算:

size_t getHash(size_t h) const {
    h ^= (h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

这段代码对key的hashCode进行扰动计算,防止不同hashCode的高位不同但低位相同导致的hash冲突。也就是说,尽量做到任何一位的变化都能对最终得到的结果产生影响。

举个例子,有一个Key值,假设经过简单的hashcode后,得到的值为“01011000110101110011111010011011”,如果当前数组的大小为16,在不进行扰动计算的情况下,他最终得到的index结果值为11。

由于15的二进制扩展到32位为“0000000000000000 0000000000001111”,所以,一个数字在和他进行按位与操作的时候,前28位无论是什么,计算结果都一样:

hashCodehashCode&00001111
0101100011010111 001111101001101111
0010000000000000 001111101001101111
0000000000000000 001111101001101111

而经过扰动计算之后,之前会产生冲突的两个hashcode,最终得到的index的值不一样了,即使是高位的不同,也会造成最终结果的不同。

getHash的更多实现解析可参考:

全网把Map中的hash()分析的最透彻的文章,别无二家。

3. 多线程:如何实现无读锁,低写锁

在数据结构上,我们使用多个桶来存放数据,当哈希足够均匀时,冲突将比较少。当多线程操作不同的链表时,完全不需要加锁,但是如果操作的是同一个链表,则需要加锁来保证正确性。因此多个桶的设计,从降低锁的粒度的角度,已经减少了很多不必要的加锁操作。

同时,单向链表的使用,给我们带来了一个意想不到的好处:多个读线程和一个写线程并发操作不会出问题。

假设链表中目前包含A和B节点,此时要在它们之间插入C节点,步骤如下:
1. 创建C节点
2. 将C的next指向B
3. 将A的next指向C

在完成1和2两步之后,读线程查询链表只能看到A和B,链表是完整的。
在第3 步,修改next指针的操作是原子的,因此无论什么时候,读线程看到的链表都是完整的,数据没有丢失。因此读操作是不需要加锁的。

读操作代码:

entry_ptr get(const KeyType & key) {
    if (m_count != 0) { // read-volatile
        for (entry_ptr entry = m_head; entry; entry = entry->getNext()) {
            if (entry->equalsKey(key)) {
                return entry;
            }
        }
    }
    static entry_ptr EMPTY = NULL;
    return EMPTY;
}

当多个线程同时执行插入时,由于next的修改可能会被覆盖,从而造成内存泄漏,因此写需要加锁。(当然这里也可以考虑CAS无锁化,效率方面看应用场景)

写操作代码:

//返回值表示key是否已经存在, 已存在返回true
bool set(const KeyType & key, const ValueType & value) {
    entry_ptr entry = get(key);
    // 如果key已经存在,直接修改
    if (entry) {
        entry->setValue(value);
        return true;
    }
    LockType lock(m_lock);
    // double check,if之后,加锁之前,entry可能被赋值了
    // 因此加完锁要再检查一遍
    entry = get(key);
    if (entry) {
        entry->setValue(value);
        return true;
    }
    m_head = new entry_type(key, value, m_head);
    ++m_count;
    return false;
}

由于我们的实现中,不对桶进行扩容,不支持删除,因此简化很多。对于链表新增的节点,均插入到头部即可。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

hashmap C++实现分析 的相关文章

  • CMakeLists.txt编写常用命令

    文章目录 一 设置cmake最小版本二 设置项目名称三 设置编译目标类型四 指定编译包含的源文件1 明确指明包含的源文件2 搜索指定目录的所有的cpp文件3 自定义搜索规则4 包含多个文件夹里的文件 五 设置包含目录六 设置链接库搜索目录七
  • Docker 拉取镜像及标签 pull | tag

    Docker 拉取镜像及标签 pull tag 重翻Fabric项目的源码 xff0c 发现Docker部分内容 xff0c 有很多不尽理解的地方 xff0c 看着看着 xff0c 就看到使用docker pull拉取Fabric镜像及使用
  • USB大容量存储设备无法启动该怎么办?

    USB大容量存储设备 xff08 USB mass storage device class xff0c 也称为USB MSC或UMS xff09 是一个协议 xff0c 允许一个USB接口的设备与电脑相连接 xff0c 以便在两者之间传输
  • Tensor数据相关的运算、函数讲解及与numpy区别

    Tensor tensorflow 中使用它来表示数据 可以看做多维数组或者list 标量是张量 xff0c 向量是张量 xff0c 矩阵是张量 xff0c 矩阵的矩阵是张量 常用几种定义方法 1 variable变量 xff0c 一般是可
  • QGC4.1.2二次开发(1)--Qt5.12.6 andorid开发环境搭建

    开发环境介绍 xff1a QGC版本 xff1a 4 1 2 Qt版本 xff1a 5 12 6 xff08 QGC要求 xff09 windows平台开发 xff1a vs2017 andorid平台 xff1a JDK Java SE
  • SITL--仿真多架无人机

    SITL仿真环境搭建 ardupliot源码下载与编译 首先需要安装Ardupliot开源飞控的开发环境 xff0c 参考这个知乎博主的文章 xff1a 链接 我的安装环境 ubuntu20 04 先下载Ardupilot源码 xff0c
  • QGC4.1.2二次开发(2)QGC连接与数据收发

    文章目录 前言一 连接原理二 连接过程与数据收发1 连接过程 xff08 以串口为例 xff09 2 数据发送 总结 前言 QGC连接无人机飞控时支持多种连接方式 xff0c 并且可以自动连接 xff0c 不由让人好奇它的实现原理 xff0
  • GRPC远程调用

    目录 FAQ gRPC1 gRPC原理 1 1 什么是RPC 1 2 gRPC的一些特性 1 3 gRPC支持的编程语言 1 4 gRPC的使用场景 1 5 谁在使用gRPC 1 6 gRPC设计之初的动机和原则 2 数据封装和数据传输问题
  • 算法导论->算法基础->2.1插入排序 (从小到大)

    1 伪代码 2 执行过程图 3 c语言实现完整代码 include lt stdio h gt include lt malloc h gt typedef struct MyArray int pbase int length MyArr
  • hihocoder图像算子(高斯消元)

    描述 在图像处理的技术中 xff0c 经常会用到算子与图像进行卷积运算 xff0c 从而达到平滑图像或是查找边界的效果 假设原图为H W的矩阵A xff0c 算子矩阵为D D的矩阵Op xff0c 则处理后的矩阵B大小为 H D 43 1
  • subs()函数()

    摘自matlab Utilities for obsolete MUTOOLS commands subs 既是目录也是函数 subs Symbolic substitution subs S OLD NEW replaces OLD wi
  • IEEE802

    IEEE 官方最新标准 xff1a Browse Standards Get Program IEEE Xplore https ieeexplore ieee org browse standards get program page s
  • 笔记本电脑3C认证要求的相关介绍

    作为CCC强制目录中的产品 xff0c 便携式计算机如果想在国内销售 xff0c 是必须要进行3C认证的 便携式计算机是什么 也就是笔记本电脑 xff0c 平板电脑等 官方的定义是以便携性为特点 xff0c 内置了输入输出设备 电池模块的微
  • 普通门禁卡及各类复制卡相关知识

    转自 xff1a https nfctool cn 42 本文带你了解M1卡的数据结构 xff0c 为以后的破解提供理论基础 同时带你了解各种IC卡 xff0c 让你对破解和复制有更清晰的目标 请注意 xff0c ID卡没有密码 xff0c
  • 用XDS510-V4专业版仿真器连接CCS3.3与28335问题记录

    今天用仿真器连接28335一直没连上 xff0c 错误有 xff1a 1 xff0c 断开仿真器用ccs3 3连接的时候显示为 xff08 不接仿真器 xff0c 空连接 xff09 Error connecting to the targ
  • NVIDIA Jetson TX2 通过vnc 桌面控制

    1 安装Xrdp Windows远程桌面使用的是RDP协议 xff0c 所以ubuntu上就要先安装Xrdp xff0c 在ubuntu软件中心搜索xrdp安装 安装xrdp的同时会自动安装vnc4server xbase clients组
  • NVIDIA Jetson TX2 查看系统参数状态

    1 xff0c 查看Jetson TX2 L4T版本 xff1a head n 1 etc nv tegra release 在刷 JetPack 3 0之前 和刷之后 版本参数发生细微的变化 xff1a REVISION xff1a 由
  • 解决 ImportError: No module named 'serial' 问题

    在pycharm里编写Python串口程序的时候 xff0c 编译时提示 ImportError No module named 39 serial 39 解决办法 xff1a 安装 serial module 这里区分python2和 p
  • 查看ubuntu下Qt的版本

    1 xff0c 查看ubuntu下Qt的版本 打开命令行输入 xff1a span style font size 14px qmake v span
  • 运算放大器基本运算

    转自 xff1a http www 21ic com jichuzhishi analog amplifier 2014 11 11 606654 html 运算放大器组成的电路五花八门 xff0c 令人眼花瞭乱 xff0c 是模拟电路中学

随机推荐

  • KEIL 注解和去注解 快捷键添加

    KEIL 注解和去注解 快捷键添加方法 xff1a 菜单栏Edit gt Configuration gt Shortcut Keys 1 例如设置 注解快捷键 xff1a Ctrl 43 2 例如设置 去注解快捷键 xff1a Ctrl
  • git、vscode免密登录

    1 git配置 git config global list 查看当前配置 git config global user name 34 xiaoyaozi 34 git config global user name 34 xiaoyao
  • 555 单稳态电路

    555 定时器成本低 xff0c 性能可靠 xff0c 只需要外接几个电阻 电容 xff0c 就可以实现多谐振荡器 单稳态 触发器及施密特触发器等脉冲产生与变换电路 它内部包括两个电压比较器 xff0c 三个5K欧姆的等值串联分压电阻 xf
  • Allegro 铺铜设置

    软件版本 xff1a Allegro16 6 敷铜 xff1a 放置禁止敷铜区域 xff1a Setup Areas Route Keepout 1 标题栏选Shap gt Global Dynamic Params Shape Polyg
  • OVP 过压保护电路

    过压保护电路 OVP 为下游电路提供保护 xff0c 使其免受过高电压的损坏 OVP电路监测外部电源 如 xff1a 离线电源或电池 的直流电压 xff0c 通过下述两种方式中的一种保护后续电路 xff1a 撬棍钳位电路或串联开关 撬棍电路
  • 超全蓝牙芯片原厂总结(含芯片型号)

    转自 xff1a https blog csdn net weixin 42583147 article details 80923946 作者 xff1a XCODER 蓝牙芯片原厂 1 CSR 高通 xff08 被高通收购 xff09
  • ST-Link的internal command error问题的解决方法

    问题 xff1a 显示 xff1a internal command error 这是由于stlink无法识别到芯片的情况 xff0c 通过解决这个问题我找到几个原因和解决方法 xff1a 1 xff0c 芯片睡眠 xff0c 停机 xff
  • 蓝牙 UUID 解释

    一 xff0c 什么是 UUID UUID 可以简单理解为编号 xff0c 唯一的编号 xff0c 用于区分不同的个体 服务和特性都有各自的UUID 比如经典的9527 UUID 就跟身份证一样 xff0c 不管是你是局长还是科长 xff0
  • 【人工智能】传教士和野人问题(M-C问题)

    摘要 本题需要解决的是一般情况下的传教士和野人问题 xff08 M C问题 xff09 通过对问题的一般化 xff0c 我们用一个三元组定义了问题的状态空间 xff0c 并根据约束条件制定了一系列的操作规则 xff0c 最后通过两个启发式函
  • 【算法设计与数据结构】为何程序员喜欢将INF设置为0x3f3f3f3f?

    在算法竞赛中 xff0c 我们常常需要用到一个 无穷大 的值 xff0c 对于我来说 xff0c 大多数时间我会根据具体问题取一个99999999之类的数 xff08 显得很不专业啊 xff01 xff09 在网上看别人代码的时候 xff0
  • 【slighttpd】基于lighttpd架构的Server项目实战(7)—http-parser

    对于http服务器 xff0c http request的解析是比较麻烦的 xff0c 由于我们的重点并不在这上面 xff0c 所以这一部分不打算自己编写 xff0c 而是使用开源的http parser库 xff0c 下面我们将使用该库来
  • select和epoll 原理概述&优缺点比较

    这个问题在面试跟网络编程相关的岗位的时候基本都会被问到 xff0c 刚刚看到一个很好的比喻 xff1a 就像收本子的班长 xff0c 以前得一个个学生地去问有没有本子 xff0c 如果没有 xff0c 它还得等待一段时间而后又继续问 xff
  • 笔记-关于神经网络黑盒模型可解释性,可视化

    原博地址 xff1a 深度学习黑盒可视化指南 xff0c 从隐藏层开始 摘 xff1a 一旦神经网络接收到相当大的所需数据集后 xff0c 该网络就会使用其精确的知识 权重 来证明或识别未知数据样本上的模式 即在经过大量数据集训练以后 xf
  • C++11 多线程 future/promise简介

    1 lt future gt 头文件简介 Classes std future std future error std packaged task std promise std shared futureFunctions std as
  • C++异步调用利器future/promise实现原理

    前言 在异步编程中 xff0c 各种回调将让人眼花缭乱 xff0c 代码分散 xff0c 维护起来十分困难 boost和C 43 43 11 的 future promise 提供了一个很好的解决方案 xff0c 使得代码更加漂亮 易维护
  • 【Heydrones】飞手百科第一篇:一定要看的无人机原理总结

    飞手百科 知识是最好的保险 本文目录 1 xff0c 无人机的飞行原理 2 xff0c 无人机的几大系统 3 xff0c 无人机的外观介绍 4 xff0c 无人机的专业术语 xff08 一 xff09 无人机的飞行原理 旋翼和轮子一样 xf
  • 【Tars】腾讯微服务框架Tars介绍

    目录 1 介绍2 设计思路3 整体架构4 平台特性 1 介绍 Tars是 基于名字服务 使用Tars协议 的高性能 RPC 开发框架 xff0c 同时配套一体化的 服务治理平台 xff0c 帮助个人或者企业快速的以微服务的方式构建自己稳定可
  • C++11常用新特性快速一览

    最近工作中 xff0c 遇到一些问题 xff0c 使用C 43 43 11实现起来会更加方便 xff0c 而线上的生产环境还不支持C 43 43 11 xff0c 于是决定新年开工后 xff0c 在组内把C 43 43 11推广开来 xff
  • 语法糖:萃取lambda表达式

    背景 现在手头主负责的服务代码 xff0c 基本上都用C 43 43 11来开发了 xff0c 异步编程使用的是TAF的future promise future的then函数 xff0c 接受的是一个Callback对象 xff0c 该对
  • hashmap C++实现分析

    一 简介 Map 是 Key Value 对映射的抽象接口 xff0c 该映射不包括重复的键 xff0c 即一个键对应一个值 在HashMap中 xff0c 其会根据hash算法来计算key value的存储位置并进行快速存取 本文介绍的C