问题一:(最长回文子串)给定一个字符串 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算法都很巧妙也很有回味价值。