如何找到字串中的最長迴文子串?
阿新 • • 發佈:2018-12-13
小史: 只要先對比第一個字元和倒數第一個字元,再對比第二個字元和倒數第二個字元,以此類推。如果都相等,那就是迴文串了。
- 題目:給你一個字串,找出裡面最長的迴文子串。 例如,輸入abcdcef,那麼輸出應該是cdc; 輸入adaelele,輸出應該是elele。
小史: 可以遍歷整個字串,把每個字元和字元間的空隙當作迴文的中心,然後向兩邊擴充套件來找到最長迴文串。小史這次搶著分析時間和空間複雜度。
解題思路
小史回到學校,把面試情況和呂老師說了一下。
呂老師: 比如cabadabae用中心擴充套件的演算法,我已經知道了第三位為中心的aba和第5位為中心的abadaba是迴文,那麼在判斷第7位為中心的迴文串的時候,有什麼已知資訊嗎?
小史: 已知第5位為中心的abadaba是迴文,由迴文的特性,就能夠知道2-4位和6-8位對稱,而又知道第3位為中心的aba是迴文,所以2-4位是迴文。這樣的話,6-8位肯定是迴文。
小史拿著筆在紙上畫了半天,突然大叫一聲。
小史: 由於之前的計算,已經知道了第5位為中心的abadaba是迴文,而第4位為中心的a的迴文長度是1,所以第6位為中心的迴文長度只能是1,不用再去擴充套件判斷了。
小史: 以第7位為中心的迴文串的計算,由之前分析已經知道最小長度是3了,但是還是需要進行擴充套件,因為第9位是什麼,根據之前的資訊無法得知,需要擴充套件進行探索。
小史: 而以第6位為中心的迴文串的計算,並不需要進行探索了,因為根據之前第5位為迴文中心串的資訊、和第4位為迴文中心串的資訊,已經可以推斷,第6位為迴文中心串的長度只能為1。
小史: 當然可以。
- 1、首先,我們要記錄下目前已知的迴文串能夠覆蓋到的最右邊的地方,就像案例中的第8位
- 2、同時,覆蓋到最右邊的迴文串所對應的迴文中心也要記錄,就像案例中的第5位
- 3、以每一位為中心的迴文串的長度也要記錄,後面進行推斷的時候能用到,就像案例中用到的以第3位為中心的迴文和第4位為中心的迴文
- 4、對於新的中心,我們判斷它是否在右邊界內,若在,就計算它相對右邊界迴文中心的對稱位置,從而得到一些資訊,同時,如果該中心需要進行擴充套件,則繼續擴充套件就行。
程式設計實現
小史: 迴文的中心有可能是兩個字元中間,這種情況沒有考慮到啊。
小史:
- 1、先對字串進行預處理,兩個字元之間加上特殊符號#;
- 2、然後遍歷整個字串,用一個數組來記錄以該字元為中心的迴文長度,為了方便計算右邊界,我在陣列中記錄長度的一半(向下取整);
- 3、每一次遍歷的時候,如果該字元在已知迴文串最右邊界的覆蓋下,那麼就計算其相對最右邊界迴文串中心對稱的位置,得出已知迴文串的長度;
- 4、判斷該長度和右邊界,如果達到了右邊界,那麼需要進行中心擴充套件探索。當然,如果第3步該字元沒有在最右邊界的“羽翼”下,則直接進行中心擴充套件探索。進行中心擴充套件探索的時候,同時又更新右邊界;
- 5、最後得到最長迴文之後,去掉其中的特殊符號即可。
理解了演算法之後,小史的程式碼寫起來也是非常快,不一會兒就寫好了: PlalindromeString.java:
/**
* @author xiaoshi on 2018/9/24.
* Happy Mid-Autumn Festival
*/
public class PlalindromeString {
// 判斷一個字串是否迴文,演算法中用不到了
@Deprecated
private boolean isPlalindrome(String s) {
int len = s.length();
for(int i = 0; i < len / 2; i++) {
if(s.charAt(i) != s.charAt(len - 1 - i)) {
return false;
}
}
return true;
}
// 預處理字串,在兩個字元之間加上#
private String preHandleString(String s) {
StringBuffer sb = new StringBuffer();
int len = s.length();
sb.append('#');
for(int i = 0; i < len; i++) {
sb.append(s.charAt(i));
sb.append('#');
}
return sb.toString();
}
// 尋找最長迴文字串
public String findLongestPlalindromeString(String s) {
// 先預處理字串
String str = preHandleString(s);
// 處理後的字串長度
int len = str.length();
// 右邊界
int rightSide = 0;
// 右邊界對應的迴文串中心
int rightSideCenter = 0;
// 儲存以每個字元為中心的迴文長度一半(向下取整)
int[] halfLenArr = new int[len];
// 記錄迴文中心
int center = 0;
// 記錄最長迴文長度
int longestHalf = 0;
for(int i = 0; i < len; i++) {
// 是否需要中心擴充套件
boolean needCalc = true;
// 如果在右邊界的覆蓋之內
if(rightSide > i) {
// 計算相對rightSideCenter的對稱位置
int leftCenter = 2 * rightSideCenter - i;
// 根據迴文性質得到的結論
halfLenArr[i] = halfLenArr[leftCenter];
// 如果超過了右邊界,進行調整
if(i + halfLenArr[i] > rightSide) {
halfLenArr[i] = rightSide - i;
}
// 如果根據已知條件計算得出的最長迴文小於右邊界,則不需要擴充套件了
if(i + halfLenArr[leftCenter] < rightSide) {
// 直接推出結論
needCalc = false;
}
}
// 中心擴充套件
if(needCalc) {
while(i - 1 - halfLenArr[i] >= 0 && i + 1 + halfLenArr[i] < len) {
if(str.charAt(i + 1 + halfLenArr[i]) == str.charAt(i - 1 - halfLenArr[i])) {
halfLenArr[i]++;
} else {
break;
}
}
// 更新右邊界及中心
rightSide = i + halfLenArr[i];
rightSideCenter = i;
// 記錄最長迴文串
if(halfLenArr[i] > longestHalf) {
center = i;
longestHalf = halfLenArr[i];
}
}
}
// 去掉之前新增的#
StringBuffer sb = new StringBuffer();
for(int i = center - longestHalf + 1; i <= center + longestHalf; i += 2) {
sb.append(str.charAt(i));
}
return sb.toString();
}
}
Main.java:
/**
* @author lixin on 2018/9/24.
*/
public class Main {
public static void main(String[] args) {
PlalindromeString ps = new PlalindromeString();
String[] testStrArr = new String[] {
"abcdcef",
"adaelele",
"cabadabae",
"aaaabcdefgfedcbaa",
"aaba",
"aaaaaaaaa"
};
for(String str : testStrArr) {
System.out.println(String.format("原字串 : %s", str));
System.out.println(String.format("最長迴文串 : %s", ps.findLongestPlalindromeString(str)));
System.out.println();
}
}
}
執行結果:
原字串 : abcdcef
最長迴文串 : cdc
原字串 : adaelele
最長迴文串 : elele
原字串 : cabadabae
最長迴文串 : abadaba
原字串 : aaaabcdefgfedcbaa
最長迴文串 : aabcdefgfedcbaa
原字串 : aaba
最長迴文串 : aba
原字串 : aaaaaaaaa
最長迴文串 : aaaaaaaaa