學習筆記:字串-Hash
Hash
Hash演算法(雜湊演算法)實際上就是將一串資料(一般是陣列或字串)通過一些特定的方法轉化成可以代表這些資料的一個數(Hash值)。通過雜湊就可以快速的完成對這一串資料的一些比較,比如說當你要檢驗很多組字串之間有哪些是一樣的,就可以先算出各個字串的Hash值,再通過比較Hash值是不是一樣來替代更慢的普通字串比較。
或者說Hash是一種從大範圍到小範圍的對映,陣列或者字串是一組大範圍的資料,而通過Hash處理後得到的Hash值就是一個小範圍的資料(一般是一個整型)。
從再數學一點的角度來看,Hash就是一個數學函式,你給它一些資料,它給你一個特徵值。你給它的資料只能轉化成一個特徵值,同時理想狀態下一個特徵值只對應一組資料。
要想做到讓這個Hash值能夠代表這一串資料,就需要使用乘法或者位運算(或者全都要)。
Hash衝突
由於Hash實際上是通過一些運算來計算出Hash值,所以有可能會出現明明是兩個不同的字串 $a , b $ ,但是最後卻得到了一樣的Hash值( \(hash(a)==hash(b)\) ),這種情況我們就叫做Hash衝突。
當然並不是Hash所有都一定會有衝突(康託展開就是一個很好的例子),但是在面對由於資料太多的而不能保證無衝突的時候,我們要做的就是選擇一個最好的Hash方式來儘可能的減小Hash衝突的發生率來保證執行結果的正確性。
Hash種類
Hash有超級多種,如果想要Hash衝突率更低,可以:
-
使用單獨的一種hash,但通過改變乘數或者餘數得到多組hash值來同時進行比較
-
使用多種不同的hash,得到多組hash值同時比較
-
使用hash表,將同一hash值的衝突的資料存在一個連結串列裡,存在衝突時通過訪問列表裡的所有元素來確定
……
反正方法有很多
乘法Hash(進位制Hash)
最基本也是花樣最多的一種雜湊。(好像還叫BKDRHash)
核心思想就是把字串看成是一個26進位制的陣列(這個是對於純小寫\純大寫的字串,如果加上數字就是36進位制,如果區分大小寫就是52進位制……)然後把他換算回十進位制。
如果不能確定取多少作為乘數的話,那就取33就行(好像如果進位制數大於33,乘數取33也是不錯的)。取31的原因主要有兩點:
-
33是一個奇質數(
雖然偶質數就那一個),它可以保證因數最少,從而儘可能減少雜湊衝突的發生; -
33在進行乘法運算時會更快,因為 \(x*33\) 可以被優化成 \((x<<5)+x\)
如果得到的十進位制數超出了 int 或者 long long 的範圍,有下面幾種方式來處理:
-
使用unsigned讓它隨便溢位,反正溢位了還是正數;
-
取模:
hash裡關於取模的模數(雜湊因子)該怎麼取是一個非常經驗的東西,這裡有一個常用雜湊模數表,或者直接記兩個:int 範圍內 :\(402653189\) ; long long 範圍內:\(212370440130137957\) (其實直接拿 \(1e7+7\) 和 \(1e9+9\) 也是可以的)
程式碼的話就是這樣:
ull hash(string x){
ull res=0;
int hash_base=33;
//int hash_mod=402653189;
for(int i=0;i<x.length();i++){
res=res*base+x[i];
//res%=hash_mod;
}
return hash;
}
位運算Hash
位運算的hash快到起飛 而且也很好記
它主要是通過異或和移位來讓每一個數據都能影響到最後的hash值。相當於是讓hash值的不同幾位儲存幾個資料異或的結果。
程式碼的話是這樣:
ull hash(string x){
ull res=0;
for(int i=0;i<x.length();i++){
res=(res<<4)^(res>>28)^x[i];
}
return res;
}
FNVHash
乘法Hash的一種高階變種玩意。全稱叫 Fowler-Noll-Vo演算法
它同時使用位運算和乘法來計算hash值。這玩意就是硬記一下hash初始值和乘數(這個是固定的對應值,不要亂改):
hash值位數 | hash初始值 | 乘數 |
---|---|---|
32 位 | 2166136261 | 16777619 |
64 位 | 14695981039346656037 | 1099511628211 |
程式碼的話就是這樣:
ull hash(string x){
ull res=2166136261;
int FNV_prime=16777619;
for(int i=0;i<x.length();i++){
hash^=x[i];
hash*=FNV_prime;
}
return hash;
}
其實上面這個是FNVHash的一種,叫FNV-1a ,還有就是交換了一下異或和乘的順序的FNV-1 (他們說FNV-1a是要比FNV-1好一點,儘量用FNV-1a)
Hash的應用
其實只要扯到字串判斷啊、陣列判斷啊、列舉字串減少重複列舉啊都可以用hash(想用就用就行)
子串判斷
(其實這玩意應該說是乘法hash的應用)
根據乘法hash的性質,我們可以得到這樣一個遞推求hash的方法:( 其實就是拿陣列存了普通乘法hash裡每一個的res值,相當於是當前字串的hash值)
ull hash_val[1000010]={0};
ull hash(string x){
int hash_base=33;
for(int i=0;i<x.length();i++){
hash=res*base+x[i];
}
return hash;
}
如果我們現在有一個字串 \(x\) ,我們現在想要求 \(x[l]\sim x[r]\) 這個區間的子串的值,我們只需要知道 \(hash\_val[l]\) 和 \(hash\_val[r]\) 就可以計算出這個子串的hash值:
\[hash=hash\_val[r]-hash\_val[l-1]*base^{r-l+1} \]