给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
说明:本题中,我们将空字符串定义为有效的回文串。
示例 1:
输入: "A man, a plan, a canal: Panama"
输出: true
解释:"amanaplanacanalpanama" 是回文串
示例 2:
输入: "race a car"
输出: false
解释:"raceacar" 不是回文串
提示:
双指针法——左右指针
使用双指针,分别在字符串最左端和最右端使用左指针left和右指针right来进行遍历。
考虑到①题目输入中会有空格和逗号等的存在,而算法只考虑数字字符和字母,因此用ASCII码作为条件过滤,只考虑ASCII码在48~57(数字0~9)、65~90(大写字母A~Z)和97~122(小写字母a~z)的字符,对其进行验证。
②题目要求忽略大小写,意味着如果left和right指向的值可以不相同且互为大小写(ASCII码值相差恰为32)。
则使用一个循环,终止条件为left==right,每次判断left和right指向的值是否在[48, 57][65, 90][97, 122]范围内,不在的则跳过向下迭代。如果left的值等于right的值或者二者较小者在[65,90]范围内且恰好比较大者小32,left与right分别向右、向左迭代,反之直接返回false。循环完成后返回true。
class Solution {
public:
bool isPalindrome(string s) {
int n = s.size();
if (n == 0)
return true;//空字符串的退化情况
int left = 0, right = n - 1;//声明双指针
while (left < right)
{
if (s[left] < 48 || s[left] > 57 && s[left] < 65 || s[left] > 90 && s[left] < 97 || s[left] >122)//如果左指针的值不是数字字符或大小写字符
{
left++;
continue;
}
if (s[right] < 48 || s[right] > 57 && s[right] < 65 || s[right] > 90 && s[right] < 97 || s[right] >122)//如果右指针的值不是数字字符或大小写字符
right--;
else
{
if (s[left] == s[right])//如果左右指针的值相等
{
left++;
right--;//那么两指针向下迭代
}
else//如果不相等,先判断分别互为大小写的情况
{
if (s[left] < s[right])
{
if (s[left] >= 65 && s[left] <= 90 && s[right] - s[left] == 32) { left++; right--; }
else return false;
}
else
{
if (s[right] >= 65 && s[right] <= 90 && s[left] - s[right] == 32) { left++; right--; }
else return false;
}
}
}
}
return true;
}
};
纠错:总结在调试该代码中所犯的错误
①
if (s[left] < 48 || s[left] > 57 && s[left] < 65 || s[left] > 90 && s[left] < 97 || s[left] >122)//如果左指针的值不是数字字符或大小写字符
该行的逻辑判断把||全写成了&&
②
if (s[left] < 48 || s[left] > 57 && s[left] < 65 || s[left] > 90 && s[left] < 97 || s[left] >122)//如果左指针的值不是数字字符或大小写字符
{
left++;
continue;
}
这里的continue一开始没加上。这会导致如果s[left]是非数字字母,于是left递增了一,但之后s[left]仍然是非数字字母,可同时s[right]是数字字母,这样将会进入后续的判断从而直接返回false。
本题考查的是语言中常用字符(串)相关 API 的使用。本题解会给出解题思路以及参考代码,如果代码中有读者不熟悉的 API,可以自行查阅资料学习。
最简单的方法是对字符串 s 进行一次遍历,并将其中的字母和数字字符进行保留,放在另一个字符串 sgood 中。这样我们只需要判断 sgood 是否是一个普通的回文串即可。
判断的方法有两种。第一种是使用语言中的字符串翻转 API 得到 sgood 的逆序字符串 sgood_rev,只要这两个字符串相同,那么 sgood 就是回文串。
class Solution {
public:
bool isPalindrome(string s) {
string sgood;
for (char ch: s) {
if (isalnum(ch)) {
sgood += tolower(ch);
}
}
string sgood_rev(sgood.rbegin(), sgood.rend());
return sgood == sgood_rev;
}
};
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/valid-palindrome/solution/yan-zheng-hui-wen-chuan-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
另一种方法是使用双指针。初始时,左右指针分别指向 sgood 的两侧,随后我们不断地将这两个指针相向移动,每次移动一步,并判断这两个指针指向的字符是否相同。当这两个指针相遇时,就说明 sgood 时回文串。
class Solution {
public:
bool isPalindrome(string s) {
string sgood;
for (char ch: s) {
if (isalnum(ch)) {
sgood += tolower(ch);
}
}
int n = sgood.size();
int left = 0, right = n - 1;
while (left < right) {
if (sgood[left] != sgood[right]) {
return false;
}
++left;
--right;
}
return true;
}
};
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/valid-palindrome/solution/yan-zheng-hui-wen-chuan-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
复杂度分析
我们可以对方法一中第二种判断回文串的方法进行优化,就可以得到只使用 O(1) 空间的算法。
我们直接在原字符串 s 上使用双指针。在移动任意一个指针时,需要不断地向另一指针的方向移动,直到遇到一个字母或数字字符,或者两指针重合为止。也就是说,我们每次将指针移到下一个字母字符或数字字符,再判断这两个指针指向的字符是否相同。
class Solution {
public:
bool isPalindrome(string s) {
int n = s.size();
int left = 0, right = n - 1;
while (left < right) {
while (left < right && !isalnum(s[left])) {
++left;
}
while (left < right && !isalnum(s[right])) {
--right;
}
if (left < right) {
if (tolower(s[left]) != tolower(s[right])) {
return false;
}
++left;
--right;
}
}
return true;
}
};
作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/valid-palindrome/solution/yan-zheng-hui-wen-chuan-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
复杂度分析
①常用字符(串)API的使用
笔者解法里通过判断ASCII码的分布范围来筛选出字母和数字,但是有三个区间段,判断条件写起来非常冗长,而且很容易出错。(这道题编译错误的时候挨个查bug就查了很久)其次,对于字母大小写的判断通过ACSII码之间的数值关系也会使得代码很冗长,并且容易出错。
但是由官解我们知道,对字母数字的判断可以直接调用API,isalnum()即可筛选出字母和数字。其次,API tolower()即可将大写字母直接转换为小写,从而省去很多不必要的判断。
通过将用ACSII码判断的方式改为调用两个API,代码明显简洁易懂,且出错率大大降低。
②循环语句的优化
开始判断字符是否为数字字母时,笔者解法里分别对左右指针使用两个for循环来判断,而前一个需要使用continue,后一个在else部分写出算法主体,即冗长又缺乏对称美,没有可读性。
if (s[left] < 48 || s[left] > 57 && s[left] < 65 || s[left] > 90 && s[left] < 97 || s[left] >122)//如果左指针的值不是数字字符或大小写字符
{
left++;
continue;
}
if (s[right] < 48 || s[right] > 57 && s[right] < 65 || s[right] > 90 && s[right] < 97 || s[right] >122)//如果右指针的值不是数字字符或大小写字符
right--;
else
{
...//主体部分
}
而在官解里分别对左右指针使用了while循环语句,来判断两个指针的值是否为数字字母,代码更加简洁易懂,同时具有对称的美感。
while (left < right && !isalnum(s[left])) {
++left;
}
while (left < right && !isalnum(s[right])) {
--right;
}
...//主体部分