Longest Palindromic SubString(最长回文)
最长回文是指给定一个字符串,找出其中最长的回文。Longest Palindromic SubString介绍了几种算法,翻译记录一下。
方法1:最长公共子串
将字符串翻转,然后找出两个字符串的最长公共子串,这个子串同时也是最长回文。
这个方法看似正确,其实有缺陷。
如,S=”caba”,S’=”abac”,最长子串是aba,求解正确。
但是,对于S=”abacdfgdcaba”,S’=”abacdgfdcaba”,最长公共子串是abacd,显然,这个不是回文。
从中可以看出,错误原因是由于非回文的子串正好是原字符串的一部分。为了修正这个错误,需要在发现最长子串的时候,检查子串的下标是不是与翻转后的原始下标一致(原文是we check if the substring’s indices are the same as the reversed substring’s original indices,我的理解是对于最长公共子串,如果是合法的回文,相对于S的头部,S’的末尾的偏移位置应是一样的,如果不一样,则意味着该子串只是公共子串,并不具备回文属性),如果一致,则更新最长回文,否则跳过该回文。该算法使用动态规划算法,时间消耗
O(n2)
O
(
n
2
)
。详见Longest Common SubString。该算法使用的是最长后缀算法,比我设想的动态规划要简洁许多,需要多复习。
方法2: 暴力破解
最简单最粗暴的算法,遍历所有可能,找到一个答案。
复杂性分析:
- 时间复杂度: O(n3) O ( n 3 ) .假设n是输入字符串的长度,则共有n(n-1)/2种子串。检查是否回文需要花费O(n),总的复杂度就是 O(n3) O ( n 3 ) 。
- 空间复杂度: O(1) O ( 1 )
方法3: 动态规划
为改进暴力破解方案,我们需要首先观察如何避免回文检查过程中的不必要的重复计算。如字符串ababa,如果我们已经知道bab是个回文,显然ababa也是一个回文,因为左边和右边的字符是相同的。
我们定义P(i,j)如下:
因此:
P(i,j) = (P(i+1, j-1) and Si == Sj)
初始条件:
P(i,i) = true
P(i, i+1) = (Si == Si+1)
这是个很直接的DP算法,我们首先初始化一个和两个字符的回文,然后计算3个或者更多.
复杂度分析:
- 时间复杂度: O(n2) O ( n 2 )
- 空间负杂度: O(n2) O ( n 2 )
方法4: 中心扩展(Expand Around Center)
我们可以用常量空间, O(n2) O ( n 2 ) 的时间来解决这个问题。
我们观察到回文总是围绕某个中心的一个镜像。因此,回文可以从某个中心扩展,共有2n-1个中心。
为什么会有2n-1个中心?因为中心也可能会在两个字符中间。如abba,中心就在两个b中间。
public String longestPalindrome(String s) {
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);
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;
}
复杂度分析:
- 时间复杂度: O(n2) O ( n 2 )
- 空间复杂度: O(1) O ( 1 )
方法5: Manacher算法
Manacher算法复杂度是O(n),详见Manacher’s algorithm. (还没来的及研究,存档先)
附上我实现的几个方法:
版本1: 暴力破解
public String longestPalindrome(String s) {
if (s == null || s.length() <= 1) {
return s;
}
int size = 0;
boolean isVliad = false;
String sub = "";
for (int i = 0; i < s.length(); i++) {
for (int k = size; k < s.length() - i; k++) {
isVliad = true;
for (int x = i,y = i + k; x <= y; x++,y--) {
if (s.charAt(x) == s.charAt(y)) {
System.out.println(String.format("%d-%d:%s", x, y, s.charAt(x)));
} else {
isVliad = false;
break;
}
}
if (isVliad && (k + 1) > size) {
size = k + 1;
sub = s.substring(i, i + k + 1);
System.out.println(String.format("%d----%d:%s", i, k, sub));
}
}
}
System.out.println(sub);
return sub;
}
毫无悬念,提交后直接超时。
版本2: 改进型的暴力破解
public String longestPalindrome(String s) {
if (s == null || s.length() <= 1) {
return s;
}
Map<Character, List<Integer>> charMap = new HashMap<>();
for (int i = 0; i < s.length(); i++) {
Character k = s.charAt(i);
List<Integer> set = charMap.get(k);
if (set == null) {
set = new ArrayList<>();
set.add(i);
charMap.put(k, set);
} else {
set.add(i);
}
}
boolean isValid = false;
int size = 1;
String sub = s.substring(0, 1);
int tail = 0;
for (int i = 0; i < s.length(); i++) {
List<Integer> set = charMap.get(s.charAt(i));
for (int k = set.size() - 1; set.get(k) > i; k--) {
isValid = true;
tail = set.get(k);
if (size > (tail - i + 1)) {
break;
}
for (int x = i,y = tail; x < y; x++,y--) {
if (s.charAt(x) == s.charAt(y)) {
// System.out.println(String.format("%d-%d:%s", x, y, s.charAt(x)));
} else {
isValid = false;
break;
}
}
if (isValid) {
if (size < (tail - i + 1)) {
size = tail - i + 1;
sub = s.substring(i, tail + 1);
System.out.println(String.format("%d----%d:%s", i, k, sub));
}
break;
}
}
}
System.out.println(sub);
return sub;
}
只是用了一些辅助手段,减少了遍历次数,提交通过,耗时144ms。(当然,我给不出复杂度分析。这是不是从一个侧面说明了工程方法可以解决算法问题?。。。)
版本3: 动态规划算法
public String longestPalindrome(String s) {
if (s == null || s.length() <= 1) {
return s;
}
boolean track[][] = new boolean[s.length()][s.length()];
String longest = s.substring(0, 1);
for (int i = 0; i < s.length(); i++) {
track[i][i] = true;
}
int j = 0;
for (int k = 1; k < s.length(); k++) {
for (int i = 0; i < s.length() - k; i++) {
j = i + k;
if (k == 1 || track[i+1][j-1]) {
if (s.charAt(i) == s.charAt(j)) {
track[i][j] = true;
if (j - i + 1 > longest.length()) {
longest = s.substring(i, j + 1);
}
}
}
}
}
return longest;
}
不负众望,60ms。
版本4: 最长公共子串
public String longestPalindrome(String s) {
if (s == null || s.length() <= 1) {
return s;
}
StringBuilder sb = new StringBuilder();
for (int i = s.length() - 1; i >= 0; i--) {
sb.append(s.charAt(i));
}
String r = sb.toString();
int track[][] = new int[s.length()][s.length()];
String sub = s.substring(0, 1);
String rsub = "";
String tmp = "";
for (int i = 0; i < s.length(); i++) {
for (int j = 0; j < s.length(); j++) {
if (s.charAt(i) == r.charAt(j)) {
if (i == 0 || j == 0) {
track[i][j] = 1;
} else {
track[i][j] = track[i-1][j-1] + 1;
}
if (track[i][j] > sub.length()) {
int start = i - track[i][j] + 1;
tmp = s.substring(start, i + 1);
rsub = r.substring(r.length() - i - 1, r.length() - start);
// System.out.println(String.format("%d-%d :: %d-%d", start, i + 1, r.length() - i - 1, r.length() - start- 1));
// System.out.println(tmp + "--" + rsub);
if (tmp.equals(rsub)) {
sub = tmp;
System.out.println(sub);
}
}
}
}
}
return sub;
}
有点意外,同样是 O(n2) O ( n 2 ) 算法,耗时是366ms,比我的改进型暴力破解算法还要多。估计是字符串截取和比较导致多余的消耗。