- 一、滚动哈希讲解 + 例题讲解
- 1、hashCode
- 2、hashCode与前缀和
- 3、例题讲解
- 二、实战题目
说到滚动哈希,相信没学过的大部分人都被劝退过,但其实高级名词都是用来吓唬新手的。滚动哈希无非就是前缀和,同时滚动哈希,字典树外加dp几乎横扫所有有难度的字符串算法。本节通过例题来讲透Rabin-Karp的滚动哈希。
一、滚动哈希讲解 + 例题讲解 1、hashCode第一步要计算一个字符串的hashCode,用过HashMap应该了解到,HashMap就是利用hashCode来将key-value存储到数组中的
我们以一下方式来定义hashCode
字母a ~ z用数字1 ~ 26来计算hashCode
我们利用一个数b,来作为各个位数上的区分,一般b取131或1331等质数
例如:对于单词apple
- 有a = 1,p = 16,l = 12,e = 5
- hashCode(apple) = 1 * pow(131, 0) + 16 * pow(131, 1) + 16 * pow(131, 2) + 12 * pow(131, 3) + 5 * pow(131, 4)
但hashCode的值可能非常大,所以
找到一个数p,每次求hashCode时,都需要对p取模
p是用来规避哈希冲突的,所以p要比较大,一般根据题目要求来界定
举个例子,若要对banana求hashCode
- 有a = 1,b = 2,n = 14
String s = "banana"; long[] hash = new long[s.length() + 1]; for (int i = 0; i < s.length(); i++) hash[i + 1] = (hash[i] * b + (s.charAt(i) - 'a' + 1)) % p;
当然,为什么要用数组?数组长度为什么要加一?下面来解释
2、hashCode与前缀和要想求出任意一段连续子串的hashCode,其想法类似于前缀和
- 对于前缀和数组s[]:欲求从l到r的和
- sum(l, r) = s[r] - s[l - 1]
- 同理若求出了整个字符串的hashCode
- hash[r]与hash[l - 1]存在一定关系
hash[r]与hash[l - 1]中间相差了一定长度的字符串,其长度为r - l + 1
所以通过求出pow(b, r - l + 1),就可以求出从l到r的hashCode
hashCode(l, r) = hash[r] - hash[l - 1] * pow(b, r - l + 1)
但注意的是,还需要对p取模,并且由于是hashCode之间相减,得到的hashCode有可能是负数
所以hashCode(l, r) = ((hash[r] - hash[l - 1] * pow(b, r - l + 1)) % p + p) % p
1)为了保证一般性,前缀和数组长度要加一
2)对于式子pow(b, r - l + 1)需要另开一个数组long[]来计算b的各个指数的值
LeetCode 1044 最长重复子串
1)初始化
首先看到字符串s的长度比较长,所以这里我使得b取1331,p取2的48次方
int len = s.length();
String ans = "";
long p = (long)Math.pow(2, 48);
long[] hash = new long[len + 1];
long[] p1331 = new long[len + 1]; // 计算pow(b, r - l + 1)
p1331[0] = 1;
for (int i = 0; i < len; i++) {
hash[i + 1] = (hash[i] * 1331 + (s.charAt(i) - 'a' + 1)) % p;
p1331[i + 1] = p1331[i] * 1331;
}
2)二分答案法求得最大长度
二分答案法的思路可以看我另一篇博客:算法合集:二分
先定义l = 0和r = len - 1来界定答案的长度,而mid就成了当前需要计算的长度
int l = 0, r = len - 1;
while (l < r) {
int mid = (l + r) >> 1;
}
接下来我们需要一个HashSet,对于长度为mid的子串,从左到右遍历字符串s,并保存hashCode,若遇到了相同的hashCode则记录答案
int l = 0, r = len - 1;
while (l < r) {
int mid = (l + r) >> 1;
HashSet set = new HashSet<>();
for (int i = mid; i <= len; i++) {
long h = ((hash[i] - hash[i - mid] * p1331[mid]) % p + p) % p;
if (set.contains(h)) {
ans = s.substring(i - mid, i);
break;
}
set.add(h);
}
}
还需要一个boolean find用来记录:当前长度为mid时,是否找到了答案,若找到了,则移动l = mid去试一个更大的mid,若没找到,令r = mid - 1,去试一个更小的mid
int l = 0, r = len - 1;
while (l < r) {
boolean find = false;
int mid = (l + r + 1) >> 1; // 若不+1,l = mid会造成死循环
HashSet set = new HashSet<>();
for (int i = mid; i <= len; i++) {
long h = ((hash[i] - hash[i - mid] * p1331[mid]) % p + p) % p;
if (set.contains(h)) {
find = true;
ans = s.substring(i - mid, i);
break;
}
set.add(h);
}
if (find) l = mid;
else r = mid - 1;
}
最后返回ans即可
二、实战题目LeetCode 28 实现strStr()
LeetCode 187 重复的DNA序列
LeetCode 718 最长重复子数组



