#LC125. 验证回文串

验证回文串

LeetCode125这道题是LeetCode上的经典题目“验证回文串”,虽然看似简单,但它考察了字符串处理的基本功和双指针思想。

为了让你适应NOI/NOIP的考试环境,我将这道题改写成了标准的OI题目格式。随后,我会带你像在集训队上课一样,一步步拆解思路。


第一部分:题目描述

题目名称:验证回文串 (Valid Palindrome)

【题目描述】 给定一个字符串 SS。如果我们在将所有大写字符转换为小写字符,并移除所有非字母数字字符之后,该字符串读起来正着读和反着读是一样的,那么该字符串就是一个回文串。 其中,字母数字字符包括字母(a-z, A-Z)和数字(0-9)。

请你需要编写一个程序,判断给定的字符串 SS 是否是回文串。如果是,输出 true,否则输出 false

【输入格式】 输入包含一行,为一个字符串 SS。字符串中可能包含空格、标点符号、字母和数字。

【输出格式】 输出一行,如果 SS 是回文串,输出 true,否则输出 false

【样例 1】 输入:

A man, a plan, a canal: Panama

输出:

true

说明:在移除非字母数字字符并进行大小写转换后,字符串变为 "amanaplanacanalpanama",这是一个回文串。

【样例 2】 输入:

race a car

输出:

false

说明:处理后的字符串为 "raceacar",这不是一个回文串。

【样例 3】 输入:

 

注意:这里输入只有一个空格 输出:

true

说明:在移除非字母数字字符之后,字符串为空字符串 ""。由于空字符串正着反着读都一样,因此它是回文串。

【数据范围与提示】

  • 1S2×1051 \le |S| \le 2 \times 10^5
  • 字符串 SS 由可打印的 ASCII 字符组成。
  • 请注意:在C++中读取带空格的字符串建议使用 getline

第二部分:预备知识点总结

在解决这道题之前,你需要掌握以下基础:

  1. 字符串基础操作:字符串的输入(特别是带空格的输入)、遍历、长度获取。
  2. ASCII码与字符判断
    • 如何判断一个字符是字母或数字?(常用函数 isalnum 或手动比较 ASCII 范围)。
    • 如何进行大小写转换?(常用函数 tolower 或位运算/加减操作)。
  3. 双指针算法 (Two Pointers):这是本题达到最优空间复杂度的核心思想(对撞指针)。
  4. 模拟:按照题目要求一步步执行逻辑的能力。

第三部分:读题关键词分析

做题的第一步是审题。看到这道题,你应该立刻圈出以下关键词:

  1. “大写转换为小写” \rightarrow 暗示我们需要做统一化处理,通常统一转为小写比较方便。
  2. “移除所有非字母数字字符” \rightarrow 意味着原字符串中的空格、逗号、冒号等都是“噪音”,处理时需要跳过或过滤。
  3. “字母数字字符” \rightarrow 这是一个坑点,很多人只保留了字母,忘记了数字也是有效字符(比如 "0P0" 也是回文)。
  4. “正着读和反着读一样” \rightarrow 这是回文串的定义,意味着 S[i]==S[len1i]S[i] == S[len-1-i]

第四部分:启发式教学与草稿纸推演

好了,同学们,请拿起你们的草稿纸和笔。不要急着写代码,我们先画图。

方案一:直观模拟法(筛选 + 判断)

引导思考: 如果题目太乱,我们是不是可以先把“干净”的字符串提取出来?

草稿纸推演: 假设输入是:"A man, a plan!"

  1. 第一步(清洗): 准备一个新容器(比如 string clean_s)。

    • 遇到 'A' -> 是字母 -> 转小写 'a' -> 放入 clean_s
    • 遇到 ' ' -> 丢弃
    • 遇到 'm' -> 放入...
    • ...
    • 结果:clean_s 变成了 "amanaplan"
  2. 第二步(翻转比较):

    • clean_s 翻转得到 reverse_s
    • 比较 clean_s == reverse_s
    • 或者,用循环对比 clean_s 的头尾。
  • 提问:这个方法好想,但是有什么缺点?
    • 学生回答:需要开一个新的字符串。
    • 教练点评:没错!如果字符串很长(2×1052 \times 10^5),你需要额外的 O(N)O(N) 空间。在内存限制紧张的题目里,这不够优雅。

方案二:双指针原位操作(最优解)

引导思考: 能不能不申请新空间,直接在原来的字符串上“跳着”比较?想象一下,你的左手食指指着字符串开头,右手食指指着结尾。

草稿纸推演: 输入:"A b, a"

初始状态: left 指向下标 0 ('A') right 指向下标 5 ('a')

第一轮循环:

  1. 左手动作s[left] 是 'A',是有效字符吗?是。停下,转小写变为 'a'。
  2. 右手动作s[right] 是 'a',是有效字符吗?是。停下,转小写变为 'a'。
  3. 比较:左手的 'a' 等于 右手的 'a' 吗?
    • 相等!继续。
    • left 往右移,right 往左移。

第二轮循环: 此时 left 指向 1 (' '),right 指向 4 (',')。

  1. 左手动作s[left] 是空格。无效!跳过left++
    • 现在 left 指向 2 ('b')。是有效字符,停下。
  2. 右手动作s[right] 是逗号。无效!跳过right--
    • 现在 right 指向 3 (' ')。还是空格,无效!跳过right--
    • 现在 right 指向 2 ('b')。是有效字符,停下。
  3. 比较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-- 继续

复杂度分析:

  • 时间复杂度O(N)O(N)。两个指针加起来总共把字符串扫了一遍。
  • 空间复杂度O(1)O(1)。我们只用了两个变量 leftright,没有申请大数组。这是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;
}

总结提示

  1. 输入陷阱:在NOI竞赛中,如果题目输入是一整行且包含空格,千万别用 cin >> s,它遇到空格就停了。一定要用 getline(cin, s)
  2. 边界条件while 循环内部寻找有效字符时,一定要加上 left < right 这个条件,否则对于全是非法字符的串(比如 ",,,,"),指针可能会越界访问导致 Runtime Error (RE)。
  3. 函数利用:熟练掌握 <cctype> 库里的 isalnum() (is alphanumeric) 和 tolower() 能大大减少你的代码量和出错率。如果没有库函数,你需要手写 if ((c>='a' && c<='z') || ...),容易写错。

希望这个解题思路能帮到你,快去试试Coding吧!