参考网站:https://leetcode-cn.com/problems/longest-duplicate-substring/
1. 题目描述
给出一个字符串 S,考虑其所有重复子串(S 的连续子串,出现两次或多次,可能会有重叠)。
返回任何具有最长可能长度的重复子串。(如果 S 不含重复子串,那么答案为 “”。)
示例 1:
输入:“banana”
输出:“ana”
示例 2:
输入:“abcd”
输出:""
提示:
2 <= S.length <= 10^5
S 由小写英文字母组成。
2. 关键词
二分查找,Rabin-Karp 字符串编码
3. 思路
- 我们最能想到的方法是:找出所有可能的字符串情况,由于是找最长子串,那么可以从长往短找,然后进行使用滑动窗口与所有可能的字符串逐个对比。但是这种时间复杂度很高为O(n^3),
- 优化思路:如何尽可能快地找到最长字符串(二分查找);如何快速匹配相同字符串(Rabin-Karp 字符串编码)。
- 原理:
- 假设符合条件的最长重复字符串的长度为L,那么一定存在其子串长度为L0(L0<L)也符合条件,那么也一定不存在长度为L2(L2>L)的字符串符合条件( 不然与已知条件冲突)
- 我们可以使用 Rabin-Karp 算法将整个字符串进行编码,这样只要有两个编码相同,就说明存在重复子串。而无需遍历两个字符串挨个字符对比。
- 具体操作:见代码
4. 代码(Java)
class Solution {
/*
使用滑动窗口Rabin-Karp。搜索至少发生2次的给定长度的子字符串。
如果子字符串退出,则返回开始位置,否则返回-1。
*/
public int search(int L, int a, long modulus, int n, int[] nums) {
// 计算字符串S[:L]的散列
long h = 0;
for (int i = 0; i < L; ++i) h = (h * a + nums[i]) % modulus;
// 已经看到长度为L的字符串的散列
HashSet<Long> seen = new HashSet();
seen.add(h);
// 经常使用的值:a**L%模数
long aL = 1;
for (int i = 1; i <= L; ++i) aL = (aL * a) % modulus;
for (int start = 1; start < n - L + 1; ++start) {
// 在O(1)时间内计算滚动散列
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();
// 将字符串转换为整数数组
// 实现恒定时间片
int[] nums = new int[n];
for (int i = 0; i < n; ++i) nums[i] = (int)S.charAt(i) - (int)'a';
// 滚动散列函数的基值
int a = 26;
// modulus value for the rolling hash function to avoid overflow
long modulus = (long)Math.pow(2, 32);
// 二进制搜索,L=重复字符串长度
int left = 1, right = n;
int L;
while (left != right) {
L = left + (right - left) / 2;
if (search(L, a, modulus, n, nums) != -1) left = L + 1;
else right = L;
}
int start = search(left - 1, a, modulus, n, nums);
return start != -1 ? S.substring(start, start + left - 1) : "";
}
}
5. 时间及空间复杂度
时间复杂度:时间复杂度:O(NlogN),二分查找的时间复杂度为O(logN),Rabin-Karp 字符串编码的时间复杂度为O(N)。
空间复杂度:O(N),用来存储字符串编码的集合。
扫描二维码关注公众号,回复:
10247096 查看本文章