滚动哈希——Rabin-Karp
说到滚动哈希,相信没学过的大部分人都被劝退过,但其实高级名词都是用来吓唬新手的。滚动哈希无非就是前缀和,同时滚动哈希,字典树外加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
的各个指数的值
3、例题讲解
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<Long> 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<Long> 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 最长重复子数组