回文串是一个正着读和反着读顺序一样的字符串。"aba" 是回文串,"abba" 是回文串,"abc" 不是回文串。
回文串的题目,都要使用一个基本的逻辑,就是判断当前这个字符串是不是回文串。以 c++ 为例,代码如下。这种方法也可以称为双指针法,两个指针从字符串的两端向中间遍历每个字符,如果中间发现两个字符不相同,则不是回文字符串;遍历到最后,说明是回文串。
bool isPalindrome(const string s) {
int len = s.size();
if (len <= 1) {
return true;
}
int left = 0;
int right = len - 1;
while (left < right) {
if (s[left] != s[right]) {
return false;
}
left++;
right--;
}
return true;
}
双指针法,在其它数据结构题目中也会用到,比如链表中会用到快慢指针,也属于双指针。快速排序算法中,给选中的数据找到合适的位置,也会使用两个指针从两边向中间对数据进行遍历,也属于双指针。
判断回文串,也可以使用从中间向两边的方法,使用这种方法时,首先需要判断字符串的长度是奇数还是偶数,如果是奇数的话,那么两个指针从中间的位置开始向两边遍历;偶数的话,两个指针分别从中间两个元素的位置开始遍历。
没有特殊要求的话,优先选用从两边向中间的方式来判断一个字符串是不是回文串。
1 验证回文串
leetcode:验证回文串
题目要求判断给定的字符串是不是回文串,如果是回文串,则返回 true;如果原字符串不是回文串,那么最多可以删除一个字符,如果删除一个字符之后的字符串是回文串,那么返回 true,否则返回 false。
1.1 基础算法
(1)判断原字符串是不是回文串,是回文串返回 true;否则执行第 2 步
(2)遍历字符串的每个字符,分别将每个字符删除,判断删除字符之后的字符串是不是回文串。
如果是回文串,则返回 true,停止遍历;如果字符遍历结束,则返回 false。
这种算法的时间复杂度是 O(n 的平方),偏大,所以优先选用第二种方法,第二种算法的时间复杂度是 O(n)。
1.2 双指针,动态判断
(1)使用双指针,从两边向中间遍历每个字符
(2)如果遍历到两个字符不相等,则讨论如下两种情况
① 删除左边的字符,判断子串是不是回文串,是的话则返回 true
② 删除右边的字符,判断子串是不是回文串,是的话返回 true
如果两种情况都不是回文串,那么返回 false。
(3)如果字符串遍历结束,都满足回文串的要求,则返回 true
class Solution {
public:
bool validPalindrome(string s) {
int len = s.size();
int left = 0;
int right = len - 1;
bool result = true;
while (left < right) {
if (s[left] != s[right]) {
if (isPalindrome(s.substr(left + 1, right - left))) {
return true;
}
if (isPalindrome(s.substr(left, right - left))) {
return true;
}
return false;
}
left++;
right--;
}
return true;
}
bool isPalindrome(const string s) {
int len = s.size();
if (len <= 1) {
return true;
}
int left = 0;
int right = len - 1;
while (left < right) {
if (s[left] != s[right]) {
return false;
}
left++;
right--;
}
return true;
}
};
2 最长回文子串
leetcode:最长回文子串
一个字符串 s,找到 s 中最长的回文子串。
2.1 动态规划
将所有的子串的情况都遍历到,在遍历的过程中,判断子串是不是回文串,如果是回文串并且长度比已有的回文串长的话,那么就更新结果。属于动态规划算法。
这个算法的事件复杂度是 O(n 的平方),时间复杂度较高,在 leetcode 上运行时会超时。
class Solution {
public:
string longestPalindrome(string s) {
int size = s.size();
for (int i = 0; i < size; i++) {
for (int j = i; j < size; j++) {
if (isPalindrome(s.substr(i, j - i + 1))) {
if (j - i + 1 > max_length) {
max_length = j - i + 1;
max_str = s.substr(i, j - i + 1);
}
}
}
}
return max_str;
}
private:
bool isPalindrome(string s) {
int size = s.size();
int i = 0;
int j = size - 1;
while (i < j) {
if (s[i] != s[j]) {
return false;
}
i++;
j--;
}
return true;
}
private:
int max_length = 0;
string max_str;
};
这个题目要找的是最长回文子串,我们能想到 j 的遍历从大向小遍历。这样遍历的话就是先遍历长度大的字符串,再遍历长度小的字符串。当第一个遍历到一个字符串是回文串,那么这个回文串就是长度最大的回文串,就可以直接返回。从小向大进行遍历,当遍历到这个字符串是回文串的时候,仍然不能返回,因为不能确定这个字符串是不是长度最大的回文串,需要将所有情况都遍历完毕才能确定最大的回文字符串。再进一步思考,我们可以以子串的长度作为遍历的依据,长度从大到小进行遍历。
如下是使用 c 语言实现的算法。
char ret[1001] = {'\0'};
char* longestPalindrome(char* s) {
int length = strlen(s);
if (length <= 1) {
return s;
}
memset(ret, 0, 1001);
for (int len = length; len >= 1; len--) {
for (int i = 0; i < length; i++) {
if (i + len - 1 >= length) {
break;
}
int start_index = i;
int end_index = i + len - 1;
if (isPalindrome(s, start_index, end_index)) {
int index = 0;
for (int i = start_index; i <= end_index; i++) {
ret[index] = s[i];
index++;
}
return ret;
}
}
}
return NULL;
}
int isPalindrome(char *s, int start_index, int end_index) {
while (start_index < end_index) {
if (s[start_index] != s[end_index]) {
return 0;
}
start_index++;
end_index--;
}
return 1;
}
官方题解中也是遍历了子串的长度,但是是从小到大进行遍历的,同时还记录了已经遍历过的子串的结果。当判断长度较大的字符串是不是回文串时,可以直接基于历史记录来做判断。这也是动态规划常用的思路,就是在遍历的过程中记录历史信息,这样在后边的遍历中可以直接使用已经记录的历史信息。
官方题解中,正因为长度是从小到大进行遍历的,所以在遍历的时候判断字符串是不是回文串的时候,可以使用历史信息进行判断。因为 s[i][j] 比 s[i + 1][j - 1] 的长度要大,后者是不是回文串已经是确定的。
2.2 中心扩展法
leetcode 官方题解中提供了另外一种方法,中心扩展法。
这个问题的多种算法之间的区别就是遍历的对象不一样:
(1)两级遍历,遍历字符串的索引
(2)两级遍历,一级遍历子串的长度,一级遍历字符串的索引
(3)中心扩展法也是遍历字符串的索引,不过在计算逻辑上,是把索引当成了要遍历的子串的中心
class Solution {
public:
string longestPalindrome(string s) {
int size = s.size();
int start = 0;
int end = 0;
for (int i = 0; i < size; i++) {
int left1 = i;
int right1 = i;
int left2 = i;
int right2 = i + 1;
// 从中心向两边扩展,要考虑两种情况
// 奇数的情况,偶数的情况
centerExpand(s, left1, right1);
centerExpand(s, left2, right2);
if (right1 - left1 > end - start) {
start = left1;
end = right1;
}
if (right2 - left2 > end - start) {
start = left2;
end = right2;
}
}
return s.substr(start, end - start + 1);
}
void centerExpand(string &s, int &left, int &right) {
while (left >= 0 && right < s.size() && s[left] == s[right]) {
left--;
right++;
}
// 循环退出,说明最后一个索引不满足回文串的情况
// 要么是 left 和 right 越界了,要么是当前这两个字符不相等
// 这两种情况下,left 需要 ++, right 需要 --
left++;
right--;
}
};
3 分割回文子串
leetcode:分割回文子串
本文,用基础的算法去思考的话,很难思考下去,遇到这种情况一般要考是不是可以使用递归算法。
class Solution {
public:
vector<vector<string>> partition(string s) {
partitionHelper(s, 0);
return result_;
}
void partitionHelper(const string &s, int start_index) {
int len = s.size();
if (start_index >= len) {
result_.push_back(one_instance_);
return;
}
for (int i = start_index; i < len; i++) {
if (isPalindome(s, start_index, i)) {
one_instance_.push_back(s.substr(start_index, i - start_index + 1));
partitionHelper(s, i + 1);
one_instance_.pop_back();
}
}
}
bool isPalindome(const string &s, int start, int end) {
if (start >= end) {
return true;
}
if (flag[start][end] == 1) {
return true;
}
if (flag[start][end] == -1) {
return false;
}
int tmp_start = start;
int tmp_end = end;
while (tmp_start < tmp_end) {
if (s[tmp_start] != s[tmp_end]) {
flag[tmp_start][tmp_end] = -1;
flag[start][end] = -1;
return false;
}
tmp_start++;
tmp_end--;
}
flag[start][end] = 1;
return true;
}
private:
int flag[20][20] = {0};
vector<vector<string>> result_;
vector<string> one_instance_;
};