1、概念
將一個字符串轉化成一個整數,並保證字符串不同,得到的哈希值不同,當然字符串相同的時候保證哈希值相同。這樣就可以用來判斷一個該字串是否重復出現過。
為什么需要有這種算法,例如在java中,定義一個map,如果直接把string當做鍵,則每次在map中查找時要一個一個字符地找,跟存在數組中區別不大,而比較數值自然更快。
下面介紹一種常用方法
2、字符串哈希算法
由哈希函數的性質,對於一個字符串:S=s1s2...sn,我們把每個字符轉換成idx(si)=si-'a',當然直接用字符串的ASCII碼表示也可以,則哈希模型為Hash(i)=Hash(i-1)*p+idx(si),其中p為素數。最終算出的Hash(n)作為該字符串的哈希值。
所以構造哈希函數的關鍵點在於使不同字符串的哈希沖突率盡可能小。
2.1 一般過程
取一固定值P,P為質數,把字符串看作P進制數,並分配一個大於0的數值,代表每種字符。 一般來說,我們分配的數值都遠小於P。例如,對於小寫字母構成的字符串,可以令a = 1 , b = 2 , . . . , z = 26 。 a=1,b=2,...,z=26。a=1,b=2,...,z=26,或者直接使用ASC碼。
取一固定值M,求出該P進制數對M的余數,作為該字符串的Hash值。
一般來說,我們取P=131或P=131313,此時Hash值產生沖突的概率極低,只要Hash值相同,我們就可以認為原字符串是相等的。
通常我們取M=2^64,即直接使用unsigned long long類型存儲這個Hash值,在計算時不處理算術溢出問題,產生溢出時相當於自動對2^64取模,這樣可以避免低效的取模運算。
除了在及特殊構造的數據上,上述Hash很難產生沖突,一般情況下上述Hash算法完全可以出現在題目的標准解答中。
2.2 計算
對字符串的各種操作,都可以直接對P進制數進行算數運算反映到Hash值上。
- 如果我們已知字符串S的Hash值為H(S),在S后添加一個字符c構成的新字符串S+c的Hash值就是
H(S+c) = ( H(S) ∗ P + value[c] ) mod M
其中乘P就相當於P進制下的左移運算,value[c]是我們的為c選定的代表數值。
- 如果我們已知字符串S的Hash值為H(S),字符串S+T的Hash值為H(S+T),那么字符串T的Hash值
H(T) = ( H(S+T) − H(S) ∗ P ^length(T) ) mod M
這就相當於通過P進制下在S后邊補0的方式,把S左移到與S+T的左端對其,然后二者相減就得到了H(T)。
看着這個公式人畜無害,但是對於取模運算來說要更加謹慎,注意括號里面是剪發,有可能是負數,故可以如下修正:
H(T) = ( ( H(S+T) − H(S) ∗ P ^length(T) ) mod M + M )mod M
可能看着不是很好理解,舉個栗子:
例如,S=“abc”,c=“d”,T=“xyz”,則: S表示為P進制數: 1 2 3 H(S) = 1 ∗ P2 + 2 ∗ P + 3 H(S+c) = 1 ∗ P3 + 2 ∗ P2 + 3 ∗ P + 4 = H(S) ∗ P + 4 S+T表示為P進制數: 1 2 3 24 25 26 H(S+T) = 1 ∗ P5 + 2 ∗ P4 + 3 ∗ P3 + 24 ∗ P2 + 25 ∗ P + 26 S在P進制下左移length(T) 位: 1 2 3 0 0 0 二者相減就是T表示為P進制數: 24 25 26 H(T) = H(S+T) − ( 1∗P2 + 2 ∗ P + 3 ) ∗ P3 = 24 ∗ P2 + 25 ∗ P + 26
根據上面兩種操作,我們可以通過O(N)的時間預處理字符串所有前綴Hash值,並在O(1)的時間內查詢它的任意子串的Hash值。
2.3 代碼
放個python版本
def getStrHash(s): """ 注意 python 沒有溢出 :param s: :return: """ n = len(s) h = [0] * (n + 1) # 存儲字符串前綴的hashcode p = [0] * (n + 1) p[0] = 1 # 預處理p的n次方 prime = 13131 # p進制 for i in range(1, n + 1): h[i] = h[i - 1] * prime + ord(s[i - 1]) p[i] = p[i-1] * prime print(h) print(p) length = 4 #子串的長度 res = [] #存儲子串 resHash = [] # 存儲子串的hashcode for i in range(n): j = i + length if j <= n: res.append(s[i:j]) hash = h[j]-h[i]*p[j-i] resHash.append(hash) for i in range(len(res)): print(res[i], resHash[i]) if __name__ == '__main__': s = "abcdefgabcdefg" getStrHash(s)
2.4 一些tips
- 雙哈希
上述使用的單哈希方式,p,mod均為質數,p<mod,p、mod取盡量大時沖突很小。除此之外,我們也可以使用雙哈希方法,來減小沖突
雙哈希方法:將字符串用不同mod單哈希兩次,結果用二元組表示
Hash1[i] = ( Hash1[i-1] * p + idx(si) )% mod
Hash2[i] = ( Hash2[i-1] * p + idx(si) )% mod
Hash[i]:<Hash1[i],Hash2[i]>
這種方法很安全。
- 質數的選擇
像1e9+7等常見素數很可能被出題人卡,所以可以選擇一些其他的素數:
比如,131 1313 131313 字符串哈希本身存在哈希沖突的可能,一般會在嘗試131之后嘗試使用1313之類,然后再嘗試使用更大的質數。
- 取模
取模不一定是必要的,例如Python可以不用取模 java可以靠自動溢出