字符串哈希算法,目的是将一个字符串转换为一个整数,这个整数仅仅受字符串的内容有关,与字符串的长度没有任何的关系,保障字符串不同,得到的整数不同,字符串相同,得到的整数一定相同。通常将字符串转换为整数的算法成为字符串哈希算法。
通常使用的字符串哈希算法如下:
哈希算法: H a s h [ i ] = H a s h [ i − 1 ] ∗ p + s t r [ i − 1 ] − ′ a ′ Hash[i]=Hash[i-1]*p+str[i-1]- 'a' Hash[i]=Hash[i−1]∗p+str[i−1]−′a′
这里p选用的是一个质数,通常可取131313,使用这种算法获得的哈希值能够很大程度上的避免哈希冲突,同时这样计算出的哈希值能通过前缀和的思想,推演出任意连续子串的哈希值,操作如下:
可以看到,通过存储的Hash数组与p的次方,就能够求出任意子串的哈希值,通过这种方法就可以判断任意子串是否出现过。为了更方便的计算p的任意次方,我们预先处理p的幂将其存入数组中,这样就可以快速求得一个子串的哈希值。
公式为:定义 h a s h [ i ] hash[i] hash[i]为索引为 [ 0 , i ] [0,i] [0,i]子串的哈希值,定义 p [ i ] p[i] p[i]表示 p i p^i pi的值,定义所求哈希值子串下标范围是 [ j , i ] [j,i] [j,i],那么该子串哈希值为: h a s h = h a s h [ i ] − p [ i − j + 1 ] ∗ h a s h [ j − 1 ] hash = hash[i]-p[i-j+1]*hash[j-1] hash=hash[i]−p[i−j+1]∗hash[j−1]
那么为什么不使用String直接作为key存入HashMap而是用这种方法计算哈希值呢?
String作为key存入hashMap,每次存入一个数,都会调用equals
,HashCode
方法,这两个方法均是
O
(
N
)
O(N)
O(N)的时间复杂度,但是如果存入的是int
整数类型,因为int的哈希值就是该值本身,因此存入hashMap中仅需要
O
(
1
)
O(1)
O(1)的时间复杂度,大大地提高了运行效率。
以1316. 不同的循环子字符串为例,使用字符串哈希解决
首先通过递推公式将
[
0
,
i
]
[0,i]
[0,i]的哈希值存放在
h
a
s
h
[
i
]
hash[i]
hash[i]数组中,同时将
p
i
p^i
pi存放在
p
[
i
]
p[i]
p[i]数组中方便使用,通过两层for循环遍历所有子串(符合题意子串长度一定是偶数
),找到中间点mid
然后判断mid两侧哈希值是否相同,如果相同就将其哈希值存入Set中用于去重,最终Set中的元素个数就是我们所求的子串数量。
class Solution {
final int P = 131313;
long []p;
long []hash;
public long getHash(String txt,int l,int r){
if(l==0){
return hash[r+1];
}
return hash[r + 1] - getHash(txt, 0, l - 1) * p[r - l + 1];
}
public int distinctEchoSubstrings(String text) {
int n = text.length();
p=new long[n+1];
hash=new long[n+1];
Set<Long> ans=new HashSet<>();
p[0]=1;
for(int i=1;i<=n;++i){
hash[i]=hash[i-1]*P+text.charAt(i-1);
p[i]=p[i-1]*P;
}
for(int i=0;i<n-1;++i){
for(int j=i+1;j<n;j+=2){
int m=i+(j-i)/2;
long hashLeft=getHash(text,i,m);
long hashRight=getHash(text,m+1,j);
if(hashRight==hashLeft){
ans.add(hashLeft);
}
}
}
return ans.size();
}
}
以187. 重复的DNA序列为例,使用字符串哈希快速解题。
- 可能你觉得这题使用滑动窗口也同样能过,确实是这样,所求序列长度固定为10,那么窗口滑动+字符串比较运算量一共就 1 0 6 10^6 106次,能够接受。但是如果数据量特别大,所求序列的长度为100或1000,那么每一次滑动都需要比较100、1000个字符,这样就特别容易超时。因此这题使用字符串哈希算法
- 我们定义一个 h a s h hash hash数组,对于下标 i i i 存放子串 [ 0... i − 1 0 ...i-1 0...i−1]子串的哈希值,同时选用质数 P = 131313 P=131313 P=131313,维护一个质数的次方数组p
class Solution {
public List<String> findRepeatedDnaSequences(String s) {
Map<Long,Integer>map=new HashMap<>();
List<String>ans=new ArrayList<>();
int n=s.length();
long []hash=new long[n+1];
long []p=new long[n+1];
//已知答案的长度
int strLen=10;
p[0]=1;
final int P=131313;
for(int i=1;i<=n;++i){
hash[i]=hash[i-1]*P+s.charAt(i-1);
p[i]=p[i-1]*P;
}
//开始遍历字符
//i为开头的下一个字符
for(int i=1;i+strLen-1<=n;++i){
//j为结束的下一个字符
int j=i+strLen-1;
long curHash=hash[j]-hash[i-1]*p[strLen];
int time=map.getOrDefault(curHash,0);
if(time==1){
ans.add(s.substring(i-1,j));
}
map.put(curHash,time+1);
}
return ans;
}
}