求解最长回文子串的问题最近经常遇到,特别是近期的笔试,因而有了一个学习的契机。先说说回文字符串,回文字符串的意思是从左往右看和从右往左看都是一样的,即我们如果以中心为轴,这个字符串是左右对称的,如字符串"abcba","abba"。字符串"abcba"有奇数个字符,所以以中间字符'c'为轴左右对称,而字符串"abba"有偶数个字符,所以是对半开来对称的。而顾名思义,最长回文子串就是指一个字符串中最长的具有回文性质的子串了。
用暴力的方法是不现实的,一个字符串的子串个数很多(用2个for循环求解,i : 0-n-1,j : i- n-1),有n * (n+1) / 2个,如果对所有子串都进行回文判断,需要O(n3),显然耗时巨大。
常规的求解方法有2种:动态规划法,中心扩展判断法,这2种算法都是O(n2)的时间复杂度。
假设dp[ i ][ j ]的值为true,表示字符串s中下标从 i 到 j 的字符组成的子串是回文串。那么可以推出:
dp[ i ][ j ] = dp[ i + 1][ j - 1] && s[ i ] == s[ j ]。
这是一般的情况,由于需要依靠i+1, j -1,所以有可能 i + 1 = j -1, i +1 = (j - 1) -1,因此需要求出基准情况才能套用以上的公式:
a. i + 1 = j -1,即回文长度为1时,dp[ i ][ i ] = true;
b. i +1 = (j - 1) -1,即回文长度为2时,dp[ i ][ i + 1] = (s[ i ] == s[ i + 1])。
有了以上分析就可以写出代码了。需要注意的是动态规划需要额外的O(n2)的空间。
//Written by zhou
//2013.11.22
string longestPalindrome(string s) {
size_t n = s.length();
bool **dp = new bool*[n];
for (size_t i = 0; i < n; ++i)
{
dp[i] = new bool[n];
}
//为基准情况赋值
int startPos = 0;
int max = 1;
for (size_t i = 0; i < n; ++i)
{
dp[i][i] = true;
if (i + 1 < n)
{
if (s[i] == s[i+1])
{
dp[i][i+1] = true;
startPos = i;
max = 2;
}
else dp[i][i+1] = false;
}
}
//动规求解,前面已求len = 1, len = 2的情况
for (int len = 3; len <= n; ++len)
{
for (int i = 0; i < n - len + 1; ++i)
{
int j = i + len - 1;
if (dp[i+1][j-1] && s[i] == s[j])
{
dp[i][j] = true;
int curLen = j - i + 1;
if (curLen > max)
{
startPos = i;
max = curLen;
}
}
else dp[i][j] = false;
}
}
//释放二维数组
for (size_t i = 0; i < n; ++i)
delete[] dp[i];
delete[] dp;
return s.substr(startPos,max);
}
2. 中心扩展法
因为回文字符串是以中心轴对称的,所以如果我们从下标 i 出发,用2个指针向 i 的两边扩展判断是否相等,那么只需要对0到
n-1的下标都做此操作,就可以求出最长的回文子串。但需要注意的是,回文字符串有奇偶对称之分,即"abcba"与"abba"2种类型,
因此需要在代码编写时都做判断。
设函数int Palindromic ( string &s, int i ,int j) 是求由下标 i 和 j 向两边扩展的回文串的长度,那么对0至n-1的下标,调用2次此函数:
int lenOdd = Palindromic( str, i, i ) 和 int lenEven = Palindromic (str , i , j ),即可求得以i 下标为奇回文和偶回文的子串长度。
接下来以lenOdd和lenEven中的最大值与当前最大值max比较即可。
这个方法有一个好处是时间复杂度为O(n2),且不需要使用额外的空间。
代码如下,欢迎指导交流~
//Written by zhou
//2013.11.22
string longestPalindrome(string s) {
size_t n = s.length();
int startPos = 0;
int max = 1;
for (int i = 0; i < n; ++i)
{
int oddLen = 0, evenLen = 0, curLen;
oddLen = Palindromic(s,i,i);
if (i + 1 < n)
evenLen = Palindromic(s,i,i+1);
curLen = oddLen > evenLen? oddLen : evenLen;
if (curLen > max)
{
max = curLen;
if (max & 0x1)
startPos = i - max / 2;
else
startPos = i - (max - 1) / 2;
}
}
return s.substr(startPos,max);
}
int Palindromic(const string &str, int i, int j)
{
size_t n = str.length();
int curLen = 0;
while (i >= 0 && j < n && str[i] == str[j])
{
--i;
++j;
}
curLen = (j-1) - (i+1) + 1;
return curLen;
}
除了以上介绍的2种方法外,还有一种Manacher算法的O(n)的算法,待有时间学习时再做更新。