想系统提升编程能力、查看更完整的学习路线,欢迎访问 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
易错点 ⚠️
-
忘记跳过非字母数字字符
- ❌ 错误:直接对比所有字符,导致标点符号影响判断
- ✅ 正确:用
while left < right and not s[left].isalnum()跳过
-
忘记忽略大小写
- ❌ 错误:
s[left] != s[right]直接对比,导致'A'和'a'被判定为不相等 - ✅ 正确:
s[left].lower() != s[right].lower()
- ❌ 错误:
-
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. 验证回文串II | Easy | 双指针+贪心 | 允许删除一个字符,判断是否能构成回文 |
| 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] 时,有两种选择:
- 删除左边字符,检查
s[left+1...right]是否为回文 - 删除右边字符,检查
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 学习资料都在这里,后续复习和拓展会更省时间。