最長公共子串(LCS:Longest Common Substring)是一個非常經典的面試題目,本人在樂視二面中被面試官問過,慘敗在該題目中。
什么是最長公共子串
最長公共子串問題的基本表述為:給定兩個字符串,求出它們之間最長的相同子字符串的長度。
最直接的解法就是暴力解法:遍歷所有子字符串,比較它們是否相同,然后去的相同子串中最長的那個。對於長度為n的字符串,它子串的數量為n(n-1)/2,假如兩個字符串長度均為n,那么該解法的復雜度為O(n^4),想想並不是取出所有的子串,那么該解法的復雜度為O(n^3)。
復雜度太高,可以進行優化,可以利用動態規划法(有重疊的子問題)。
暴力解法
對於該問題,直接的思路就是要什么就找什么,要子串就要子串,要相同就比較每個字符,要長度就計算長度,所以很容易寫出下列代碼:
1 //暴力 2 public static int longestCommonSubstring(String s1, String s2){ 3 char[] str1 = s1.toCharArray(); 4 char[] str2 = s2.toCharArray(); 5 int str1_length = str1.length; 6 int str2_length = str2.length; 7 if(str1_length == 0 || str2_length == 0) 8 return 0; 9 //最大長度 10 int maxLength = 0; 11 int compareNum = 0; 12 int start1 = -1; 13 int start2 = -1; 14 for(int i=0;i<str1_length;i++){ 15 for(int j=0;j<str2_length;j++){ 16 int m = i; 17 int n = j; 18 //相同子串長度 19 int length = 0; 20 while(m < str1_length && n < str2_length){ 21 compareNum++; 22 if(str1[m] != str2[n]) 23 break; 24 m++; 25 n++; 26 length++; 27 } 28 if(length > maxLength){ 29 maxLength = length; 30 start1 = i + 1; 31 start2 = j + 1; 32 } 33 34 } 35 } 36 System.out.println("比較次數" + compareNum + ",s1起始位置:" + start1 + ",s2起始位置:" + start2); 37 return maxLength; 38 }
該思路以字符串中每個字符作為子串的開始,判斷以此開始的子串的相同字符所能達到的最大長度。從上述代碼來看,復雜度是O(n^2),但是在比較兩個相同開端的子串的效率不是O(1),是O(n),所以上述算法的復雜度為O(n^3)。
動態規划-空間換時間
上述解法回答面試官,面試官肯定會讓你優化!
我們發現,在相同開端的子串的比較中,有很多事重復動作。比如在比較以i,j分別為起點的子串時,有可能會進行i+1和j+1以及i+2和j+2位置的字符的比較。而以i+1,j+1分別為起點的子串時,這些字符又被比較了一次。也就說該問題有非常相似的子問題,而子問題之間又有重疊,這就給動態規划法創造了契機。
暴力解法是以子串開端開始尋找,現在換個思路,以相同子串的字符結尾來利用動態規划法。
假設兩個字符串分別為A、B,A[i]和B[j]分別表示其第i和j個字符,再假設K[i,j]表示以A[i]和B[j]結尾的子串的最大長度。那么A,B分別再向下走一個字符,我們可以推斷出K[i+1,j+1]與K[i,j]之間的關系,如果A[i] == B[j],那么K[i+1,j+1] = K[i,j] + 1;否則K[i+1,j+1] =0。而如果A[i+1]和B[j+1]相同,那么就只要在以A[i]和B[j]結尾的最長相同子串之后分別添上這兩個字符即可,這樣就可以讓長度增加一位,綜上所述,就是K[i+1,j+1] = (A[i] == B[j] ? K[i,j] + 1 : 0)的關系。
由上述K[i+1,j+1] = (A[i] == B[j] ? K[i,j] + 1 : 0)的關系,想到了使用二維數組來存儲兩個字符串之間的相同子串關系,因為K[i+1,j+1] = (A[i+1] == B[j+1] ? K[i,j] + 1 : 0)關系,只計算二維數據的最上列和最左列數值即可,其他數值通過K[i+1,j+1] = (A[i+1] == B[j+1] ? K[i,j] + 1 : 0)可得。如下圖所示:

代碼如下:
1 //優化 2 public static int longestCommonSubstring1(String s1, String s2){ 3 if(s1.length() == 0 || s2.length() == 0) 4 return 0; 5 char[] str1 = s1.toCharArray(); 6 char[] str2 = s2.toCharArray(); 7 int start1 = -1; 8 int start2 = -1; 9 int[][] results = new int[str2.length][str1.length]; 10 //最大長度 11 int maxLength = 0; 12 int compareNum = 0; 13 for(int i=0;i<str1.length;i++){ 14 results[0][i] = (str2[0] == str1[i] ? 1 : 0); 15 compareNum++; 16 for(int j=1;j<str2.length;j++){ 17 results[j][0] = (str1[0] == str2[j] ? 1 : 0); 18 if(i>0 && j>0){ 19 if(str1[i] == str2[j]){ 20 results[j][i] = results[j-1][i-1] + 1; 21 compareNum++; 22 } 23 } 24 if(maxLength < results[j][i]){ 25 maxLength = results[j][i]; 26 start1 = i - maxLength + 2; 27 start2 = j - maxLength + 2; 28 } 29 } 30 } 31 System.out.println("比較次數" + (compareNum+str2.length) + ",s1起始位置:" + start1 + ",s2起始位置:" + start2); 32 return maxLength; 33 }
用二維數組保存計算結果,避免了重復計算,運算的時間復雜度降低到了O(n^2)。
