滚动哈希解决子串匹配问题
滚动哈希
我们判断两个字符串是否相同,往往是通过比较两个字符串的哈希。
常用语言中计算字符串哈希的方法往往是一个字符一个字符的计算,导致计算字符串哈希的时间复杂度是O(C),其中C是字符串的长度。
如果我们要计算字符串长度为C的子串的哈希,时间复杂度为O(NC)。
我们可以利用前缀的思想,采用滚动哈希。
滚动哈希就是一种O(1)时间复杂度的计算子串哈希的方法。
重复的DNA序列
//一种直观的思路,遍历所有可能存在的子串,然后判断子串出现了几次(算哈希)
//需要注意的是,传统对字符串求哈希的过程,实际是O(C)复杂度,C是字符串的长度
//因此我们可以使用滚动哈希把这一过程变为O(1)
class Solution {
public List<String> findRepeatedDnaSequences(String s) {
int n = s.length();
//P是一个质数。一般我们先尝试13,再尝试131313,来避免哈希冲突
int P=131313;
List<String> ans = new ArrayList<>();
int[] p=new int[n+1];
//h[k]表示前k-1个字符的哈希值
int[] h=new int[n+1];
p[0] = 1;
for (int i = 1; i<=n; i++) {
h[i]=h[i-1]*P+s.charAt(i-1);
p[i]=p[i-1]*P;
}
Map<Integer, Integer> map = new HashMap<>();
for(int i=1;i+10-1<=n;i++){
int j=i+10-1;
//这里✖p[10],10是子串的长度
//至于为啥是这样,建议举个例子自己算算
int hash=h[j]-h[i-1]*p[10];
int cnt = map.getOrDefault(hash, 0);
if (cnt == 1) ans.add(s.substring(i - 1, i + 10 - 1));
map.put(hash, cnt + 1);
}
return ans;
}
}
实现strstr()
//滚动哈希也能解决
class Solution {
public int strStr(String ss, String pp) {
int n=ss.length();
int m=pp.length();
int P=131313;
long hash=0;
//先算我们的目标哈希
for(int i=0;i<m;i++){
hash=hash*P+pp.charAt(i);
}
long[] p=new long[n+1];
long[] h=new long[n+1];
p[0]=1;
for(int i=1;i<=n;i++){
h[i]=h[i-1]*P+ss.charAt(i-1);
p[i]=p[i-1]*P;
}
for(int i=1;i+m-1<=n;i++){
int j=i+m-1;
long cur=h[j]-h[i-1]*p[m];
if(cur==hash){
return i-1;
}
}
return -1;
}
}
检查一个字符串是否包含所有长度为k的二进制子串
//滚动哈希很好用!
class Solution {
public boolean hasAllCodes(String s, int k) {
int n=s.length();
long[] p=new long[n+1];
long[] h=new long[n+1];
int P=13;
p[0]=1;
for(int i=1;i<=n;i++){
h[i]=h[i-1]*P+s.charAt(i-1);
p[i]=p[i-1]*P;
}
HashSet<Long> set=new HashSet<>();
for(int i=1;i+k-1<=n;i++){
long hash=h[i+k-1]-h[i-1]*p[k];
set.add(hash);
}
return set.size()==Math.pow(2,k);
}
}
我有一壶酒
足以慰风尘
尽倾江海里
赠饮天下人