题目描述
给定两个字符串str1和str2,输出两个字符串的最长公共子串,如果最长公共子串为空,输出-1。
输入 "1AB2345CD","12345EF"
返回值 "2345"
参考
链接:https://www.nowcoder.com/questionTerminal/f33f5adc55f444baa0e0ca87ad8a6aac?answerType=1&f=discussion
来源:牛客网
1.滑动窗口
- 从
str1
中按照窗口截取窗口子串,检查str2
中是否包含,如果包含就扩展窗口 - 如果
str2
中不包含窗口子串,我们就移动窗口起始位置 sb
是用来记录最大的窗口子串的,窗口最小为0个单位
这个算法精妙的地方在于窗口大小是不会缩小的,我们只需要找最大公共子串,所以一旦有某个窗口子串命中了,往后的窗口子串就不能小于上次命中的长度,所以我们在算法里可以看到当start++
时必然伴随着end++,保证窗口不会变小
public class Solution {
// write code here
/**
* 滑动窗口算法
*
* @param str1 string字符串 the string
* @param str2 string字符串 the string
* @return string字符串
*/
public static String LCS1(String str1, String str2) {
// write code here
StringBuilder sb = new StringBuilder();
int start = 0, end = 1;
while (end < str1.length() + 1) {
// 判断截取窗口,如果包含窗口扩大end++
// 如果不包含,start++ 缩小窗口
if (str2.contains(str1.substring(start, end))) {
if (sb.length() < end - start) {
sb.delete(0, sb.length());
sb.append(str1, start, end);
}
} else {
//当start==end时,substring获取的是"",此时contains必然为true
//所以当start == end时,必然会走end++分支
//不会出现end>start的情况crash程序;
start++;
}
end++;
}
if (sb.length() == 0) {
return "-1";
}
return sb.toString();
}
}
2.动态规划
public static String LCS2(String str1, String str2) {
if (str1 == null || str2 == null) return "-1";
int n1 = str1.length(), n2 = str2.length();
if (n1 == 0 || n2 == 0) return "-1";
int[][] dp = new int[n1 + 1][n2 + 1];
int maxLen = 0, x = 0;
for (int i = 1; i <= n1; i++) {
char ch1 = str1.charAt(i - 1);
for (int j = 1; j <= n2; j++) {
char ch2 = str2.charAt(j - 1);
if (ch1 == ch2) {
dp[i][j] = dp[i - 1][j - 1] + 1;
if (dp[i][j] > maxLen) {
maxLen = dp[i][j];
x = i;
}
}
}
}
return maxLen == 0 ? "-1" : str1.substring(x - maxLen, x);
}
这里dp
的索引是从1开始,0位是防止数组越界,并且也有实际意义,可以理解为空子串,结果自然也是0
相比之下优点
- 最大公共子串的出现,必然发生在有字符相等的时候。这个应该很好理解,所以算法里只关心出现这个情况的
dp
值,但是同时需要注意,此时dp
里记录的值不再是连续的值了,不能再认为dp[i][j]
描述的是str1
中从0~i
与str2
中从0~j
的最大公共子串,此时的dp
记录值有点类似滑动窗口了,某一段时间内是持续增长的,所以他用maxLen
来记录出现的最大值 - 几个临时变量存储。首先是
maxLen
和x
,这两个变量描述了一个子串,当出现更大的字串时更新这两个值;还有ch1
和ch2
,也有一定性能优化