滚动哈希:常数时间生成哈希码
生成一个长度为 L 数组的哈希码,需要 O(L) 时间。
如何在常数时间生成滑动窗口数组的哈希码?利用滑动窗口的特性,每次滑动都有一个元素进,一个出。
由于只会出现小写的英文字母,因此可以将字符串转化成值为 0 到 25 的整数数组: arr[i] = (int)S.charAt(i) - (int)‘a’。
按照这种规则,abcd 整数数组形式就是 [0, 1, 2, 3],转换公式如下所示。
可以将上面的公式写成通式,如下所示。其中 ci为整数数组中的元素,a = 26,其为字符集的个数。类似于26进制数转换成10进制数一样。
下面来考虑窗口从 abcd 滑动到 bcde 的情况。这时候整数形式数组从 [0, 1, 2, 3] 变成了 [1, 2, 3, 4],
数组最左边的 0 被移除,同时最右边新添了 4。滑动后数组的哈希值可以根据滑动前数组的哈希值来计算,
计算公式如下所示。
写成通式如下所示。
如何避免溢出:
aL可能是一个很大的数字,因此需要设置数值上限来避免溢出。
设置数值上限可以用取模的方式,即用 h % modulus 来代替原本的哈希值。
hash碰撞问题:
最后如果得到的hash之相同,还要检查子串和目标串是否相同
复杂度分析时间复杂度:
O(N),计算 needle 字符串的哈希值需要 O(L) 时间,之后需要执行 (N−L) 次循环,每次循环的计算复杂度为常数。
空间复杂度:O(1)。
Rabin-Karp算法被称道的三个原因
它可以用来检测抄袭,因为它能够处理多模式匹配;
Rabin-Karp算法能够有效地检测抄袭
虽然在理论上并不比暴力匹配法更优,但在实际应用中它的复杂度仅为O(n+m);
如果能够选择一个好的哈希函数,它的效率将会很高,而且也易于实现。
Rabin-Karp算法被诟病的两个原因
有许多字符串匹配算法的复杂度小于O(n+m);
有时候它和暴力匹配法一样慢,并且它需要额外空间。
结语:
Rabin-Karp算法之所以出众最大的原因就是它可以对多模型进行匹配。
这一特性使得它在检测抄袭方面(尤其是大篇幅文字)非常好用。
package 字符串匹配;
public class 滚动hash {
public static int strStr(String haystack, String needle) {
int n = haystack.length(), L = needle.length();
if(L > n) return -1;
char[] src = haystack.toCharArray(), target = needle.toCharArray();
int a = 128, h0 = 0, L0 = 0, mod = 1 << 30;
for (int i = 0; i < L; i++) {
h0 = (h0 * a + src[i]) % mod;
L0 = (L0 * a + target[i]) % mod;
}
if(h0 == L0 && needle.equals(haystack.substring(0, L))) {
return 0;
}
int al = 1;
for (int i = 1; i < L; i++) {
al = al * a % mod;
}
for (int i = L; i < n; i++) {
h0 = ((h0 - src[i-L] * al) * a + src[i]) % mod;
if(h0 == L0 && needle.equals(haystack.substring(i - L + 1, i + 1))) {
return i - L + 1;
}
}
return -1;
}
public static void main(String[] args) {
System.out.println(strStr("3热3火irnr3陈849Irma,0493从da", "rma,"));
}
}