1. 程式人生 > 其它 >學習筆記:字串-Hash

學習筆記:字串-Hash

亂七八糟的各種各樣的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衝突率更低,可以:

  1. 使用單獨的一種hash,但通過改變乘數或者餘數得到多組hash值來同時進行比較

  2. 使用多種不同的hash,得到多組hash值同時比較

  3. 使用hash表,將同一hash值的衝突的資料存在一個連結串列裡,存在衝突時通過訪問列表裡的所有元素來確定

    ……

反正方法有很多

乘法Hash(進位制Hash)

最基本也是花樣最多的一種雜湊。(好像還叫BKDRHash)

核心思想就是把字串看成是一個26進位制的陣列(這個是對於純小寫\純大寫的字串,如果加上數字就是36進位制,如果區分大小寫就是52進位制……)然後把他換算回十進位制。

如果不能確定取多少作為乘數的話,那就取33就行(好像如果進位制數大於33,乘數取33也是不錯的)。取31的原因主要有兩點:

  1. ​ 33是一個奇質數(雖然偶質數就那一個),它可以保證因數最少,從而儘可能減少雜湊衝突的發生;

  2. ​ 33在進行乘法運算時會更快,因為 \(x*33\)​​ 可以被優化成 \((x<<5)+x\)​​

如果得到的十進位制數超出了 int 或者 long long 的範圍,有下面幾種方式來處理:

  1. 使用unsigned讓它隨便溢位,反正溢位了還是正數;

  2. ​ 取模:

    ​ 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} \]