本文介绍三道跟Palindrome相关的题目。
首先是125. Valid Palindrome
Given a string, determine if it is a palindrome, considering only alphanumeric characters and ignoring cases.
Note: For the purpose of this problem, we define empty string as valid palindrome.
Example 1:
Input: “A man, a plan, a canal: Panama”
Output: true
Example 2:
Input: “race a car”
Output: false
大意为,只考虑字母和数字,且忽略大小写,判断所给的input是否为回文字符串。
那么思路就很简单了:
先遍历整个字符串,将其保存为只有大(或小)写字母和数字的字符串,然后对这个转换得到的字符串判断是否为回文
class Solution {
public:
bool isPalindrome(string s) {
string res = "";
for(int i = 0; i < s.length(); ++i)
{
if(s[i] > 64 && s[i] < 91)
{
//转为小写字符
res += (s[i] + 32);
}
else if(s[i] > 96 && s[i] < 123)
res += s[i];
else if(s[i] > 47 && s[i] < 58)
res += s[i];
}
int len = res.length();
//判断是否为回文,只遍历一半即可
for(int i = 0; i < len / 2; ++i)
if(res[i] != res[len - 1 - i])
return false;
return true;
}
};
Given a non-empty string s, you may delete at most one character. Judge whether you can make it a palindrome.
Example 1:
Input: “aba”
Output: True
Example 2:
Input: “abca”
Output: True
Explanation: You could delete the character ‘c’.
题意:给定一个字符串,判断在至多删除一个字符时,是否为回文串。
最开始会想到的方法很简单,一个字符一个字符遍历,看删除这个字符后,剩下的字符串是否为回文串。出题者很显然不想让我们这么做,会TLE,因此想到另一种方法:
如果是回文串,那么头尾的对应位置一定是相同的
因此分别比较头尾的对应位置,一旦发现某个位置两个字符不相等,则必定删除其中之一,只需判断这两种情况下得到的字符串是否回文,若有一个回文,则返回true,否则为false
题外话:删除字符串中某个位置的字符,我这里用了substr方法,假如有个字符串string s = "fdajwel";
要删除字符a,它在s[2],则可以这么写:s = s.substr(0, 2) + s.substr(3)
该函数原型为string substr (size_t pos = 0, size_t len = npos) const;
size_t pos
为起始位置,size_t len
为长度,若省略长度字段,则表示从第pos位置起,一直到字符串末尾
class Solution {
public:
//判断是否为回文串
bool isValid(string s)
{
int len = s.length();
for (int i = 0; i < len / 2; ++i)
if (s[i] != s[len - 1 - i])
return false;
return true;
}
bool validPalindrome(string s) {
if (isValid(s)) return true;
int len = s.length();
//若长度为2,肯定可以通过删除一个字符为回文
if (len == 0 || len == 2) return true;
if (s[0] != s[len - 1])
{
if (isValid(s.substr(1)) || isValid(s.substr(0, len - 1)))
return true;
return false;
}
for (int i = 0; i < len / 2; ++i)
{
if (s[i] != s[len - 1 - i])
{
//删除第i个字符
if (isValid(s.substr(0, i) + s.substr(i + 1))) return true;
//删除第len - 1 - i个字符
if (isValid(s.substr(0, len - 1 - i) + s.substr(len - i))) return true;
//若两者均不为true,直接返回false
return false;
}
}
return false;
}
};
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”
题意,给定一个字符串,在字符串前面添加尽可能少的字符,使其变为回文串
注意一点,这里是在前面添加,因此对于aabba,最短的回文串是abbaabba,虽然如果允许在后面加的话,应该是aabbaa,但我不知道题目故意这么出还是有什么别的用意,因此下面的代码也没考虑后面加字符。
首先是暴力解法。
class Solution {
public:
bool isPalindrome(string s)
{
int len = s.length();
for (int i = 0; i < len / 2; ++i)
{
if (s[i] != s[len - 1 - i])
return false;
}
return true;
}
string shortestPalindrome(string s) {
int len = s.length();
if (len == 0 || len == 1) return s;
int pos1 = 0;
char c = s[0];
for (int j = len - 1; j >= 0; --j)
{
if (s[j] == c)
{
if (isPalindrome(s.substr(0, j + 1)))
{
pos1 = j;
break;
}
}
}
string ss = s.substr(pos1 + 1);
reverse(ss.begin(), ss.end());
return ss + s;
}
};
首先,如果是回文串,那么原串的第一个字符一定在回文串中,因此先找到原串中,以第一个字符为起点的最长的回文串,之后将剩下的字符倒转放在原串前即可。
例如对于abbaa,发现以第一个字符a开头最长的回文串是abba,还剩末尾的a,则将其放在开头得到aabbaa
当然,这道题被赋予Hard难度,也不能这么水过…这种方法耗时200ms,看了别人那么多<50的也是很心动,因此接下来介绍KMP算法
网上已经有很多相关教程(贴个链接: 如何更好的理解和掌握KMP算法,觉得比那些所谓详解更易理解,特别是PMT和next指针),大多是用于匹配两个字符串的,乍一看好像跟我们这里不相关,那么这里先把前缀和后缀的概念引入,来解这道题:
仍旧是abbaa,我们知道它最长的回文串是aabbaabbaa(倒转串+原串),然后分别计算原串和倒转串的前缀和后缀:
原串 | 前缀 | 倒转串 | 后缀 |
---|---|---|---|
abbaa | a, ab, abb, abba | aabba | a, ba, bba, abba |
因此公共的最大子串是abba,长度为4,倒转串长度为5,因此从倒转串中截取5-4=1的字符放入原串开头,即得到aabbaa
那么再返回KMP中:
KMP算法原先是用来匹配两个字符串的,比如在aabbabcdabbabde 中查找abbabd,第二次朴素比较:
aabbabcdabbabde
abbabd
这时候发现,原串第七个字符c和目标串第六个字符d不匹配,朴素的办法是,把原串上的指针i往后移动一位至b,目标串的指针回到第一位a,重新新一轮的比较,而KMP则采用另一种思路:我去找已匹配的字符串中,前缀和后缀相同长度最长的位置。
例如已匹配完的字符串是abbab,后缀有字符串ab,前缀也有字符串ab,那么我们就可以知道下一次比较,其余部分不需要再比较了,下一轮比较应该是:
aabbabcdabbabde
abbabd
从代码上讲,某个字符串加上一个新字符后的公共前后缀长度,一定与该字符前一个字符的长度相关:要么长度不变(例如aaceaa和aaceaaa),要么长度加一(例如aacea和aaceaa),要么长度置零(例如aacea和aacead)
于是结合这道题,只要把前缀和后缀相同的部分剔除,保留不相同的那部分即可,就有了下面的代码:
class Solution {
public:
string shortestPalindrome(string s) {
string ts = s;
reverse(ts.begin(), ts.end());
string str = s + "#" + ts;
vector<int> v(str.length(), 0);
for (int i = 1; i < str.length(); ++i)
{
int j = v[i - 1];
//j最少为0,这里是当发生不匹配的时候,回溯的过程,如上面abbabd的例子,回溯到ab
while (j > 0 && str[i] != str[j])
j = v[j - 1];
//决定了接下来是否加1
if (str[i] == str[j])
j++;
v[i] = j;
}
//v[str.length() - 1]是公共前后缀的最大长度,用上面的方法从倒转串中减去响应长度即可
int len = s.length() - v[str.length() - 1];
return ts.substr(0, len) + s;
}
};
上面的代码中加了’#’,是为了防止例如aaa这样的字符串,如果不加分隔符,会影响最后的结果。
然后就贴一张运行时间图(中间的TLE只怪自己又作妖了= =):
可以看到加快了不止一点。