题目来源:LeetCode 12.23 每日一题 1044 最长重复子串 困难
原理概述——滚动哈希(Rabin-Karp)
参考博客:字符串哈希:从零开始的十分钟包会教程
参考书籍:挑战程序设计竞赛 P373 字符串匹配
应用场景
在字符串子串统计有关的问题中,统计子串个数的情况下,需要用map来统计字符串个数时,可选用字符串哈希算法,key由string可转化为整型
基本思想
把一个字符串转化成随机分布的哈希值,不同字符串哈希值相等的概率很低,我们当作这种情况不会发生,避免了O(m)的复杂度直接比较字符串间是否匹配,此时整体复杂度为O(nm)。使用字符串哈希后复杂度可以达到O(n)。
实现方式
核心:取素数b,以b为“进制”处理子串,生成唯一随机值
例:字符串 “ACGACG” 统计m位子串个数
ACG即为c1c2c3
- 定义哈希函数为:H(S) = (c1bm-1+c2bm-2+…+cmb0)
把c1c2c3看成b进制数,左高位右低位进行于运算,得到该长度为m的字符串的随机哈希值 - 哈希值的递推
前段哈希值*进制-左前一位×进制m+右前一位
注意:此处省略了哈希函数对求模运算(求模是为了让哈希值在一定范围内随机,不会越界),通过自然溢出省去了求模运算
前缀哈希法
概述:
前缀和思想运用到字符串哈希中
前缀和思想:
前缀和:即保存相同长度的数组,每个位置上对应原数组前缀元素之和
数列:【1,2,3,4,5,6,7,8, 9】
1的前缀和:1 = 1
2的前缀和:1+2 = 3
3的前缀和:1+2+3 = 6
4的前缀和:1+2+3+4 = 10
5的前缀和:1+2+3+4+5 = 15
6的前缀和:1+2+3+4+5+6 = 21
7的前缀和:1+2+3+4+5+6+7 = 28
8的前缀和:1+2+3+4+5+6+7+8 = 36
9的前缀和:1+2+3+4+5+6+7+8+9 = 45
前缀和思想的核心在于右边一项减去左边一项就可以得到其中一项的值
前缀哈希
- 这个字符串看成p进制的数,那么这个值就是Y = (c1 * p3 +c2 * p2 + c3 * p1 + c4 * p0 )
- 其中一段的哈希值 = 最右端的前缀和 - 最左端前一位的前缀和×进度^字符串长度
- h[l,r] = h[r] - h[l - 1] * P^(r - l + 1)
- 滚动哈希是持续生成哈希数,进行减左加右更新;而前缀哈希是生成前缀和后,利用公式单次生成哈希值,更方便使用
前缀哈希的简单运用
进制数p一般取p = 131或13331,素数
class Solution {
int N = (int)1e5+10, P = 131313;
int[] h = new int[N], p = new int[N];
public List<String> findRepeatedDnaSequences(String s) {
int n = s.length();
List<String> ans = new ArrayList<>();
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;
int hash = h[j] - h[i - 1] * p[j - i + 1];
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;
}
}
解释:因为这里的前缀和是不包括自己的,所以一段字符串的哈希值应该是右边一个元素前缀和减去左边一个元素的前缀和,因此循环中i是从1开始的,而求的是从i-1开始的字符串。