#LC125. 验证回文串
验证回文串
LeetCode125这道题是LeetCode上的经典题目“验证回文串”,虽然看似简单,但它考察了字符串处理的基本功和双指针思想。
为了让你适应NOI/NOIP的考试环境,我将这道题改写成了标准的OI题目格式。随后,我会带你像在集训队上课一样,一步步拆解思路。
第一部分:题目描述
题目名称:验证回文串 (Valid Palindrome)
【题目描述】
给定一个字符串 。如果我们在将所有大写字符转换为小写字符,并移除所有非字母数字字符之后,该字符串读起来正着读和反着读是一样的,那么该字符串就是一个回文串。
其中,字母数字字符包括字母(a-z, A-Z)和数字(0-9)。
请你需要编写一个程序,判断给定的字符串 是否是回文串。如果是,输出 true,否则输出 false。
【输入格式】 输入包含一行,为一个字符串 。字符串中可能包含空格、标点符号、字母和数字。
【输出格式】
输出一行,如果 是回文串,输出 true,否则输出 false。
【样例 1】 输入:
A man, a plan, a canal: Panama
输出:
true
说明:在移除非字母数字字符并进行大小写转换后,字符串变为 "amanaplanacanalpanama",这是一个回文串。
【样例 2】 输入:
race a car
输出:
false
说明:处理后的字符串为 "raceacar",这不是一个回文串。
【样例 3】 输入:
注意:这里输入只有一个空格 输出:
true
说明:在移除非字母数字字符之后,字符串为空字符串 ""。由于空字符串正着反着读都一样,因此它是回文串。
【数据范围与提示】
- 字符串 由可打印的 ASCII 字符组成。
- 请注意:在C++中读取带空格的字符串建议使用
getline。
第二部分:预备知识点总结
在解决这道题之前,你需要掌握以下基础:
- 字符串基础操作:字符串的输入(特别是带空格的输入)、遍历、长度获取。
- ASCII码与字符判断:
- 如何判断一个字符是字母或数字?(常用函数
isalnum或手动比较 ASCII 范围)。 - 如何进行大小写转换?(常用函数
tolower或位运算/加减操作)。
- 如何判断一个字符是字母或数字?(常用函数
- 双指针算法 (Two Pointers):这是本题达到最优空间复杂度的核心思想(对撞指针)。
- 模拟:按照题目要求一步步执行逻辑的能力。
第三部分:读题关键词分析
做题的第一步是审题。看到这道题,你应该立刻圈出以下关键词:
- “大写转换为小写” 暗示我们需要做统一化处理,通常统一转为小写比较方便。
- “移除所有非字母数字字符” 意味着原字符串中的空格、逗号、冒号等都是“噪音”,处理时需要跳过或过滤。
- “字母数字字符” 这是一个坑点,很多人只保留了字母,忘记了数字也是有效字符(比如 "0P0" 也是回文)。
- “正着读和反着读一样” 这是回文串的定义,意味着 。
第四部分:启发式教学与草稿纸推演
好了,同学们,请拿起你们的草稿纸和笔。不要急着写代码,我们先画图。
方案一:直观模拟法(筛选 + 判断)
引导思考: 如果题目太乱,我们是不是可以先把“干净”的字符串提取出来?
草稿纸推演:
假设输入是:"A man, a plan!"
-
第一步(清洗): 准备一个新容器(比如
string clean_s)。- 遇到 'A' -> 是字母 -> 转小写 'a' -> 放入
clean_s - 遇到 ' ' -> 丢弃
- 遇到 'm' -> 放入...
- ...
- 结果:
clean_s变成了"amanaplan"
- 遇到 'A' -> 是字母 -> 转小写 'a' -> 放入
-
第二步(翻转比较):
- 把
clean_s翻转得到reverse_s。 - 比较
clean_s == reverse_s? - 或者,用循环对比
clean_s的头尾。
- 把
- 提问:这个方法好想,但是有什么缺点?
- 学生回答:需要开一个新的字符串。
- 教练点评:没错!如果字符串很长(),你需要额外的 空间。在内存限制紧张的题目里,这不够优雅。
方案二:双指针原位操作(最优解)
引导思考: 能不能不申请新空间,直接在原来的字符串上“跳着”比较?想象一下,你的左手食指指着字符串开头,右手食指指着结尾。
草稿纸推演:
输入:"A b, a"
初始状态:
left 指向下标 0 ('A')
right 指向下标 5 ('a')
第一轮循环:
- 左手动作:
s[left]是 'A',是有效字符吗?是。停下,转小写变为 'a'。 - 右手动作:
s[right]是 'a',是有效字符吗?是。停下,转小写变为 'a'。 - 比较:左手的 'a' 等于 右手的 'a' 吗?
- 相等!继续。
left往右移,right往左移。
第二轮循环:
此时 left 指向 1 (' '),right 指向 4 (',')。
- 左手动作:
s[left]是空格。无效!跳过,left++。- 现在
left指向 2 ('b')。是有效字符,停下。
- 现在
- 右手动作:
s[right]是逗号。无效!跳过,right--。- 现在
right指向 3 (' ')。还是空格,无效!跳过,right--。 - 现在
right指向 2 ('b')。是有效字符,停下。
- 现在
- 比较:
s[2]('b') 等于s[2]('b') 吗?- 相等。
结束条件: left >= right,相遇了,说明检查完毕,是回文串。
图解逻辑:
L -> <- R
[ A m a n , a p l a n ]
| |
有效(a) 有效(n)
不相等? -> return false
相等? -> L++, R-- 继续
复杂度分析:
- 时间复杂度:。两个指针加起来总共把字符串扫了一遍。
- 空间复杂度:。我们只用了两个变量
left和right,没有申请大数组。这是NOI金牌选手应该追求的写法。
第五部分:NOI标准 C++14 代码实现
以下代码完全符合NOI竞赛规范,使用了标准输入输出流。
/*
* Problem: Valid Palindrome
* Standard: C++14 (NOI compatible)
* Time Complexity: O(N)
* Space Complexity: O(1)
*/
#include <iostream>
#include <string>
#include <cctype> // 用于 isalnum, tolower
using namespace std;
// 判断是否为回文串的核心函数
bool isPalindrome(string s) {
int n = s.length();
if (n == 0) return true; // 空串视为回文
int left = 0;
int right = n - 1;
while (left < right) {
// 1. 左指针向右移动,跳过非字母数字字符
// 注意:必须保证 left < right,防止越界
while (left < right && !isalnum(s[left])) {
left++;
}
// 2. 右指针向左移动,跳过非字母数字字符
while (left < right && !isalnum(s[right])) {
right--;
}
// 3. 比较字符
// 如果都跳过完了还在范围内,开始比较
if (left < right) {
// 统一转换为小写进行比较
if (tolower(s[left]) != tolower(s[right])) {
return false; // 发现不匹配,立即返回 false
}
// 匹配成功,指针继续内缩
left++;
right--;
}
}
return true; // 全部通过,是回文串
}
int main() {
// 优化 I/O 速度,养成好习惯
ios::sync_with_stdio(false);
cin.tie(0);
string s;
// 关键点:题目输入可能包含空格,不能用 cin >> s,必须用 getline
getline(cin, s);
if (isPalindrome(s)) {
cout << "true" << endl;
} else {
cout << "false" << endl;
}
return 0;
}
总结提示
- 输入陷阱:在NOI竞赛中,如果题目输入是一整行且包含空格,千万别用
cin >> s,它遇到空格就停了。一定要用getline(cin, s)。 - 边界条件:
while循环内部寻找有效字符时,一定要加上left < right这个条件,否则对于全是非法字符的串(比如",,,,"),指针可能会越界访问导致 Runtime Error (RE)。 - 函数利用:熟练掌握
<cctype>库里的isalnum()(is alphanumeric) 和tolower()能大大减少你的代码量和出错率。如果没有库函数,你需要手写if ((c>='a' && c<='z') || ...),容易写错。
希望这个解题思路能帮到你,快去试试Coding吧!