力扣解题-125. 验证回文串
如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后,短语正着读和反着读都一样。则可以认为该短语是一个 回文串 。
字母和数字都属于字母数字字符。
给你一个字符串 s,如果它是 回文串 ,返回 true ;否则,返回 false 。
示例 1:
输入: s = "A man, a plan, a canal: Panama"
输出:true
解释:"amanaplanacanalpanama" 是回文串。
示例 2:
输入:s = "race a car"
输出:false
解释:"raceacar" 不是回文串。
示例 3:
输入:s = " "
输出:true
解释:在移除非字母数字字符之后,s 是一个空字符串 "" 。
由于空字符串正着反着读都一样,所以是回文串。
提示:
1 <= s.length <= 2 * 105
s 仅由可打印的 ASCII 字符组成
Related Topics
双指针、字符串
第一次解答
解题思路
核心方法:预处理 + 双指针验证,先通过正则替换移除非字母数字字符并转小写,再用左右双指针从两端向中间逐字符比对,逻辑直观但正则替换存在性能损耗。
核心逻辑拆解
验证回文串的核心是“先清洗字符串,再验证对称”:
- 清洗字符串:去掉所有非字母数字的字符(如逗号、空格、冒号),并将所有字母转为小写,得到纯字母数字的小写字符串;
- 边界处理:清洗后为空或只有1个字符,直接返回true(天然是回文);
- 双指针验证:左指针从开头、右指针从结尾同时向中间移动,逐字符比对是否相等;
- 只要有一对字符不相等,返回false;全部比对完成则返回true。
性能损耗分析
- 时间复杂度:O(n)(正则替换O(n) + 双指针遍历O(n)),但正则替换的常数因子大;
- 空间复杂度:O(n)(生成清洗后的新字符串);
- 核心损耗点:
replaceAll("[^a-zA-Z0-9]", "")使用正则表达式匹配替换,正则引擎的匹配过程耗时且生成新字符串;- 清洗后的字符串占用额外内存,导致内存开销高;
- 性能表现:耗时14ms仅击败10.50%用户,内存45.7MB击败10.71%用户,正是正则替换的损耗导致。
public boolean isPalindrome(String s) {
String t = s.replaceAll("[^a-zA-Z0-9]", "").toLowerCase();
if(t.length()==0 || t.length()==1){
return true;
}
int left =0;
int right = t.length()-1;
while (left<=right){
if (t.charAt(left)!=t.charAt(right)){
return false;
}
left++;
right--;
}
return true;
}
第二次解答
解题思路
核心方法:预处理 + 字符串反转比对,清洗字符串的逻辑与第一次一致,改为通过StringBuilder.reverse()反转字符串后直接比对,逻辑更简洁但性能仍受限于正则替换。
核心逻辑拆解
该解法是第一次解答的简化版:
- 同样先清洗字符串(去非字母数字+转小写);
- 将清洗后的字符串反转,若反转前后完全一致,则是回文串;
- 无需手动双指针遍历,利用
String.equals()直接比对。
性能说明
- 时间复杂度:O(n)(正则替换O(n) + 反转字符串O(n) + 比对O(n));
- 空间复杂度:O(n)(清洗后的字符串 + 反转后的字符串);
- 核心损耗:相比第一次解答,多了反转字符串的O(n)操作,且
StringBuilder.toString()生成新字符串,内存开销进一步增加; - 性能表现:耗时14ms(与第一次持平,正则仍为主要耗时),内存46.9MB仅击败5.03%用户,内存损耗更严重。
public boolean isPalindrome(String s) {
String t = s.replaceAll("[^a-zA-Z0-9]", "").toLowerCase();
return t.equals(new StringBuilder(t).reverse().toString());
}
示例解答
解题思路
解法1:原地双指针法(最优解)
核心方法:原字符串直接双指针遍历 + 字符过滤转换,无需生成清洗后的新字符串,在遍历过程中跳过非字母数字字符、实时转换大小写,空间复杂度O(1),是本题的最优解。
核心逻辑拆解
原地双指针的核心是“边过滤边比对”,无需预处理整个字符串:
- 左指针
left从0开始,右指针right从字符串末尾开始; - 移动左指针,跳过所有非字母数字字符,找到第一个有效字符;
- 移动右指针,跳过所有非字母数字字符,找到最后一个有效字符;
- 将两个有效字符转为小写后比对,若不相等直接返回false;
- 若相等,左指针右移、右指针左移,重复上述步骤,直到左右指针相遇。
关键优化点
- 字符有效性判断:用
Character.isLetterOrDigit(char c)替代正则,直接判断字符是否为字母/数字,无正则开销; - 大小写转换:用
Character.toLowerCase(char c)实时转换,无需提前转换整个字符串; - 原地遍历:无新字符串生成,仅使用两个指针变量,空间复杂度O(1)。
代码实现
public boolean isPalindrome(String s) {
if (s == null || s.length() == 0) {
return true;
}
int left = 0;
int right = s.length() - 1;
while (left < right) {
// 左指针跳过非字母数字字符
while (left < right && !Character.isLetterOrDigit(s.charAt(left))) {
left++;
}
// 右指针跳过非字母数字字符
while (left < right && !Character.isLetterOrDigit(s.charAt(right))) {
right--;
}
// 转换为小写后比对
char leftChar = Character.toLowerCase(s.charAt(left));
char rightChar = Character.toLowerCase(s.charAt(right));
if (leftChar != rightChar) {
return false;
}
// 指针移动
left++;
right--;
}
return true;
}
性能优势
- 时间复杂度:O(n)(仅一次遍历,每个字符最多被访问一次);
- 空间复杂度:O(1)(无新字符串生成,仅使用指针和临时字符变量);
- 性能表现:耗时可降至1~2ms(击败99%+用户),内存占用≤40MB(击败90%+用户),是本题的最优写法。
解法2:手动字符过滤法(替代正则的预处理方案)
核心方法:手动遍历过滤字符 + 双指针,用手动遍历替代正则替换清洗字符串,减少正则引擎的开销,性能优于前两种解法。
代码实现
public boolean isPalindrome(String s) {
// 手动过滤字符,避免正则开销
StringBuilder sb = new StringBuilder();
for (char c : s.toCharArray()) {
if (Character.isLetterOrDigit(c)) {
sb.append(Character.toLowerCase(c));
}
}
String t = sb.toString();
int left = 0, right = t.length() - 1;
while (left < right) {
if (t.charAt(left) != t.charAt(right)) {
return false;
}
left++;
right--;
}
return true;
}
性能说明
- 时间复杂度:O(n)(手动过滤O(n) + 双指针O(n)),手动过滤的常数因子远小于正则;
- 空间复杂度:O(n)(仍生成清洗后的字符串);
- 优势:性能介于正则解法和原地双指针解法之间,逻辑比原地双指针简单,比正则解法高效。
总结
- 正则预处理解法(第一次/第二次):逻辑直观但性能差,正则替换是核心损耗,仅适合快速编写代码;
- 手动过滤+双指针:性能优于正则解法,逻辑简洁,是工程中折中选择;
- 原地双指针法(最优解):空间O(1)、时间O(n)且常数因子小,无额外内存开销,是本题的最优写法;
- 关键优化技巧:
- 避免使用正则处理简单字符过滤,优先用
Character.isLetterOrDigit()/toLowerCase(); - 尽可能在原字符串上操作,减少新字符串生成;
- 双指针遍历过程中提前终止(发现不匹配直接返回),减少无效操作。
- 避免使用正则处理简单字符过滤,优先用