這個馬拉車算法Manacher‘s Algorithm是用來查找一個字符串的最長回文子串的線性方法,由一個叫Manacher的人在1975年發明的,這個方法的最大貢獻是在于將時間復雜度提升到了線性,這是非常了不起的。對于回文串想必大家都不陌生,就是正讀反讀都一樣的字符串,比如 "bob", "level", "noon" 等等,那么如何在一個字符串中找出最長回文子串呢,可以以每一個字符為中心,向兩邊尋找回文子串,在遍歷完整個數組后,就可以找到最長的回文子串。但是這個方法的時間復雜度為O(n*n),并不是很高效,下面我們來看時間復雜度為O(n)的馬拉車算法。
由于回文串的長度可奇可偶,比如"bob"是奇數形式的回文,"noon"就是偶數形式的回文,馬拉車算法的第一步是預處理,做法是在每一個字符的左右都加上一個特殊字符,比如加上'#',那么
bob --> #b#o#b#
noon --> #n#o#o#n#
這樣做的好處是不論原字符串是奇數還是偶數個,處理之后得到的字符串的個數都是奇數個,這樣就不用分情況討論了,而可以一起搞定。接下來我們還需要和處理后的字符串t等長的數組p,其中p[i]表示以t[i]字符為中心的回文子串的半徑,若p[i] = 1,則該回文子串就是t[i]本身,那么我們來看一個簡單的例子:
# 1 # 2 # 2 # 1 # 2 # 2 #
1 2 1 2 5 2 1 6 1 2 3 2 1
由于第一個和最后一個字符都是#號,且也需要搜索回文,為了防止越界,我們還需要在首尾再加上非#號字符,實際操作時我們還需給開頭加上個非#號字符'$',結尾加一個'@'。通過p數組我們就可以找到其最大值和其位置,就能確定最長回文子串了,那么下面我們就來看如何求p數組,需要新增兩個輔助變量mx和id,其中id為最大回文子串中心的位置,mx是回文串能延伸到的最右端的位置(很多文章都這樣說,其實并不是,根據下面的代碼就能看出來,mx應該是已找到的回文子串所能到達的最右端,而id是最右端回文子串對應的中心),這個算法的最核心的一行如下:
p[i] = mx > i ? min(p[2 * id - i], mx - i) : 1;
可以這么說,這行要是理解了,那么馬拉車算法基本上就沒啥問題了,那么這一行代碼拆開來看就是
如果mx > i, 則 p[i] = min(p[2 * id - i], mx - i)
否則, p[i] = 1
當 mx - i > P[j] 的時候,以S[j]為中心的回文子串包含在以S[id]為中心的回文子串中,由于 i 和 j 對稱,以S[i]為中心的回文子串必然包含在以S[id]為中心的回文子串中,所以必有 P[i] = P[j]。
對于 mx <= i 的情況,無法對 P[i]做更多的假設,只能P[i] = 1,然后再去匹配了。
參見如下實現代碼:
public static void main(String[] args) {
String str = "abcde";
Scanner s = new Scanner(System.in);
while (true) {
System.out.print("請輸入字符串:");
String line = s.nextLine();
if (line.equals("exit")) break;
else {
System.out.println("最大回文子串: "+Manacher(line));
}
}
}
public static String Manacher(String s) {
// Insert '#'
String t = "$#";
for (int i = 0; i < s.length(); ++i) {
t += s.charAt(i);
t += "#";
}
t += "@";
// Process t
int[] p = new int[t.length()];;
int mx = 0, id = 0, resLen = 0, resCenter = 0;
for (int i = 1; i < t.length()-1; ++i) {
p[i] = mx > i ? Math.min(p[2 * id - i], mx - i) : 1;
while (((i - p[i])>=0) && ((i + p[i])<t.length()-1) && (t.charAt(i + p[i]) == t.charAt(i - p[i])))
++p[i];
if (mx < i + p[i]) {
mx = i + p[i];
id = i;
}
if (resLen < p[i]) {
resLen = p[i];
resCenter = i;
}
}
return s.substring((resCenter - resLen) / 2, (resCenter - resLen) / 2 + resLen-1);
}