問題一:(最長迴文子串)給定一個字符串 s,找到 s 中最長的迴文子串。
第一眼的想法是暴力法,由於其時間複雜度高達O(n^3),當s過長時效率會特別低。
方法一:中心擴展算法
其思想就是遍歷一遍字符串,其中在每一個點都進行以其爲中心而均勻展開(分奇偶),然後找到每個點能夠展開到的最大值,最後也就不難得到最長迴文串了。
具體代碼如下:
public String longestPalindrome1(String s) {
if (s == null || s.length() < 1) return "";
int start = 0, end = 0;
for (int i = 0; i < s.length(); i++) {
//分兩種情況解決了奇偶的問題
int len1 = expandAroundCenter(s, i, i);
int len2 = expandAroundCenter(s, i, i + 1);
int len = Math.max(len1, len2);
//通過start和end來保存最大len的兩端
if (len > end - start) {
start = i - (len - 1) / 2;
end = i + len / 2;
}
}
return s.substring(start, end + 1);
}
private int expandAroundCenter(String s, int left, int right) {
int L = left, R = right;
while (L >= 0 && R < s.length() && s.charAt(L) == s.charAt(R)) {
L--;
R++;
}
return R - L - 1;
}
方法二:Manacher 算法
其思想就是將原字符串用同一的符號擴展兩倍+1,這樣可以巧妙的迴避了奇偶迴文串的問題。然後再定義一個類似計數排序的一個計數數組,用它在擴展後的字符串上進行類型中心擴展的操作。最終的Manacher 算法還有一個對計數數組的一個算法優化。
具體代碼如下:
public String longestPalindrome2(String s) {
char[] ch = s.toCharArray();
char[] ah = new char[2 * s.length() + 1];//迴避了要討論奇偶兩種情形的弊端
for (int i = 0; i < 2 * s.length(); i = i + 2) {
ah[i] = '#';
ah[i + 1] = ch[i / 2];
}
ah[2 * s.length()] = '#';
int[] arr = Manacher1(ah);
return s.substring((arr[1] - arr[0]) / 2, (arr[1] + arr[0]) / 2);
}
private int[] Manacher1(char[] t) {
int[] arr = new int[2];
int[] len = new int[t.length];//既用於計數還能參與遍歷
int ans = 0;
for (int i = 0; i < len.length; i++) {
len[i] = 1;
}
for (int i = 0; i < len.length; i++) {
while ((i - len[i] >= 0) && (i + len[i] < len.length) && t[i - len[i]] == t[i + len[i]]) {
len[i]++;
}
ans = Math.max(ans, len[i]);
}
arr[0] = ans - 1;
int i = 0;
while (len[i] != ans) {
i++;
}
arr[1] = i;
return arr;
}
private int[] Manacher2(char[] t) {
int[] arr = new int[2];
int[] len = new int[t.length];
int ans = 0;
//另外兩個指針起到優化算法的作用
int mx = 0;
int po = 0;
for (int i = 0; i < len.length; i++) {
if (mx > i) {
len[i] = Math.min(mx - i, len[2 * po - i]);//如果前一個位置所形成的的迴文串長度很大,那麼下一個位置必定繼承其部分長度
} else {
len[i] = 1;
}
while ((i - len[i] >= 0) && (i + len[i] < len.length) && t[i - len[i]] == t[i + len[i]]) {
len[i]++;
}
if (len[i] + i > mx) {
mx = len[i] + i;
po = i;
}
ans = Math.max(ans, len[i]);
}
arr[0] = ans - 1;
int i = 0;
while (len[i] != ans) {
i++;
}
arr[1] = i;
return arr;
}
問題二:(最短迴文串)給定一個字符串 s,你可以通過在字符串前面添加字符將其轉換爲迴文串。找到並返回可以用這種方式轉換的最短迴文串。
思路:由於只能在字符串的前面添加字符,所以不由的想出要先找到原字符串的最長前綴迴文串,下面給出兩種方法。
方法一:直接尋找
這裏需要倒序遍歷字符串,定義一頭一尾兩個指針,再用一個while的內循環來取得其最長的前綴迴文串。
具體代碼如下:
public String shortestPalindrome1(String s) {
if (s.equals("") || s.length() == 1) {
return s;
}
int x = getFrontS(s);
return new StringBuilder(s.substring(x + 1)).reverse().toString() + s;
}
//獲得原字符串的最長前綴迴文字符串長度
private int getFrontS(String s) {
int a = 0;
for (int i = s.length() - 1; i > 0; i--) {
if (s.charAt(a) == s.charAt(i)) {
int b = i - 1;
a++;
while (a < b) {
if (s.charAt(a) == s.charAt(b)) {
a++;
b--;
}
else {
break;
}
}
if (a >= b) {
return i;
}
a = 0;
}
}
return 0;
}
方法二:KMP算法的next數組
這個next數組也很類似計數排序的那個計數數組,只不過這個數組記錄的是當前位置的最大前綴數,有了這個數組,最長前綴迴文串也就手到擒來了。
具體代碼如下:
public String shortestPalindrome2(String s) {
if (s.equals("") || s.length() == 1) {
return s;
}
String temp = s + "#" + new StringBuilder(s).reverse().toString() + "#";
//中間那個#是爲了把兩段字符串分隔開,避免產生干擾
int[] next = getNext1(temp);
int index = next[temp.length()-1]; //取得最長前綴迴文字符串的下標
return new StringBuilder(s.substring(index)).reverse().toString() + s;
}
private int[] getNext1(String p) {
int[] next = new int[p.length() + 1];
next[0] = -1;
for (int i = 0; i < p.length(); i++) {
next[i + 1] = next[i] + 1;
while (next[i + 1] > 0 && p.charAt(next[i + 1] - 1) != p.charAt(i)) {
next[i + 1] = next[next[i + 1] - 1] + 1;
}
}
return next;
}
特別是後面的Manacher 算法和KMP算法都很巧妙也很有回味價值。