📖 第22课:验证回文串

3 阅读13分钟

想系统提升编程能力、查看更完整的学习路线,欢迎访问 AI Compass:github.com/tingaicompa… 仓库持续更新刷题题解、Python 基础和 AI 实战内容,适合想高效进阶的你。

📖 第22课:验证回文串

模块:字符串 | 难度:Easy ⭐⭐ LeetCode 链接:leetcode.cn/problems/va… 前置知识:无(基础题) 预计学习时间:15分钟


🎯 题目描述

给你一个字符串 s,判断它是否为回文串。在验证时,只考虑字母和数字字符,并且忽略字母的大小写

示例 1:

输入:s = "A man, a plan, a canal: Panama"
输出:true
解释:去掉非字母数字字符并转为小写后为 "amanaplanacanalpanama",是回文串

示例 2:

输入:s = "race a car"
输出:false
解释:去掉非字母数字字符并转为小写后为 "raceacar",不是回文串

示例 3:

输入:s = " "
输出:true
解释:去掉非字母数字字符后为空字符串 "",空串认为是回文串

约束条件:

  • 1 <= s.length <= 2 * 10⁵
  • s 仅由可打印的 ASCII 字符组成

🧪 边界用例(面试必考)

用例类型输入期望输出考察点
空内容" "true空串是回文
单字符"a"true基本情况
纯符号".,;"true过滤后为空
大小写混合"Aa"true大小写忽略
数字字母混合"0P"false数字也算字符
长回文"A man...Panama"true示例1
最大长度n=200000性能边界

💡 思路引导

生活化比喻

想象你在检查一张纸上的文字是否对称,就像看"上海自来水来自海上"这种回文句。

🐌 笨办法:你先用橡皮擦把所有标点符号、空格都擦掉,再把大写字母全改成小写,最后抄写一份倒过来的版本,对比两份是否完全一样。这样要擦、要改、要抄,步骤太多了!

🚀 聪明办法:你用两根手指,一根从左边第一个有效字符开始,另一根从右边第一个有效字符开始,向中间靠拢。每次对比两个手指指向的字符(忽略大小写),如果不一样就立刻判定不是回文,如果一样就继续靠拢。关键是边走边跳过无效字符,不需要提前清理整张纸!

关键洞察

用双指针从两端向中间逼近,遇到非字母数字字符就跳过,同时比较时忽略大小写。


🧠 解题思维链

这一节模拟你在面试中"从零开始思考"的过程。

Step 1:理解题目 → 锁定输入输出

  • 输入:字符串 s,可能包含字母、数字、标点、空格
  • 输出:布尔值,判断是否为回文串
  • 限制:只看字母和数字,忽略大小写

Step 2:先想笨办法(预处理法)

先遍历一遍字符串,把所有字母和数字提取出来并转为小写,得到一个纯净的字符串 filtered。然后判断 filtered == filtered[::-1]

  • 时间复杂度:O(n) - 遍历一次 + 切片反转一次
  • 瓶颈在哪:需要额外O(n)空间存储过滤后的字符串

Step 3:瓶颈分析 → 优化方向

预处理法虽然简单,但需要额外空间。能不能原地判断?

  • 核心问题:"每次都要复制一份反转字符串来对比"
  • 优化思路:"用双指针直接在原字符串上对比,遇到非字母数字就跳过"

Step 4:选择武器

  • 选用:对撞双指针
  • 理由:回文串的特点就是"两端对称",双指针天然适合这种场景

🔑 模式识别提示:当题目要求判断"对称性"、"回文"时,优先考虑"对撞双指针"


🔑 解法一:预处理 + 双指针(简单直接)

思路

先过滤出所有字母和数字并转为小写,得到纯净字符串,然后用双指针或直接反转对比。

图解过程

示例: s = "A man, a plan, a canal: Panama"

Step 1: 过滤并转小写
  原字符串: "A man, a plan, a canal: Panama"
  过滤后:   "amanaplanacanalpanama"

Step 2: 双指针对比(或直接反转对比)
  left = 0, right = 20

  a m a n a p l a n a c a n a l p a n a m a
  ↑                                       ↑
  left                                  right
  s[0]='a' == s[20]='a' ✓

  a m a n a p l a n a c a n a l p a n a m a
    ↑                                   ↑
    left                              right
  s[1]='m' == s[19]='m' ✓

  ... 继续对比 ...

  最终所有字符都相等 → 返回 true

Python代码

def is_palindrome_v1(s: str) -> bool:
    """
    解法一:预处理 + 双指针
    思路:先过滤字母数字并转小写,再双指针对比
    """
    # 过滤并转小写
    filtered = ''.join(c.lower() for c in s if c.isalnum())

    # 双指针对比
    left, right = 0, len(filtered) - 1
    while left < right:
        if filtered[left] != filtered[right]:
            return False
        left += 1
        right -= 1

    return True


# ✅ 测试
print(is_palindrome_v1("A man, a plan, a canal: Panama"))  # 期望输出: True
print(is_palindrome_v1("race a car"))                       # 期望输出: False
print(is_palindrome_v1(" "))                                # 期望输出: True

复杂度分析

  • 时间复杂度:O(n) - 过滤一遍 O(n) + 对比一遍 O(n/2)
    • 具体地说:如果输入规模 n=1000,大约需要 1500 次操作
  • 空间复杂度:O(n) - 存储过滤后的字符串

优缺点

  • ✅ 代码简洁,易于理解
  • ✅ 逻辑清晰,不容易出错
  • ❌ 需要额外O(n)空间(面试官可能追问"能否O(1)空间?")

⚡ 解法二:原地双指针(空间优化)

优化思路

不预处理,直接在原字符串上用双指针。遇到非字母数字字符时,跳过该位置(left右移或right左移),只对比有效字符。

💡 关键想法:边走边跳过无效字符,不需要额外空间

图解过程

示例: s = "A man, a plan, a canal: Panama"

初始化: left=0, right=30

Step 1: left指向'A', right指向'a'
  A  m  a  n  ,     a     p  l  a  n  ,     a  ...
  ↑                                              ↑
  left                                        right
  'A'.lower() == 'a' ✓ → left++, right--

Step 2: left指向' '(空格,非字母数字), right指向'm'
  A     m  a  n  ,     a  ...
     ↑                                        ↑
    left                                   right
  left指向非字母数字 → left++

Step 3: left指向'm', right指向'm'
  A     m  a  n  ,     a  ...
        ↑                                  ↑
       left                              right
  'm'.lower() == 'm' ✓ → left++, right--

  ... 继续处理 ...

最终 left >= right → 返回 true

Python代码

def is_palindrome(s: str) -> bool:
    """
    解法二:原地双指针(推荐)
    思路:双指针从两端靠拢,跳过非字母数字,对比时忽略大小写
    """
    left, right = 0, len(s) - 1

    while left < right:
        # 跳过左边的非字母数字字符
        while left < right and not s[left].isalnum():
            left += 1

        # 跳过右边的非字母数字字符
        while left < right and not s[right].isalnum():
            right -= 1

        # 对比当前字符(忽略大小写)
        if s[left].lower() != s[right].lower():
            return False

        left += 1
        right -= 1

    return True


# ✅ 测试
print(is_palindrome("A man, a plan, a canal: Panama"))  # 期望输出: True
print(is_palindrome("race a car"))                       # 期望输出: False
print(is_palindrome(" "))                                # 期望输出: True
print(is_palindrome("0P"))                               # 期望输出: False

复杂度分析

  • 时间复杂度:O(n) - 每个字符最多访问一次
  • 空间复杂度:O(1) - 只用了两个指针变量

🐍 Pythonic 写法

利用 Python 的列表推导和切片反转:

def is_palindrome_pythonic(s: str) -> bool:
    """Pythonic 一行写法"""
    # 过滤并转小写,然后对比正反是否相同
    filtered = ''.join(c.lower() for c in s if c.isalnum())
    return filtered == filtered[::-1]

或者更极致的一行:

def is_palindrome_oneliner(s: str) -> bool:
    """极致一行写法"""
    return (filtered := ''.join(c.lower() for c in s if c.isalnum())) == filtered[::-1]

解释:

  • c.isalnum():判断字符是否为字母或数字
  • c.lower():转为小写
  • filtered[::-1]:字符串反转
  • :=:海象运算符(Python 3.8+),在表达式中赋值

⚠️ 面试建议:先写清晰版本(解法二)展示思路,再提 Pythonic 写法(解法一)展示语言功底。 面试官更看重你的思考过程。一行流虽然简洁,但不易调试和解释。


📊 解法对比

维度解法一:预处理解法二:原地双指针Pythonic一行
时间复杂度O(n)O(n)O(n)
空间复杂度O(n)O(1) ⭐O(n)
代码难度简单简单极简
面试推荐⭐⭐⭐⭐⭐
适用场景快速实现,逻辑清晰追求空间优化展示语言特性

面试建议:优先讲解法二(原地双指针),因为它空间最优且逻辑清晰。如果时间充裕,可以先说"最简单的思路是预处理(解法一)",然后主动提出"我们可以优化空间到O(1)"并实现解法二。


🎤 面试现场

模拟面试中的完整对话流程,帮你练习"边想边说"。

面试官:请你判断一个字符串是否为回文串,只考虑字母和数字,忽略大小写。

:(审题10秒)好的,我理解了。举个例子,"A man, a plan, a canal: Panama" 去掉标点空格后是 "amanaplanacanalpanama",正反读都一样,所以是回文串。

我的思路是用双指针从两端向中间靠拢:

  • 左指针从左边开始,右指针从右边开始
  • 遇到非字母数字字符就跳过
  • 对比时忽略大小写
  • 如果发现不相等就返回false,全部相等就返回true

时间复杂度 O(n),空间复杂度 O(1)。

面试官:很好,请写一下代码。

:(边写边说)我用两个while循环分别跳过左右两边的非字母数字字符,然后用 lower() 方法转小写后对比...

(写完代码)

面试官:测试一下?

:用示例 "A man, a plan, a canal: Panama" 走一遍:

  • left从'A'开始,right从'a'开始,转小写后都是'a',相等
  • 继续...最后left >= right,返回true

再测一个边界情况 " "(只有空格):

  • left和right都会因为空格而不断右移/左移,最终 left >= right,返回true

结果正确!

高频追问

追问应答策略
"如果不忽略大小写呢?"去掉 .lower() 即可,直接对比 s[left] != s[right]
"如果要考虑所有字符(包括标点)呢?"去掉 isalnum() 判断,直接对比所有字符
"空间能否优化?"解法二已经是O(1)空间。如果用预处理法,可以说"我们不预处理,直接双指针就能做到O(1)"
"如果字符串特别长,如何优化?"可以提前判断:如果过滤后长度为0或1,直接返回true;也可以用多线程分段判断(工程优化,算法层面已经最优)

🎓 知识点总结

Python技巧卡片 🐍

# 技巧1:isalnum() — 判断字符是否为字母或数字
c = 'A'
c.isalnum()  # True
c = ','
c.isalnum()  # False

# 技巧2:lower() — 转小写
'A'.lower()  # 'a'
'1'.lower()  # '1' (数字不变)

# 技巧3:字符串切片反转
s = "abc"
s[::-1]  # "cba"

# 技巧4:列表推导 + join
s = "A1 B2"
''.join(c for c in s if c.isalnum())  # "A1B2"

# 技巧5:海象运算符 := (Python 3.8+)
if (n := len(s)) > 10:  # 在条件中赋值
    print(f"长度{n}大于10")

💡 底层原理(选读)

为什么双指针能判断回文?

回文串的定义是"正着读和反着读一样",等价于"第i个字符 == 第(n-1-i)个字符"(对所有i成立)。双指针从两端靠拢,本质上就是在验证这个对称性。当 left < right 时,我们不断对比 s[left]s[right],一旦发现不相等,说明对称性被破坏,立即返回false;如果所有对比都通过,说明满足回文定义。

isalnum() 的底层实现?

Python的 isalnum() 是C语言实现的,内部调用了标准库的 isalnum() 函数,它通过查表(ASCII码表)来判断字符是否属于字母或数字范围。时间复杂度为O(1)。

算法模式卡片 📐

  • 模式名称:对撞双指针(回文判断)
  • 适用条件:判断序列是否对称、回文、镜像
  • 识别关键词:"回文"、"对称"、"正反读一样"
  • 模板代码:
def is_palindrome_template(s: str) -> bool:
    """对撞双指针判断回文的通用模板"""
    left, right = 0, len(s) - 1

    while left < right:
        # 可选:跳过无效字符
        while left < right and not is_valid(s[left]):
            left += 1
        while left < right and not is_valid(s[right]):
            right -= 1

        # 对比(可能需要归一化,如转小写)
        if normalize(s[left]) != normalize(s[right]):
            return False

        left += 1
        right -= 1

    return True

易错点 ⚠️

  1. 忘记跳过非字母数字字符

    • ❌ 错误:直接对比所有字符,导致标点符号影响判断
    • ✅ 正确:用 while left < right and not s[left].isalnum() 跳过
  2. 忘记忽略大小写

    • ❌ 错误:s[left] != s[right] 直接对比,导致'A'和'a'被判定为不相等
    • ✅ 正确:s[left].lower() != s[right].lower()
  3. while循环的边界条件

    • ❌ 错误:while left < len(s) and not s[left].isalnum() 可能导致right越界
    • ✅ 正确:while left < right and not s[left].isalnum() 确保不越界

🏗️ 工程实战(选读)

这个算法思想在真实项目中的应用,让你知道"学了有什么用"。

  • 场景1:用户名/密码验证

    • 一些网站要求用户名必须是回文(如对称ID),或者检测弱密码时排除回文串(如"12321"太简单)
  • 场景2:DNA序列分析

    • 生物信息学中,某些DNA序列具有回文特性(如限制性内切酶识别位点),需要高效检测
  • 场景3:日志去重

    • 检测日志文件中是否有回文式的错误信息(可能是程序bug导致的重复输出)

🏋️ 举一反三

完成本课后,试试这些同类题目来巩固知识:

题目难度相关知识点提示
LeetCode 9. 回文数Easy双指针/数学不转字符串的情况下判断整数是否回文
LeetCode 680. 验证回文串IIEasy双指针+贪心允许删除一个字符,判断是否能构成回文
LeetCode 5. 最长回文子串Medium中心扩展/DP找出字符串中最长的回文子串(第18课)
LeetCode 647. 回文子串Medium中心扩展统计字符串中有多少个回文子串(第20课)
LeetCode 131. 分割回文串Medium回溯+DP将字符串分割成若干回文子串,返回所有方案

📝 课后小测

试试这道变体题,不要看答案,自己先想5分钟!

题目:给定一个字符串,判断删除最多一个字符后,能否构成回文串?(LeetCode 680简化版)

例如:"abca" → 可以删除'b'或'c',得到 "aca""aba",都是回文,返回true

💡 提示(实在想不出来再点开)

用双指针从两端靠拢,当发现 s[left] != s[right] 时,有两种选择:

  1. 删除左边字符,检查 s[left+1...right] 是否为回文
  2. 删除右边字符,检查 s[left...right-1] 是否为回文

只要其中一种能构成回文,就返回true

✅ 参考答案
def valid_palindrome_delete_one(s: str) -> bool:
    """
    双指针 + 递归辅助函数
    """
    def is_palindrome_range(left: int, right: int) -> bool:
        """辅助函数:检查子串是否为回文"""
        while left < right:
            if s[left] != s[right]:
                return False
            left += 1
            right -= 1
        return True

    left, right = 0, len(s) - 1

    while left < right:
        if s[left] != s[right]:
            # 尝试删除左边或右边
            return (is_palindrome_range(left + 1, right) or
                    is_palindrome_range(left, right - 1))
        left += 1
        right -= 1

    return True  # 本身就是回文


# 测试
print(valid_palindrome_delete_one("abca"))    # True
print(valid_palindrome_delete_one("abc"))     # False

核心思路:正常双指针,遇到不匹配时"试探性地删除一个字符",检查剩余部分是否为回文。

时间复杂度 O(n),空间复杂度 O(1)。


如果这篇内容对你有帮助,推荐收藏 AI Compass:github.com/tingaicompa… 更多系统化题解、编程基础和 AI 学习资料都在这里,后续复习和拓展会更省时间。