子字符串的定义和子序列的定义类似,但要求是连续分布在其他字符串中。比如输入两个字符串BDCABA和ABCBDAB的最长公共字符串有BD和AB,它们的长度都是2。
最直接的解法自然是找出两个字符串的所有子字符串进行比较看他们是否相同,然后取得相同最长的那个。对于一个长度为n的字符串,它有n(n+1)/2 个非空子串。所以假如两个字符串的长度同为n,通过比较各个子串其算法复杂度大致为O(n4)。这还没有考虑字符串比较所需的时间。简单想想其实并不需要取出所有的子串,而只要考虑每个子串的开始位置就可以,这样可以把复杂度减到O(n3)。
但这个问题最好的解决办法是动态规划法,在后边会更加详细介绍这个问题使用动态规划法的契机:有重叠的子问题。进而可以通过空间换时间,让复杂度优化到O(n2),代价是空间复杂度从O(1)一下子提到了O(n2)。
从时间复杂度的角度讲,对于最长公共子串问题,O(n2)已经是目前我所知最优的了,也是面试时所期望达到的。但是对于空间复杂度O(n2)并不算什么,毕竟算法上时间比空间更重要。
1.暴力求解
直观的思路就是问题要求什么就找出什么。要子串,就找子串;要相同,就比较每个字符;要最长就记录最长。
public class LongestSubString {
/**
* 最长子串
* 方法一:暴力求解
*
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.print(longestCommonSubstring_n3("abcdefg","bcabcfde"));
}
public static int longestCommonSubstring_n3(String str1, String str2)
{ char ch1[]=str1.toCharArray();
char ch2[]=str2.toCharArray();
int size1 = ch1.length;
int size2 = ch2.length;
if (size1 == 0 || size2 == 0) return 0;
int start1 = -1;
int start2 = -1;
int longest = 0; //记录最长值
for (int i = 0; i < size1; ++i)
{
for (int j = 0; j < size2; ++j)
{
int length = 0;
int m = i;
int n = j;
while(m < size1 && n < size2)
{
if (ch1[m] != ch2[n]) break;
++length;
++m;
++n;
}
if (longest < length)
{
longest = length;
start1 = i;
start2 = j;
}
}
}
return longest;
}
}
2. 动态规划法
假设两个字符串分别为s和t,s[i]和t[j]分别表示其第i和第j个字符(字符顺序从0开始),再令L[i, j]表示以s[i]和s[j]为结尾的相同子串的最大长度。应该不难递推出L[i, j]和L[i+1,j+1]之间的关系,因为两者其实只差s[i+1]和t[j+1]这一对字符。若s[i+1]和t[j+1]不同,那么L[i+1, j+1]自然应该是0,因为任何以它们为结尾的子串都不可能完全相同;而如果s[i+1]和t[j+1]相同,那么就只要在以s[i]和t[j]结尾的最长相同子串之后分别添上这两个字符即可,这样就可以让长度增加一位。合并上述两种情况,也就得到L[i+1,j+1]=(s[i]==t[j]?L[i,j]+1:0)这样的关系。
最后就是要小心的就是临界位置:如若两个字符串中任何一个是空串,那么最长公共子串的长度只能是0;当i为0时,L[0,j]应该是等于L[-1,j-1]再加上s[0]和t[j]提供的值,但L[-1,j-1]本是无效,但可以视s[-1]是空字符也就变成了前面一种临界情况,这样就可知L[-1,j-1]==0,所以L[0,j]=(s[0]==t[j]?1:0)。对于j为0也是一样的,同样可得L[i,0]=(s[i]==t[0]?1:0)。
public class LongestSubString {
/**
* 最长子串
* 方法一:动态规划求解
*
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println(longestCommonSubstring_n2_n2("abcdefg","bcabfde"));
}
//方法二:动态规划
public static int longestCommonSubstring_n2_n2(String str1, String str2)
{
char ch1[]=str1.toCharArray();
char ch2[]=str2.toCharArray();
int size1 = ch1.length;
int size2 = ch2.length;
if (size1 == 0 || size2 == 0) return 0;
int table[][]=new int[size1][size2];
int start1 = -1;
int start2 = -1;
int longest = 0;
for (int j = 0; j < size2; ++j)
{
table[0][j] = (ch1[0] == ch2[j] ? 1 :0);
}
for (int i = 1; i < size1; ++i)
{
table[i][0] = (ch1[i] == ch2[0] ? 1 :0);
for (int j = 1; j < size2; ++j)
{
if (ch1[i] ==ch2[j])
{
table[i][j] = table[i-1][j-1]+1;
}
}
}
for (int i = 0; i < size1; ++i)
{
for (int j = 0; j < size2; ++j)
{
if (longest < table[i][j])
{
longest = table[i][j];
start1 = i-longest+1;
start2 = j-longest+1;
}
}
}
return longest;
}
}