一文快速入门哈希表

news2024/11/8 20:05:20

目录

  • 一、基本概念
    • 1.1 哈希冲突
  • 二、整数哈希
    • 2.1 哈希函数的设计
    • 2.2 解决哈希冲突
      • 2.2.1 开放寻址法
      • 2.2.2 拉链法
  • 三、字符串哈希
    • 3.1 应用:重复的DNA序列
  • References

一、基本概念

哈希表又称散列表,一种以「key-value」形式存储数据的数据结构。所谓以「key-value」形式存储数据,是指任意的键值 key 都唯一对应到内存中的某个位置。只需要输入查找的键值,就可以快速地找到其对应的 value。可以把哈希表理解为一种高级的数组,这种数组的下标可以是很大的整数,浮点数,字符串甚至结构体。

哈希表存储的基本思路是:设要存储的元素个数为 n n n,设置一个长度为 m   ( m ≥ n ) m\,(m\geq n) m(mn)连续内存单元,以每个元素的关键字 k i   ( 1 ≤ i ≤ n ) k_i\,(1\leq i\leq n) ki(1in) 为自变量,通过一个哈希函数 h h h k i k_i ki 映射为内存单元的地址 h ( k i ) h(k_i) h(ki),并把该元素存储在这个内存单元中。

例如,我们可以开辟一个长度大于等于 n n n 的数组 a,并以 a[h(key)] = value 的方式来存储键值对 (key, value)

哈希表最常见的两种操作是插入查找

1.1 哈希冲突

注意到当 k i ≠ k j k_i\neq k_j ki=kj 时是有可能出现 h ( k i ) = h ( k j ) h(k_i)=h(k_j) h(ki)=h(kj) 的,这种现象称为哈希冲突。我们将具有不同关键字但具有相同哈希地址的元素称为「同义词」,这种冲突也称为同义词冲突。

在一般的哈希表中哈希冲突是很难避免的,但我们又不得不解决冲突,否则后面插入的元素会覆盖前面已经插入的元素。解决冲突的方法有很多,可分为「开放寻址法」和「拉链法」两大类,下文会详细介绍。

二、整数哈希

key 为整数时,h(key) 称为整数哈希。

2.1 哈希函数的设计

构造哈希函数的目标是使得到的 n n n 个元素的哈希地址尽可能均匀地分布在 m m m 个连续内存单元地址上,同时使计算过程尽可能简单以达到尽可能高的时间效率。

构造哈希函数有许多种方法,这里只介绍最常用的「除留余数法」。

不妨设哈希表要存储的元素个数为 n n n,于是我们可以开一个长度为 n n n 的数组并定义

h ( k ) = k    mod    p h(k)=k\;\text{mod}\;p h(k)=kmodp

其中 p p p不大于 n n n 且最接近 n n n质数

但这样做的弊端在于,如果 n n n 不是质数(通常都不是),则有 0 ≤ h ( k ) ≤ p − 1 < n − 1 0\leq h(k) \leq p-1 <n-1 0h(k)p1<n1,从而一定会造成哈希冲突。为避免这一现象,我们可以寻找大于等于 n n n 且最接近 n n n 的质数,不妨设为 m m m,然后开一个长度为 m m m 的数组并取 p = m p=m p=m

事实上,若数组长度 m m m(即质数 p p p)能够离 2 2 2 的幂尽可能地,则冲突概率可以进一步降低,详情见[3]。这里我们可以根据 n n n 的数量级来给出一个简化版的表格:

n n n m m m
1 0 3 10^3 103 1543 1543 1543
1 0 4 10^4 104 12289 12289 12289
1 0 5 10^5 105 196613 196613 196613
1 0 6 10^6 106 1572869 1572869 1572869

注意到 k k k 可能是负数,因此我们需要将哈希函数修改成

h ( k ) = ( k    mod    m + m )    mod    m h(k)=(k\;\text{mod} \; m+m)\;\text{mod}\; m h(k)=(kmodm+m)modm

来确保 h ( k ) ≥ 0 h(k)\geq 0 h(k)0

2.2 解决哈希冲突

哈希冲突无法彻底避免,因此我们必须要考虑如何解决哈希冲突。

2.2.1 开放寻址法

开放寻址法就是在插入一个关键字为 k k k 的元素时,若发生哈希冲突,则通过某种哈希冲突解决函数(也称为再哈希)得到一个新空闲地址再插入该元素的方法。

再哈希的设计有很多种,常见的有「线性探测法」和「平方探测法」,本文只讲解前者,后者可类比得到。

线性探测法是从发生冲突的地址开始,依次探测下一个地址,直到找到一个空闲单元为止。当到达下标为 m − 1 m-1 m1 的哈希表表尾时,下一个探测地址是表首地址 0 0 0。当 m ≥ n m\geq n mn 时一定能找到一个空闲单元。

使用开放寻址法时, m m m 通常取 n n n 2 ∼ 3 2\sim 3 23 倍左右。例如若 n ≤ 1 0 5 n\leq 10^5 n105,则我们可以取上表中的 196613 196613 196613 作为哈希表的大小。

基于开放寻址法的哈希表实现如下(以下均假定 k = v ≜ x k=v\triangleq x k=vx):

const int N = 196613, INF = 0x3f3f3f3f;  // 假定数据范围不超过1e9

struct HashTable {
    int h[N];

    HashTable() { memset(h, 0x3f, sizeof(h)); }  // 未存储元素的地方均为INF

    int hash(int x) {
        int idx = (x % N + N) % N;
        while (h[idx] != INF && h[idx] != x) idx = (idx + 1) % N;
        return idx;
    }

    void insert(int x) {
        h[hash(x)] = x;
    }

    bool query(int x) {
        return h[hash(x)] == x;
    }
};

线性探测法的优点是解决冲突简单,但一个重大的缺点是容易产生堆积问题。平方探测法虽然可以避免出现堆积问题,但是其不一定能探测到哈希表上的所有单元(至少能探测到一半单元)。

2.2.2 拉链法

拉链法是把所有的同义词用单链表链接起来的方法。在这种方法中,哈希表的每个单元存储的不再是元素本身,而是相应同义词单链表的头指针(注意是头指针而不是头节点)。

对于单链表,我们可以采用数组的方式进行实现。此外,使用拉链法时, m m m 的大小通常和 n n n 差不多。例如,若 n ≤ 1 0 5 n\leq 10^5 n105,我们可以寻找大于等于 1 0 5 10^5 105 的第一个质数,即 m = 100003 m=100003 m=100003

基于拉链法的哈希表实现如下:

const int N = 100003;

struct HashTable {
    int h[N], val[N], nxt[N], p;  // p是指向待插入位置的指针

    HashTable() : p(0) { memset(h, -1, sizeof(h)); }  // 空指针用-1表示

    int hash(int x) {
        return (x % N + N) % N;
    }

    void insert(int x) {
        int idx = hash(x);
        val[p] = x, nxt[p] = h[idx], h[idx] = p++;
    }

    bool query(int x) {
        for (int i = h[hash(x)]; ~i; i = nxt[i])
            if (val[i] == x)
                return true;
        return false;
    }
};

三、字符串哈希

⚠️ 本节讨论的下标均从 1 1 1 开始。

key 为字符串时,h(key) 称为字符串哈希。

这里我们介绍「多项式哈希方法」,对于一个长度为 l l l 的字符串 s s s 来说,哈希函数定义如下

h ( s ) = ∑ i = 1 l s [ i ] × p l − i    ( mod    M ) h(s)=\sum_{i=1}^{l} s[i]\times p^{l-i}\;(\text{mod}\; M) h(s)=i=1ls[i]×pli(modM)

其中 s [ i ] s[i] s[i] 为字符的ASCII码, p p p 通常取 131 131 131 131313 131313 131313 M M M 2 64 2^{64} 264(使用这种取值,哈希冲突的概率几乎为 0 0 0,故下文不再考虑哈希冲突)。

注意到使用 unsigned long long 这个变量类型存储哈希值,溢出时就相当于对 M M M 取模。记 h ( s ) ≜ h ( s [ 1.. l ] ) h(s)\triangleq h(s[1..l]) h(s)h(s[1..l]),不难发现

h ( s [ 1.. l ] ) = ∑ i = 1 l − 1 s [ i ] × p l − i + s [ l ] = p × h ( s [ 1.. l − 1 ] ) + s [ l ] h(s[1..l])=\sum_{i=1}^{l-1}s[i]\times p^{l-i}+s[l]=p\times h(s[1..l-1])+s[l] h(s[1..l])=i=1l1s[i]×pli+s[l]=p×h(s[1..l1])+s[l]

接下来分别开两个数组 h h h p p p,其中 h [ i ] ≜ h ( s [ 1.. i ] ) h[i]\triangleq h(s[1..i]) h[i]h(s[1..i]) 用来存储原串长度为 i i i 的前缀的哈希值, p [ i ] p[i] p[i] 用来存储 p i p^i pi,于是得到递推式:

{ h [ i ] = h [ i − 1 ] ⋅ p + s [ i ] , 1 ≤ i ≤ l p [ i ] = p [ i − 1 ] ⋅ p , 1 ≤ i ≤ l h [ 0 ] = 0 , p [ 0 ] = 1 \begin{cases} h[i]=h[i-1]\cdot p+s[i],\quad 1\leq i\leq l \\ p[i] = p[i-1]\cdot p,\quad 1\leq i\leq l\\ h[0]=0,\quad p[0]=1 \end{cases} h[i]=h[i1]p+s[i],1ilp[i]=p[i1]p,1ilh[0]=0,p[0]=1

从而, h [ l ] h[l] h[l] 就代表字符串 s s s 的哈希值。在求解的过程中我们还得到了 s s s 的所有前缀哈希值。

那前缀哈希值有什么用呢?利用它,我们可以在 O ( 1 ) O(1) O(1) 的时间内求出 s s s任一子串的哈希值。具体来说,设子串为 s [ l . . r ] s[l..r] s[l..r](这里的 l l l 并非指长度,而是left的意思),注意到

h [ l − 1 ] = ∑ i = 1 l − 1 s [ i ] × p l − 1 − i h [ r ] = ∑ i = 1 r s [ i ] × p r − i \begin{aligned} h[l-1]&=\sum_{i=1}^{l-1}s[i]\times p^{l-1-i} \\ h[r]&=\sum_{i=1}^rs[i]\times p^{r-i} \end{aligned} h[l1]h[r]=i=1l1s[i]×pl1i=i=1rs[i]×pri

不难看出

h [ r ] − p r − l + 1 ⋅ h [ l − 1 ] = ∑ i = 1 r s [ i ] × p r − i − ∑ i = 1 l − 1 s [ i ] × p r − i = ∑ i = l r s [ i ] × p r − i \begin{aligned} h[r]-p^{r-l+1}\cdot h[l-1]&=\sum_{i=1}^rs[i]\times p^{r-i}-\sum_{i=1}^{l-1}s[i]\times p^{r-i} \\ &=\sum_{i=l}^rs[i]\times p^{r-i} \\ \end{aligned} h[r]prl+1h[l1]=i=1rs[i]×prii=1l1s[i]×pri=i=lrs[i]×pri

正是子串 s [ l . . r ] s[l..r] s[l..r] 的哈希值。

基于多项式哈希法的字符串哈希实现如下(假定字符串长度 ≤ 1 0 5 \leq 10^5 105):

typedef unsigned long long ULL;

const int N = 1e5 + 10, P = 131313;

struct HashTable {
    ULL h[N], p[N];

    HashTable(string s) {
        h[0] = 0, p[0] = 1;
        for (size_t i = 1; i <= s.size(); i++) {
            h[i] = h[i - 1] * P + s[i - 1];  // 注意下标的转换
            p[i] = p[i - 1] * P;
        }
    }

    ULL get(int l, int r) {
        return h[r] - h[l - 1] * p[r - l + 1];
    }
};

3.1 应用:重复的DNA序列

原题链接:LeetCode 187. 重复的DNA序列

AC代码(本题如果取 p = 131 p=131 p=131 会WA):

typedef unsigned long long ULL;

const int N = 1e5 + 10, P = 131313;

struct HashTable {...};  // 这里省略

class Solution {
public:
    vector<string> findRepeatedDnaSequences(string s) {
        if (s.size() <= 10) return {};

        HashTable ht(s);
        unordered_map<ULL, int> cnt;
        vector<string> ans;

        for (int i = 1; i + 9 <= s.size(); i++) {
            int j = i + 9, hash = ht.get(i, j);
            if (cnt[hash] == 1) ans.push_back(s.substr(i - 1, 10));
            cnt[hash]++;
        }

        return ans;
    }
};

References

[1] https://oi-wiki.org/ds/hash/
[2] https://www.acwing.com/activity/content/11/
[3] https://planetmath.org/goodhashtableprimes
[4] 数据结构教程(Python语言描述)
[5] https://oi-wiki.org/string/hash/

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

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

相关文章

RA4M2开发(1)----使用串口进行打印

为什么使用Cube进行FreeRTOS配置 本篇文章主要介绍如何使用e2studio对瑞萨RA4M2开发板进行串口打印配置。 硬件准备 首先需要准备一个开发板&#xff0c;这里我准备的是芯片型号R7FAM2AD3CFP的开发板&#xff1a; 新建工程 工程模板 保存工程路径 芯片配置 本文中使用R7F…

【GlobalMapper精品教程】043:图片自动矢量化

本文讲解Globalmapper自动矢量化教程,配套案例数据。 参考教程:ArcGIS实验教程——实验三十三:ArcScan自动矢量化完整案例教程 文章目录 一、加载实验数据二、启动矢量化工具三、矢量化栅格四、矢量化结果五、注意事项一、加载实验数据 打开配套实验数据包中的data043.rar…

参数检验与非参数检验

综述 假设检验 参数检验 T检验 T检验是通过比较不同数据的均值&#xff0c;研究两组数据之间是否存在显著差异。 单总体检验&#xff1a;单总体t检验是检验一个样本平均数与一个已知的总体平均数的差异是否显著。当总体分布是正态分布&#xff0c;如总体标准差未知且样本容量小…

算法——垃圾回收算法——标记清除

标记清除简介算法过程1.标记阶段2.清除阶段3.缺点3.1内存碎片化简介 标记清除算法简介。 文章中使用的动画网站地址&#xff1a; 限 pc: 标记清除动画 &#xff1a;http://www.donghuasuanfa.com/platform/portal?pcmark-sweep 算法一览表&#xff1a;https://blog.csdn.net…

23种设计模式之面向对象的设计原则

23种设计模式之面向对象的设计原则1. 设计模式概述1.1 什么是设计模式1.2 设计模式的好处2. 设计原则分类3. 详解3.1 单一职责原则3.2 开闭原则3.3 里氏代换原则3.4 依赖倒转原则3.5 接口隔离原则3.6 合成复用原则3.7 迪米特法则4. Awakening1. 设计模式概述 我们的软件开发技术…

18《Protein Actions Principles and Modeling》-《蛋白质作用原理和建模》中文分享

《Protein Actions Principles and Modeling》-《蛋白质作用原理和建模》 本人能力有限&#xff0c;如果错误欢迎批评指正。 第四章&#xff1a;Protein Binding Leads to Biological Actions &#xff08;蛋白质的结合会产生生物作用&#xff09; -偶联结合是调控、信号传…

【Java基础】-【Spring Boot】-【Spring】

文章目录Spring BootSpring Boot的启动流程Spring Boot项目是如何导入包的&#xff1f;Spring Boot自动装配的过程Spring Boot注解Spring的核心Spring AOP既然有没有接口都可以用CGLIB&#xff0c;为什么Spring还要使用JDK动态代理&#xff1f;AOP的应用场景Spring AOP不能对哪…

图论(7)负环和差分约束

一、概念 给定一张有向图&#xff0c;如果存在一个环&#xff0c;环上各边权值之和是负数&#xff0c;则称这个环为负环。 判断方式&#xff1a;bellman-ford算法和spfa算法。抽屉原理 这里只介绍spfa。设立cnt数组表示从1到x的最短路径包含的边数&#xff0c;如果cnt[i]大于…

JZ65 不用加减乘除做加法

【答案解析】&#xff1a;十进制相加思想&#xff1a; 1507 &#xff0c; 先计算不考虑进位的相加结果 12 &#xff08;因为 57 的不考虑进位的结果是 2 &#xff0c;遇 10 进位嘛&#xff09;&#xff0c;然后计算进位 57 进位是 10 &#xff0c;则 10 与 12 再次相加&#xf…

2023年山东最新交安安全员考试题库及答案

百分百题库提供交安安全员考试试题、交安安全员考试真题、交安安全员证考试题库等&#xff0c;提供在线做题刷题&#xff0c;在线模拟考试&#xff0c;助你考试轻松过关。 35.建设行政主管部门或者其他有关部门可以将施工现场的监督检查委托给建设工程&#xff08;&#xff09;…

Visual Studio 17.5: 有关 C++ 的新特性预览

Visual Studio 17.5 在 C 代码编辑方面带来了一些新的改进&#xff0c;这些改进包括&#xff1a;C 代码中的大括号对着色&#xff0c;拼写检查&#xff0c;多合一搜索&#xff0c;重新设计的成员列表以及宏展开改进等。上述这些改进都融入到了 Visual Studio 的最新预览版本 17…

CTF-Horizontall HackTheBox渗透测试(一)

** 0X01 简介** #Horizontall#难度是一个相对“简单”的 CTF Linux盒子。该CTF环境涵盖了通过利用Strapi RCE 漏洞并使用内部应用程序 (Laravel) 将隧道传输到本地计算机&#xff0c;并且在 Laravel v 7.4.18 上运行 漏洞PoC最来提升权限拿到root用户权限。 ** 1.1信息收集**…

GuLi商城-人人开源搭建后台管理系统

参考&#xff1a; 谷粒商城-基础篇(详细流程梳理代码) 谷粒商城-day01 项目的基本搭建_周周写不完的代码的博客-CSDN博客_谷粒商城 若依 谷粒商城分布式基础篇1-个人版_断河愁的博客-CSDN博客_谷粒商城 https://blog.csdn.net/yudbl/category_11902060.html 学习路线 源代…

博客搭建教程2-hexo框架初搭建

文章目录1 前言2 准备工作3 hexo安装4 共享文件夹创建(额外)1 前言 本次教程选用hexo来搭建博客&#xff0c;hexo是一个开源的架构&#xff0c;只需要进行简单的操作就可以拥有自己的博客。 参考网站&#xff1a; hexo官网 注意:下面的命令在root下进行&#xff0c;在日常的工…

【雷丰阳-谷粒商城 】【分布式基础篇-全栈开发篇】【04】跨域_OSS_后端校验

持续学习&持续更新中… 学习态度&#xff1a;守破离 【雷丰阳-谷粒商城 】【分布式基础篇-全栈开发篇】【04】跨域问题解决实现逻辑删除文件存储普通上传云存储阿里云OSS简介术语简单使用使用SpringCloudAlibaba—oss服务端签名后直传普通上传方式&#xff1a;服务端签名后…

Win10安装ElasticSearch笔记

1、安装前准备条件因为ElasticSearch7.17需要JDK1.8的支持&#xff0c;首先确保你的win10已经提前安装好了jdk8的版本ElasticSearch支持的JDK最低版本是1.8.0。ElasticSearch7.17及以下的版本最低版本是JDK1.8.0ElasticSearch8.0及以上的版本最低版本是JDK162、官网下载ES安装包…

Java开发环境配置 “IntelliJ IDEA”(超详细整理,适合新手入门)

前言 &#x1f4dc; “ 作者 久绊A ” 专注记录自己所整理的Java、web、sql等&#xff0c;IT技术干货、学习经验、面试资料、刷题记录&#xff0c;以及遇到的问题和解决方案&#xff0c;记录自己成长的点滴 目录 前言 一、IDEA的介绍 1、大概介绍 2、详细介绍 二、Intelli…

Python中的logging模块

软件开发中通过日志记录程序的运行情况是一个开发的好习惯&#xff0c;对于错误排查和系统运维都有很大帮助。Python标准库自带日志模块&#xff0c;程序的日志功能直接调用标准库的日志模块即可通过日志&#xff0c;开发者可以清楚的了解发生了哪些事件&#xff0c;包括出现了…

Linux随记(五)

一、已用statefulset创了两个nginx副本 web-0 和 web-1 &#xff0c;目的将各自容器的hostname重定向到index.html。但显示的是master节点的hostname。 怎么写才是搞成 pod里面的主机名&#xff1f;#最终解决方法&#xff1a; 在sh -c 双引号里$符号前面加上\转义。 或者 sh -…

Redis原理篇(二)网络模型

一、用户空间和内核空间 应用需要通过Linux内核与硬件交互。 内核本质也是应用&#xff0c;运行的时候也需要CPU资源、内存资源。用户应用也在消耗这些资源。 为了避免用户应用导致冲突甚至内核崩溃&#xff0c;用户应用与内核是分离的&#xff1a; 进程的寻址空间会划分为两…