哈希表及模板
1. 哈希表的主要内容
2. 哈希表的定义
哈希表是一种数据结构。主要作用就是将一个范围很大的数据,映射到较小的范围(0 -N)(一般来讲N为10 的5 次方~10 的6 次方之间)。映射之后,可以进行高效的存储和查找。在哈希表中,我们通常用哈希函数h (x)来完成映射的功能。
因此,如何设计哈希函数就成了哈希表中至关重要的功能之一。一般来讲,比较常用的哈希函数主要是模除法。例如,我们需要将一堆数映射到0 -9 之间,那么哈希函数直接设置为:
h(x) = x mod 10
这样的哈希函数就可以满足我们的要求。模除法是哈希函数最常用的形式。
需要注意的是,模除的数最好取成一个质数。因为这样可以使得冲突最少。
注意:在进行模除的时候,结果可能为负数,这样的情况下我们为了保持结果为正数,可以进行如下处理:
int k = (x
这样的话,就可以保证哈希值k始终为正数。
当我们使用哈希函数将一堆数映射到指定范围时,可能会出现冲突的情况。例如,我们将若干个数映射到0 -9 之间的数,就会产生冲突。
h (10 ) = 10 mod 10 = 0
h (20 ) = 20 mod 10 = 0
根据上述的例子,我们可以发现:将10 和20 分别使用h (x)进行映射,发现映射的是同一个位置。这就产生了上述所说的冲突。
我们需要处理这样的冲突,以便于哈希表可以存储类似10 和20 这样的可能会产生冲突的数。
根据处理冲突方式的不同,导致了哈希表存储结构的不同。接下来分别介绍一下哈希表的两大存储结构:开放寻址法和拉链法。
这里需要补充一下:离散化是及其特殊的哈希方式。因为,离散化通常需要保序。而本博客所讲述的哈希表是一般意义上的哈希方式,不要求保序。
3. 哈希表的操作
在算法题中,哈希表一般会有两大基本操作:
1 . 添加
先通过h (x)来进行映射,根据映射的位置进行存储即可。
2 . 查找
先通过h (x)来进行映射,根据映射的位置进行查找即可。
对于删除操作,一般算法题是没有的。如果非要将哈希表的元素进行删除的话,我们可以将对应的元素设置一个删除标记(bool类型变量)。当查找到这个元素时,如果该标记为true代表该元素已被删除,否则就未被删除。
需要注意的是,无论哈希表采用何种存储结构,哈希表的操作一般都是O (1 )。
3. 哈希表的存储结构
拉链法原理:
1 . 首先使用一个数组来存储h (x)所映射的哈希值。
2 . 当产生冲突时,在数组对应的元素下拉一个单链表,将冲突的值存在链表中。
开放寻址法原理:
1 . 首先使用一个数组来存储h (x)所映射的哈希值。这个数组的长度原则上说是题目数据规模个数的2 ~3 倍。这样的话,发生冲突的概率较低。
2 . 当在k(k为哈希值)这个位置产生冲突时,就去看下一个位置是否空闲,如果空闲就存储,否则就继续往下一个位置寻找,直到找到空闲的位置为止。
4. 哈希表存储结构模板
int h[N], e[N], ne[N], idx;
void insert (int x)
{
int k = (x % N + N) % N;
e[idx] = x;
ne[idx] = h[k];
h[k] = idx ++ ;
}
bool find (int x)
{
int k = (x % N + N) % N;
for (int i = h[k]; i != -1 ; i = ne[i])
if (e[i] == x)
return true ;
return false ;
}
int h[N];
int find (int x)
{
int t = (x % N + N) % N;
while (h[t] != null && h[t] != x)
{
t ++ ;
if (t == N) t = 0 ;
}
return t;
}
5. 例题
https: //www.acwing.com/problem /content/ 842 /
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std ;
const int N = 100003 ;
int e[N],ne[N],h[N],idx = 0 ;
void insert (int x) {
int k = (x % N + N) % N;
e[idx] = x;
ne[idx] = h[k];
h[k] = idx++;
}
bool query (int x) {
int k = (x % N + N) % N;
for (int i = h[k]; i != -1 ; i = ne[i]){
if (e[i] == x){
return true ;
}
}
return false ;
}
int main () {
int n;
scanf ("%d" ,&n);
char op[2 ];
memset (h,-1 ,sizeof (h));
while (n--){
int number;
scanf ("%s%d" ,op,&number);
if (op[0 ] == 'I' ){
insert(number);
}else {
if (query(number)){
printf ("Yes\n" );
}else {
printf ("No\n" );
}
}
}
return 0 ;
}
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std ;
const int N = 200003 ;
const int null = 0x3f3f3f3f ;
int h[N];
int find (int x) {
int k = (x % N + N) % N;
while (h[k] != null && h[k] != x){
k++;
if (k == N){
k = 0 ;
}
}
return k;
}
int main () {
memset (h,0x3f ,sizeof (h));
int n,number;
char op[2 ];
scanf ("%d" ,&n);
while (n--){
scanf ("%s" ,op);
scanf ("%d" ,&number);
int k = find(number);
if (op[0 ] == 'I' ){
h[k] = number;
}else {
if (h[k] != null){
printf ("Yes\n" );
}else {
printf ("No\n" );
}
}
}
return 0 ;
}
在实际的题目中,这两种方法任选其一即可。
6. 字符串哈希方式
这里所阐述的字符串哈希方式是非常特殊的哈希方式,叫做:字符串前缀哈希法。
这种方式在算法题中经常出现,因此有必要在这里阐述一下。
注意:字符串前缀哈希法一般不需要考虑和处理冲突。(在后面会讲到)
字符串前缀哈希法原理:
假设给定一个字符串str。str = "ABCABCDEYXCACWING"
1 . 预处理所有前缀的字符串哈希值。
例如:h [0 ] = 0
h [1 ] = "A" 的哈希值;
h [2 ] = "AB" 的哈希值;
h [3 ] = "ABC" 的哈希值;
h [4 ] = "ABCA" 的哈希值;
.....
其中,h [0 ]为特殊处理,代表前0 个字符串前缀的哈希值。
所有前缀的子串哈希值我们也可以通过公式求得:
h (i) = h (i-1 ) * p + str[i ];
根据上述的内容,我们可以引出两个问题。
1.1 字符串前缀的哈希值怎么定义?
我们可以将整个字符串看作是一个p进制数,而字符串当中的每一个字符看作是这个p进制数的每一位数字。我们来举一个案例:
假设字符串str="ABCD" ,那么怎么求其前缀的哈希值?
我们可以将str看作是一个P进制数,而A、B、C、D则看作是这个P进制数的每一个位数。这个位数我们可以从A-Z =>1 -26 (这里每个字母不能映射为0 )。因此ABCD=>(1234 )p。那么这个p进制数(1234 )p转换为10 进制数就是1 *p的3 次方+2 *p的2 次方+3 *p的1 次方+4 *p的0 次方。因此,我们可以通过这个方式来将一个字符串映射为一个数字。这个数字可能会很大,因此我们还需要将这个数字mod上一个很小的数Q。即:
ABCD的哈希值 = (1234 )p = (1 *p的3 次方+2 *p的2 次方+3 *p的1 次方+4 *p的0 次方) mod Q;
1.2 p和Q怎么取?
这里有一个经验值:p一般取131 或13331 ;Q一般取2 的64 次方。
如果这么取值的话,我们就不需要考虑冲突问题。
2 . 我们可以利用前面求得的所有字符串前缀哈希值来求得任何一个子串的哈希值。主要是通过如下公式来实现:
假设,给定一个字符串str,我们已经求得这个str的所有字符串前缀的哈希值。那么,我们现在想要求得str中[L ,R ]这个区间的子串哈希值。
我们已知:h [R ]代表1 到R 这个字符串前缀的哈希值(1 代表字符串所对应数字的高位,R 代表低位)
h [L -1 ]代表1 到L-1 这个前缀的哈希值(1 代表字符串所对应数字的高位,L-1 代表低位)
因此,h [R ]的哈希值:p的R -1 次方+...+p的0 次方
h [L -1 ]的哈希值:p的L-2 次方+...+p的0 次方
求得[L ,R ]这个区间子串的哈希值公式:
其中r -l +1 代表R 和L-1 之间所相差的位数
h [R ] - (h [L -1 ] * p的(R -L +1 )次方);
3 . 拥有了以上的公式后,我们就可以在O(1 )的时间内求出任何一个子串的哈希值。
字符串哈希的作用:
当我们想快速的判断两个字符串是否相等,就可以用字符串哈希这种方式。时间复杂度为O (1 )
我们可以通过上述案例,来解释一下区间子串的哈希值公式。
7. 字符串哈希模板
typedef unsigned long long ULL;
ULL h[N], p[N];
p[0 ] = 1 ;
for (int i = 1 ; i <= n; i ++ )
{
h[i] = h[i - 1 ] * P + str[i];
p[i] = p[i - 1 ] * P;
}
ULL get (int l, int r)
{
return h[r] - h[l - 1 ] * p[r - l + 1 ];
}
8. 例题
https: //www.acwing.com/problem /content/ 843 /
#include <iostream>
#include <cstdio>
using namespace std ;
const int N = 100010 ;
const int P = 131 ;
unsigned long long h[N],p[N];
unsigned long long get (int l,int r) {
return h[r] - (h[l-1 ]*p[r-l+1 ]);
}
int main () {
int n,m;
char str[N];
scanf ("%d%d" ,&n,&m);
scanf ("%s" ,str+1 );
p[0 ] = 1 ;
for (int i=1 ;i<=n;i++){
p[i] = p[i-1 ] * P;
h[i] = h[i-1 ] * P + str[i];
}
while (m--){
int l1,r1,l2,r2;
scanf ("%d%d%d%d" ,&l1,&r1,&l2,&r2);
if (get(l1,r1) == get(l2,r2)){
printf ("Yes\n" );
}else {
printf ("No\n" );
}
}
return 0 ;
}
作者:gao79138
链接:https://www.acwing.com/
来源:本博客中的截图、代码模板及题目地址均来自于Acwing。其余内容均为作者原创。
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现