https://leetcode-cn.com/problems/longest-duplicate-substring/
题目:
给你一个字符串 s ,考虑其所有 重复子串 :即,s 的连续子串,在 s 中出现 2 次或更多次。这些出现之间可能存在重叠。返回 任意一个 可能具有最长长度的重复子串。如果 s 不含重复子串,那么答案为 “” 。
示例 1:
输入:s = "banana"
输出:"ana"
示例 2:
输入:s = "abcd"
输出:""
理解
力扣上面写的例子太简单,一开始没读懂题,先从理解开始,字符串从头开始遍历,然后返回后面重复的字符串,比如“cabcbac”返回c,c是第一位,后面无规律,所以后面只有c是最小子串,“cabcabac”返回cab,从头开始遍历cab,后面有重复,所以cab是最小子串,再给字符串最前面加个a,“acabcabac”还是返回cab而不是ac,因为离cab最近的还是cab,再给前面的基础上再加个c,“acabacabac”返回acabac
重复子串如图所示:
解题
读懂题目后,就再看一下官方的解答
数学基础差,难度很高,一上来肯定会看不懂
这里介绍了一个概念Rabin-Karp 字符串编码,所以要先百度下RK算法
,理解个大概,其他解法如DC3算法
等先打个眼熟,之后在优化的时候可以进阶学习一下
RK 算法
一般字符串匹配的时候,如果使用暴力匹配算法
我们假设主串长度为n,模式串长度m,暴力匹配的时间复杂度为O(n*m)。当主串特别长的时候,匹配效率不够高。
哈希算法的应用
我们知道,哈希算法是可以将任意数据转换成一组固定长度的二进制串,所以,我们可以将主串中所有与模式串长度相同的子串的哈希值计算出来,匹配时,首先比较模式串与子串的哈希值。
考虑存在哈希冲突的情况,在匹配到相同哈希值的子串后,还需要进行字符比较,我们将大量的匹配交给了哈希值比较,而计算哈希值和哈希值的比较都是很高效的,因此,效率大大高于暴力匹配法。
哈希算法的设计
假设我们要匹配的主串只包含[a-z]的小写字母,可以类比十进制数的表示方法,将这些小写字母转换成 26 进制。
在计算哈希值时,可以通过前一个子串简单计算得到当前子串的哈希值:
公式:
h[i] = 26 * (h[i-1] - 26^(m-1) * (s[i-1]-'a')) + (s[i+m-1]-'a');
其中, h[i]、h[i-1] 分别对应 s[i] 和 s[i-1] 两个子串的哈希值
注:这里的公式看起来复杂,但是是算法代码里很关键的实现,记不住没关系但一定要眼熟
时间复杂度分析
假设主串长度为n
,模式串长度为m
。
RK 算法匹配时,分为两步:
- 计算
n-m+1
个子串的哈希值和模式串的哈希值 - 遍历比较子串哈希值和模式串哈希值
第一步中,时间复杂度为 O(n)
。
第二步中,哈希值之间的比较时间复杂度为 O(1)
,因此比较 n-m+1
次,时间复杂度为 O(n)
。
综上:RK 算法整体时间复杂度为O(n)
。
光看没用,还要画一下,就会很好理解了
注意要点:
- 当模式串长,计算得到的哈希值超过整数存储范围时,如何处理?
前文中,我们设计的哈希算法是不存在哈希冲突的,因此,可以允许存在哈希冲突,从而减小哈希值的范围。
例如,可以将字符串对应值直接相加:bcd ==> 1+2+3=6,这必定会导致严重散列冲突。可以采用其他方法,以达到效率和冲突的平衡。 - 存在散列冲突时,如何处理?
当匹配到哈希值相同的子串时,由于存在散列冲突,因此不能保证该子串与模式串完全一样,我们需要进一步比较子串与模式串的字符,从而确定是否匹配成功。
代码
之后在本地起一下代码,然后打断点一步步理解
/**
* @author: lzq
* @description: Algr
* @date: 2021/12/23 4:37 下午
*/
public class Algr {
public static void main(String[] args) {
String a = longestDupSubstring("cabcbac");
System.out.println(a);
}
public static String longestDupSubstring(String s) {
Random random = new Random();
// 生成两个进制
int a1 = random.nextInt(75) + 26;
int a2 = random.nextInt(75) + 26;
// 生成两个模
int mod1 = random.nextInt(Integer.MAX_VALUE - 1000000007 + 1) + 1000000007;
int mod2 = random.nextInt(Integer.MAX_VALUE - 1000000007 + 1) + 1000000007;
int n = s.length();
// 先对所有字符进行编码
int[] arr = new int[n];
for (int i = 0; i < n; ++i) {
arr[i] = s.charAt(i) - 'a';
}
// 二分查找的范围是[1, n-1]
int l = 1, r = n - 1;
int length = 0, start = -1;
while (l <= r) {
int m = l + (r - l + 1) / 2;
int idx = check(arr, m, a1, a2, mod1, mod2);
if (idx != -1) {
// 有重复子串,移动左边界
l = m + 1;
length = m;
start = idx;
} else {
// 无重复子串,移动右边界
r = m - 1;
}
}
return start != -1 ? s.substring(start, start + length) : "";
}
public static int check(int[] arr, int m, int a1, int a2, int mod1, int mod2) {
int n = arr.length;
long aL1 = pow(a1, m, mod1);
long aL2 = pow(a2, m, mod2);
long h1 = 0, h2 = 0;
for (int i = 0; i < m; ++i) {
h1 = (h1 * a1 % mod1 + arr[i]) % mod1;
h2 = (h2 * a2 % mod2 + arr[i]) % mod2;
if (h1 < 0) {
h1 += mod1;
}
if (h2 < 0) {
h2 += mod2;
}
}
// 存储一个编码组合是否出现过
Set<Long> seen = new HashSet<Long>();
seen.add(h1 * mod2 + h2);
for (int start = 1; start <= n - m; ++start) {
h1 = (h1 * a1 % mod1 - arr[start - 1] * aL1 % mod1 + arr[start + m - 1]) % mod1;
h2 = (h2 * a2 % mod2 - arr[start - 1] * aL2 % mod2 + arr[start + m - 1]) % mod2;
if (h1 < 0) {
h1 += mod1;
}
if (h2 < 0) {
h2 += mod2;
}
long num = h1 * mod2 + h2;
// 如果重复,则返回重复串的起点
if (!seen.add(num)) {
return start;
}
}
// 没有重复,则返回-1
return -1;
}
public static long pow(int a, int m, int mod) {
long ans = 1;
long contribute = a;
while (m > 0) {
if (m % 2 == 1) {
ans = ans * contribute % mod;
if (ans < 0) {
ans += mod;
}
}
contribute = contribute * contribute % mod;
if (contribute < 0) {
contribute += mod;
}
m /= 2;
}
return ans;
}
}
按照顺序,先生成必要的几个参数
然后开始编码,遍历-’a‘生成arr的编码数组
这里进入二分查找循环,left和right取中间m,l和r的范围是[1, n-1]
idx经过check方法处理,返回根据是否是-1判断重复子串,有重复子串,移动左边界,无重复子串,移动右边界,也就是例子里是[1,6],有重复下次遍历[5,6],无重复下次遍历[1,3]
遍历完成后,截取的字符串则是[不为-1的idx,idx+中位m],例子里的是[3,4],也就是cabcbac中的第二个c,结果返回c
主流程就没问题了,下面看下关键的check方法,他的目的是为了返回idx,没有重复,则返回-1,如果重复,则返回重复串的起点
在看之前还要再进入pow方法,拿到aL1和aL2
contribute小于0就加模
最后遍历到1之后算出ans,返回
返回check接着往下走,算出h1和h2
遍历到中间数,累加出哈希数
声明一个set,存放哈希数
再次遍历,根据公式计算
模式串匹配上了就返回start,否则返回-1
最后整个流程都打通了,公式还要具体看,还是比较复杂的