Given a string s, you are allowed to convert it to a palindrome by adding characters in front of it. Find and return the shortest palindrome you can find by performing this transformation.
Example 1:
Input:"aacecaaa"
Output:"aaacecaaa"
Example 2:
Input:"abcd"
Output:"dcbabcd"
Credits:
Special thanks to @ifanchu for adding this problem and creating all test cases. Thanks to @Freezen for additional test cases.
這道題讓我們求最短的回文串,LeetCode 中關於回文串的其他的題目有 Palindrome Number,Validate Palindrome,Palindrome Partitioning,Palindrome Partitioning II 和 Longest Palindromic Substring。題目讓在給定字符串s的前面加上最少個字符,使之變成回文串,來看題目中給的兩個例子,最壞的情況下是s中沒有相同的字符,那么最小需要添加字符的個數為 s.size() - 1 個,第一個例子的字符串包含一個回文串,只需再在前面添加一個字符即可,還有一點需要注意的是,前面添加的字符串都是從s的末尾開始,一位一位往前添加的,那么只需要知道從s末尾開始需要添加到前面的個數。
首先還是先將待處理的字符串s翻轉得到t,然后比較原字符串s和翻轉字符串t,從第一個字符開始逐一比較,如果相等,說明s本身就是回文串,不用添加任何字符,直接返回即可;如果不相等,s去掉最后一位,t去掉第一位,繼續比較,以此類推直至有相等,或者循環結束,這樣就能將兩個字符串在正確的位置拼接起來了,代碼請參見評論區5樓。但這種寫法卻會超時 TLE,無法通過 OJ,所以需要用一些比較巧妙的方法來解。
這里使用雙指針來找出字符串s的最長回文前綴的大概范圍,這里所謂的最長回文前綴是指從開頭開始到某個位置為止是回文串,比如 "abbac" 這個子串,可以知道前四個字符組成的回文串 "abba" 就是最長回文前綴。方法是指針i和j分別指向s串的開頭和末尾,若 s[i] 和 s[j] 相等,則i自增1,j自減1,否則只有j自減1。需要注意的是,這樣遍歷一遍后,得到的范圍 [0, i) 中的子串並不一定就是最大回文前綴,也可能還需要添加字符,舉個例子來說,對於 "adcba",在 for 循環執行之后,i=2,可以發現前面的 "ad" 並不是最長回文前綴,其本身甚至不是回文串,需要再次調用遞歸函數來填充使其變為回文串,但可以確定的是可以添加最少的字符數讓其變為回文串。而且可以確定的是后面剩余的部分肯定不屬於回文前綴,此時提取出剩下的字符,翻轉一下加到最前面,而對范圍 [0, i) 內的子串再次遞歸調用本函數,這樣,在子函數最終會組成最短的回文串,從而使得整個的回文串就是最短的,參見代碼如下:
C++ 解法一:
class Solution { public: string shortestPalindrome(string s) { int i = 0, n = s.size(); for (int j = n - 1; j >= 0; --j) { if (s[i] == s[j]) ++i; } if (i == n) return s; string rem = s.substr(i); reverse(rem.begin(), rem.end()); return rem + shortestPalindrome(s.substr(0, i)) + s.substr(i); } };
Java 解法一:
public class Solution { public String shortestPalindrome(String s) { int i = 0, n = s.length(); for (int j = n - 1; j >= 0; --j) { if (s.charAt(i) == s.charAt(j)) ++i; } if (i == n) return s; String rem = s.substring(i); String rem_rev = new StringBuilder(rem).reverse().toString(); return rem_rev + shortestPalindrome(s.substring(0, i)) + rem; } }
其實這道題的最快的解法是使用 KMP 算法,KMP 算法是一種專門用來匹配字符串的高效的算法,具體方法可以參見博主之前的這篇博文 KMP Algorithm 字符串匹配算法KMP小結。把s和其轉置r連接起來,中間加上一個其他字符,形成一個新的字符串t,還需要一個和t長度相同的一位數組 next,其中 next[i] 表示從 t[i] 到開頭的子串的相同前綴后綴的個數,具體可參考 KMP 算法中解釋。最后把不相同的個數對應的字符串添加到s之前即可,代碼如下:
C++ 解法二:
class Solution { public: string shortestPalindrome(string s) { string r = s; reverse(r.begin(), r.end()); string t = s + "#" + r; vector<int> next(t.size(), 0); for (int i = 1; i < t.size(); ++i) { int j = next[i - 1]; while (j > 0 && t[i] != t[j]) j = next[j - 1]; next[i] = (j += t[i] == t[j]); } return r.substr(0, s.size() - next.back()) + s; } };
Java 解法二:
public class Solution { public String shortestPalindrome(String s) { String r = new StringBuilder(s).reverse().toString(); String t = s + "#" + r; int[] next = new int[t.length()]; for (int i = 1; i < t.length(); ++i) { int j = next[i - 1]; while (j > 0 && t.charAt(i) != t.charAt(j)) j = next[j - 1]; j += (t.charAt(i) == t.charAt(j)) ? 1 : 0; next[i] = j; } return r.substring(0, s.length() - next[t.length() - 1]) + s; } }
從上面的 Java 和 C++ 的代碼中,可以看出來 C++ 和 Java 在使用雙等號上的明顯的不同,感覺 Java 對於雙等號對使用更加苛刻一些,比如 Java 中的雙等號只對 primitive 類數據結構(比如 int, char 等)有效,但是即便有效,也不能將結果直接當1或者0來用。而對於一些從 Object 派生出來的類,比如 Integer 或者 String 等,不能直接用雙等號來比較,而是要用其自帶的 equals() 函數來比較,因為雙等號判斷的是不是同一個對象,而不是他們所表示的值是否相同。同樣需要注意的是,Stack 的 peek() 函數取出的也是對象,不能直接和另一個 Stack 的 peek() 取出的對象直接雙等,而是使用 equals 或者先將其中一個強行轉換成 primitive 類,再和另一個強行比較。
下面這種方法的寫法比較簡潔,雖然不是明顯的 KMP 算法,但是也有其的影子在里面。這種 Java 寫法也是在找相同的前綴后綴,但是並沒有每次把前綴后綴取出來比較,而是用兩個指針分別指向對應的位置比較,然后用 end 指向相同后綴的起始位置,最后再根據 end 的值來拼接兩個字符串。有意思的是這種方法對應的 C++ 寫法會 TLE,跟上面正好相反,那么我們是否能得出 Java 的 substring 操作略慢,而 C++ 的 reverse 略慢呢,博主也僅僅是猜測而已。
Java 解法三:
public class Solution { public String shortestPalindrome(String s) { int i = 0, end = s.length() - 1, j = end; char arr = s.toCharArray(); while (i < j) { if (arr[i] == arr[j]) { ++i; --j; } else { i = 0; --end; j = end; } } return new StringBuilder(s.substring(end + 1)).reverse().toString() + s; } }
Github 同步地址:
https://github.com/grandyang/leetcode/issues/214
類似題目:
Longest Palindromic Subsequence
參考資料:
https://leetcode.com/problems/shortest-palindrome/
https://leetcode.com/problems/shortest-palindrome/discuss/60098/My-7-lines-recursive-Java-solution