重复子串比较的核心是使用 Rabin-Karp (Rolling Hash)。
Rabin-Karp
字符串编码的本质是对字符串进行哈希,将字符串之间的比较转化为编码之间的比较
有N个不同的字符,可以将字符组成的串,映射成 N进制表示的10进制数。每一个数可以代表一种字符。 去头加尾的哈希计算可以在
的时间内完成
题目A(困难)
1044. 最长重复子串
给出一个字符串S
,考虑其所有 重复子串(S
的连续子串,出现两次或多次,可能会有重叠)。
返回 任何具有最长可能长度的重复子串。(如果S
不含重复子串,那么答案为""
。) 示例 1: 输入:"banana" 输出:"ana" 示例 2: 输入:"abcd" 输出:""
二分查找 + Rabin-Karp 字符串编码
- 从 1 到 N 中选取子串的长度 L
- 检查字符串中是否存在长度为 L 的重复子串
子任务一:二分查找
如果字符串中存在长度为 L 的重复子串,那么一定存在长度为 K < L 的重复子串(选取长度为 L 的重复子串的某个长度为 K 的子串即可),因此我们可以使用二分查找的方法,找到最大的 L。
子任务二:是否存在长度为L的重复子串(Rabin-Karp 字符串编)
使用 Rabin-Karp 算法将字符串进行编码,这样只要有两个编码相同,就说明存在重复子串。
对于选取的长度 L:使用长度为 L 的滑动窗口在长度为 N 的字符串上从左向右滑动;
字符一共有26,a最小可以取26.
检查当前处于滑动窗口中的子串的编码是否已经出现过(用一个集合存储已经出现过的编码)。若已经出现过,就说明找到了长度为 L 的重复子串;若没有出现过,就把当前子串的编码加入到集合中。
注意事项:取模
最后一个需要解决的问题是,在实际的编码计算中,hash值、
可能会非常大,在 C++ 和 Java 语言中,会导致整数的上溢出。所以,需要对编码值进行取模,将编码控制在一定的范围内,防止溢出,即h = h % modulus。
h = (h * a - nums[start - 1] * aL % modulus + modulus) % modulus;
h = (h + nums[start + L - 1]) % modulus;
注意事项2:哈希冲突
取模会导致哈希冲突,既是有可能两个字符串是不同的,但是哈希值相同。后续的话学习一下hash冲突的解决方式。
最终代码
public int search(int L, int n, int[] nums) {
// roll hash的base值,26个字符,用26进制玩。
int a = 26;
// 防止hash值溢出,使用的mod数
long modulus = (long) Math.pow(10, 16);
// 计算长度为L的第一个子串的 roll hash 值
long h = 0;
for (int i = 0; i < L; ++i)
h = (h * a + nums[i]) % modulus;
// 存储已出现过的hash值
HashSet<Long> seen = new HashSet<>();
seen.add(h);
// 第一位的26的L幂 取模 : aL % modulus
long aL = 1;
for (int i = 1; i <= L; ++i)
aL = (aL * a) % modulus;
for (int start = 1; start < n - L + 1; ++start) {
// compute rolling hash in O(1) time
h = (h * a - nums[start - 1] * aL % modulus + modulus) % modulus;
h = (h + nums[start + L - 1]) % modulus;
if (seen.contains(h)) return start;
seen.add(h);
}
return -1;
}
public String longestDupSubstring(String S) {
int n = S.length();
// 将字符串映射成 hash值 (26进制的数值)
// 实现常数时间复杂度的滑动窗口
int[] nums = new int[n];
for (int i = 0; i < n; ++i)
nums[i] = (int) S.charAt(i) - (int) 'a';
// 二分搜索, L = 要定位的重复字符串的长度
int minRepeat = 1, maxRepeat = n;
int L;
while (minRepeat != maxRepeat) {
L = minRepeat + (maxRepeat - minRepeat) / 2;
if (search(L, n, nums) != -1)
minRepeat = L + 1;
else
maxRepeat = L;
}
int start = search(minRepeat - 1, n, nums);
return start != -1 ? S.substring(start, start + minRepeat - 1) : "";
}