算法競賽——哈希表
- 2021 年 11 月 20 日
- 筆記
- 算法競賽——數據結構
一、哈希表介紹
什麼是哈希表?
散列表(Hash table,也叫哈希表),是根據鍵(Key)而直接訪問在內存存儲位置的數據結構。也就是說,它通過計算一個關於鍵值的函數,將所需查詢的數據映射到表中一個位置來訪問記錄,這加快了查找速度。這個映射函數稱做散列函數,存放記錄的數組稱做散列表。
哈希表有什麼用?
在 OI 中,最常見的情況應該是鍵值為整數的情況。當鍵值的範圍比較小的時候,可以直接把鍵值作為數組的下標,但當鍵值的範圍比較大,比如以 10^9
範圍內的整數作為鍵值的時候,就需要用到哈希表。即把一個龐大的空間/值域)映射到一個較小的空間,0~10^9 ---> 0~10^5
,即0 ~ N
的一個數
二、哈希函數兩大操作
什麼是哈希函數?
要讓鍵值對應到內存中的位置,就要為鍵值計算索引,也就是計算這個數據應該放到哪裡。這個根據鍵值計算索引的函數就叫做哈希函數,也稱散列函數。
1.計算哈希函數
\]
2.解決衝突
如果對於任意的鍵值,哈希函數計算出來的索引都不相同,那隻用根據索引把 (key, value)
放到對應的位置就行了。但實際上,常常會出現兩個不同的鍵值,他們用哈希函數計算出來的索引是相同的。這時候就需要一些方法來處理衝突。在 OI 中,最常用的方法是拉鏈法和開放尋址法。
2.1.拉鏈法
拉鏈法是在每個存放數據的地方開一個鏈表,如果有多個鍵值索引到同一個地方,只用把他們都放到那個位置的鏈表裡就行了(鏈表的添加操作)。查詢的時候需要把對應位置的鏈表整個掃一遍,對其中的每個數據比較其鍵值與查詢的鍵值是否一致。
思路:利用鏈表處理衝突。輸入x,將x進行mod映射為k,然後h[k]
作為鏈表的頭指針(相當於head記錄的是頭節點位置的下標),然後就是鏈表頭插法
操作:e[idx] = x, ne[idx] = h[k], h[k] = idx ++;
這樣就把新元素像拉鏈一樣掛在了h數組的下面了。尋找也是同理先求x的映射k,然後從h[k]
開始鏈表的遍歷。當然,在main函數中記得要把h數組全部初始化為-1(空指針)。下圖為數組模擬單鏈表的頭插法操作:
【拉鏈法——參考代碼】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5 + 3; // 大於10萬的第一個質數
int h[N];
int e[N], ne[N], idx; // idx表示當前用到了哪一個位置
// 頭插法,將x查到單鏈表h[k]的頭部————看之前模擬單鏈表的頭插法
void insert(int x)
{
int k = (x % N + N) % N; // 將x映射成哈希值
e[idx] = x;
// h[k]是hash之後下標為k的位置存放的idx(或者說指向的元素),之前原本h[]的所有值初始化為-1(head指向頭節點的指針(下標))
ne[idx] = h[k];
h[k] = idx;
idx ++;
}
bool find(int x)
{
int k = (x % N + N) % N;
// 在k的鏈錶鏈表找是否存在X,h[k]即為指向頭節點的下標(指針)
for(int i = h[k]; i != -1;i = ne[i])
if(e[i] == x)
return true;
return false;
}
int main()
{
int n;
cin >> n;
// 千萬別忘了初始化,不然鏈表就沒有頭節點啦!
memset(h, -1 , sizeof h); // 初始化鏈表,一開始為空:-1
while (n -- )
{
string opt;
int x;
cin >> opt;
if(opt == "I")
{
cin >> x;
insert(x);
}
else
{
cin >> x;
if(find(x)) cout << "Yes" << endl;
else cout << "No" << endl;
}
}
return 0;
}
註:
h[N]相當於每個單鏈表的head指針。idx存儲當前用到了哪個節點,不同單鏈表裡的節點都是從idx這裡分配的,所以它們可以共用一個idx變量。之前原本h[]的所有值初始化為-1(head指向頭節點的指針)
【C++ STL】
#include<bits/stdc++.h>
using namespace std;
set<int>s;
char op;
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
int r;
cin>>op>>r;
if(op=='I')
s.insert(r);
else {
if(s.find(r)==s.end())
cout<<"No"<<endl;
else cout<<"Yes"<<endl;
}
}
return 0;
}
2.2.開放尋址法
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
// 數組長度一般開到個數上限的2-3倍。足夠存放,這樣大概率就沒有衝突了
// null 用來判斷當前槽位是否被使用過(不超過int範圍的無窮大的數)
//N 為大於範圍的第一個質數
const int N = 2e5 + 3, 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; // 找到尾了,從頭再來
}
//返回:可以插入x的位置k,若位置k里已存在x直接返回k的位置
return k;
}
int main()
{
int n;
cin >> n;
memset(h, 0x3f, sizeof h); // 一開始所有槽全部初始化為null
while (n -- )
{
string opt;
int x;
cin >> opt;
if(opt == "I")
{
cin >> x;
//找到x能夠插入的位置
int k = find(x);
h[k] = x;
}
else
{
cin >> x;
//找x的位置看看是否存在
int k = find(x);
// 判斷h[]中是否存在x
if(h[k] != null) cout << "Yes" << endl;
else cout << "No" << endl;
}
}
return 0;
}
開放尋址法總結
- memse是按位元組來初始化的,int中有四個位元組,初始化成0x3f就是將每個位元組都初始化成0x3f,所以每個int就是 0x3f3f3f3f。
- 數組長度一般開到個數上限的2-3倍。
三、memset總結**
memse是逐一位元組的進行初始的!
- 使用memset初始化一定要慎重,一般只用來初始化0、-1、0x3f這幾個數字,其他的建議使用循環初始化,其他值盡量用for循環吧。
- 作為無窮大,一個數除了要保證足夠大外,還要保證不能溢出。
使用0x3f3f3f3f作為INF主要原因是,兩個0x3f3f3f3f的和只比int類型的最大值小一點,這樣既能保證一般情況下的足夠大,在兩個無窮相加時還能夠保證不會溢出。
四、整數哈希模板總結
拉鏈法:
// 拉鏈法
const int N = 大於範圍的第一個質數(先求一下)
int h[N], e[N], ne[N], idx;
// 插入x(頭插法——可以回顧之前數組模擬單鏈表的操作)
void insert(int x)
{
int k = (x % N + N) % N;
e[idx] = x;
ne[idx] = h[k];
h[k] = idx;
idx ++;
}
// 查詢是否存在x
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;
}
註:千萬別忘記了初始化鏈槽數組h[]:memset(h, -1, sizeof h)
開放尋址法:
// 開放尋址法
const int N = 大於2~3倍範圍的第一個質數(先求一下), null = 0x3f3f3f3f; // null用來判空
int h[N]; // 數組的長度一般開到範圍的2~3倍
// 尋找可以插入x的位置k/返回已存在x的位置k
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;
}
註:千萬別忘記了初始化數組h[]:memset(h, 0x3f, sizeof h)
五、字符串哈希
(字符串哈希) O(n)+O(m)
全稱字符串前綴哈希法,把字符串變成一個p進制數字(哈希值),實現不同的字符串映射到不同的數字。
映射公式 (X1×Pn−1+X2×Pn−2+⋯+Xn−1×P1+Xn×P0)modQ
\]
注意事項:
-
任意字符不可以映射成0,否則會出現不同的字符串都映射成0的情況,比如A,AA,AAA皆為0
-
\[衝突問題:通過巧妙設置P (131 或 13331) , Q (2^{64})的值,一般可以理解為不產生衝突。
\]
typedef unsigned long long ULL
,當溢出時等價於 mod 2^64方
字符串前綴哈希法的好處:
利用前綴哈希值就可以算出任一子段的哈希值!
【acwing 字符串哈希】
給定一個長度為n的字符串,再給定m個詢問,每個詢問包含四個整數l1,r1,l2,r2l1,r1,l2,r2,請你判斷[l1,r1l1,r1]和[l2,r2l2,r2]這兩個區間所包含的字符串子串是否完全相同。
字符串中只包含大小寫英文字母和數字。
輸入格式
第一行包含整數n和m,表示字符串長度和詢問次數。第二行包含一個長度為n的字符串,字符串中只包含大小寫英文字母和數字。
接下來m行,每行包含四個整數l1,r1,l2,r2l1,r1,l2,r2,表示一次詢問所涉及的兩個區間。
注意,字符串的位置從1開始編號。
輸出格式
對於每個詢問輸出一個結果,如果兩個字符串子串完全相同則輸出「Yes」,否則輸出「No」。每個結果佔一行。
數據範圍
1≤n,m≤1051≤n,m≤105
輸入樣例:
8 3
aabbaabb
1 3 5 7
1 3 6 8
1 2 1 2
輸出樣例:
Yes
No
Yes
思路:
本題需要求到一個字符串中任意兩個區間的子串是否相同
可以轉換為求兩個區間子串的哈希值是否相等
\]
\]
舉例說明:
“ABCDEFGHI”
123456789 (下標)
L R
字符串”A”的 哈希值為 p^0+A
字符串”AB” 哈希值為 p^1+A + p^0+B
字符串”ABC” 哈希值為 p^2+A + p^1+B + C
字符串[1,L-1]的哈希值為 p^3+A + p^2+B + p^1+C + p^0+D
字符串[1,R] 的哈希值為 p^8+A + p^7+B + … + P^0+I 從[1,L-1]每個數都多乘了p^5(p^(R - L + 1))
那麼如何求[L,R]字符串的哈希值呢,根據前綴和的思想,就是h[R] – h[L-1] (h[R]表示從[1,R]的字符串哈希值)
但是發現h[R]從[1,L-1]這一段left,每個數都比right這一段多乘了p^(R-(L-1))
所以字符串從[L,R]的哈希值為h[R] - h[L - 1] * p^(R-L+1)
【參考代碼】
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef unsigned long long ULL;//由於前綴值的值會很大 所以應該將數組中的數據定義為ULL型
const int N = 1e5 + 10, P = 131;// 註:這裡不能用p = 131(小寫——p代表p[]數組的首地址)
char str[N];
ULL h[N]; // h[i]表示前i個字符的哈希值(前綴哈希值)h[0] = 0
ULL p[N]; // p[i]表示p^i次方
int n, m;
// 計算子區間(l ~ r)的哈希值
int get(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i ++ ) cin >> str[i];
p[0] = 1;
for (int i = 1; i <= n; i ++ )
{
h[i] = h[i - 1] * P + str[i]; // 求前綴哈希值
p[i] = p[i - 1] * P; // 計算p[i]
}
while (m -- )
{
int l1, r1, l2, r2;
cin >> l1 >> r1 >> l2 >> r2;
if(get(l1, r1) == get(l2, r2)) puts("Yes");
else puts("No");
}
return 0;
}
字符串前綴哈希法總結
`typedef unsigned long long ULL`,當溢出時等價於 mod 2^64方 ULL h[N],P[N]
衝突問題:通過巧妙設置或的值P=131/P=13331,一般可以理解為不產生衝突。
前綴和公式(前綴哈希值):h[i] = h[i] * p + str[i];
區間和公式[l,r]的哈希值 = h[r] - h[l -1] * p[l - r + 1];
學習內容參考自:
1、[哈希表 – OI Wiki (oi-wiki.org)](//oi-wiki.org/ds/binary-heap/)
2、acwing算法基礎課
註:如果文章有任何錯誤或不足,請各位大佬盡情指出,評論留言留下您寶貴的建議!如果這篇文章對你有些許幫助,希望可愛親切的您點個贊推薦一手,非常感謝啦