验证回文串
先忽略空格和标点,把大小写统一,再让左右两位字符检查员从两端寻找下一对有效字符。
忽略非字母数字字符和大小写后,判断字符串是否正反相同。
在原字符串上完成过滤与比较,避免额外创建新字符串。
左右跳过无效字符,统一小写比较;相等收缩,不同立即失败。
先说结论:这道题到底解决什么
原字符串里混有空格、标点和大小写差异,怎样不创建过滤副本,就让左右指针每次都比较“有效字符流”最外侧的一对字符?
中心结论:左右跳过无效字符,统一小写比较;相等收缩,不同立即失败。
- 1.题目真正判断的是原字符串,还是过滤并统一大小写后的有效字符序列?
- 2.左右指针怎样安全跳过无效字符,并且不会在全标点输入上越界?
- 3.为什么每个字符只会被指针经过一次,双指针仍然是 O(n) 而不是 O(n²)?
完整题目与题意拆解
给定字符串 s,如果把所有大写字母转换成小写、删除所有非字母数字字符后,字符串正读和反读相同,就返回 true。
字母数字字符包括 a-z、A-Z 和 0-9。数字不能被当成标点跳过。
- • 空字符串或全部由标点组成的字符串返回 true。
- • 大小写不敏感,但大写字母仍参与比较。
- • 题目字符范围适合用 Go byte 处理 ASCII。
输入:s = "A man, a plan, a canal: Panama"
输出:true
解释:处理后得到 "amanaplanacanalpanama"。
输入:s = "race a car"
输出:false题目判断的不是原句表面,而是“保留字母数字、统一大小写”后的有效字符序列。
教学时可以先想象过滤后的字符串,但最优实现不需要真的创建它。双指针能在原字符串上边找边比较。
真正要比较的字符串是哪一个
原字符串与下方规范化有效流同步出现,标点淡出、字母统一小写,先建立准确的判断对象。
第一层方案:暴力做法
第一版可以遍历字符串,把所有字母数字转成小写后写入新切片,再用反转或双指针检查新切片。
它时间是 O(n),空间也是 O(n)。这是很好的题意验证版本,但面试通常期待原地双指针把额外空间降到 O(1)。
filtered := []byte{}
for each c in s:
if isAlnum(c): append(toLower(c))
return filtered == reverse(filtered)先构造新字符串的代价
演示把所有有效字符复制到新数组再比较的直观方案,并突出它额外占用与输入等长的空间。
优化方向:两端字符天然成对。只要指针能够跳过无效字符,就可以在原字符串上完成同样判断。
整体地图:先做什么,再做什么
先把判断对象从原字符串改写为“只保留字母和数字、忽略大小写后的有效字符流”。教学上可以想象先得到过滤串,但生产代码无需真的分配它。
左右指针从原字符串两端出发,各自跳过无效字符;找到有效字符后统一成小写比较。相同则同时收缩,不同立即失败,直到指针相遇。
- • 跳过无效字符是在原串上惰性构造有效流。
- • 数字也是有效字符,不能只判断字母。
- • 两侧跳过循环都必须带 l<r 边界。
在原字符串上惰性读取规范化后的有效字符流
如果显式过滤,可以得到一个只含字母数字且全部小写的新字符串,再检查它是否回文。这很直观,但需要 O(n) 额外空间。
双指针把过滤过程推迟到真正需要字符时:左指针寻找下一个有效字符,右指针寻找上一个有效字符。此时它们恰好对应剩余有效流最外侧的一对。
“A man, a plan, a canal: Panama” 的首轮有效比较是 A 与 a,而不是直接拿 A 与最后一个标点或空格比较。双指针惰性发现有效流两端
左右指针在原字符串上越过灰色无效字符,只在碰到有效字符时停下并形成一对。
左跳过、右跳过、规范化比较、同时收缩
外层循环保持 l<r。左侧 while 在 l<r 且 s[l] 不是字母数字时递增 l;右侧对称地递减 r。两个跳过循环结束后,再将 s[l] 与 s[r] 统一小写比较。
若不同立即返回 false;若相同则 l++、r--。循环结束说明有效流剩余零个或一个字符,天然满足回文,返回 true。
- • isAlnum 必须覆盖 a-z、A-Z 和 0-9。
- • toLower 只转换大写字母,数字和小写字母保持不变。
- • 每个指针单向移动,不会回头重复扫描。
- • 空字符串或全标点会让指针相遇并正确返回 true。
初始化 l=0、r=len(s)-1。只要 l<r,就分别跳过两端无效字符,然后比较 toLower(s[l]) 与 toLower(s[r])。
不同返回 false;相同则 l++、r--。循环结束表示所有需要比较的字符对都通过,返回 true。
跳过、比较、收缩的固定循环
四个动作按相同节奏重复,当前字符、规范化结果与指针下一位置始终同步显示。
为什么跳过和两端收缩不会改变答案
循环不变量是:已经越过的有效字符都已成对相等,l 与 r 之间包含尚未验证的有效字符流。跳过的标点和空格不属于有效流,所以移动指针不会改变待验证对象。
跳过结束后,s[l] 与 s[r] 是待验证有效流最外侧的一对。规范化后若不同,任何内部字符都无法修复最外侧不相等,立即返回 false 正确。
若相等,删除这对外侧字符后,原问题等价于更短的内部有效流。不断收缩到零或一个字符时,剩余部分必为回文。
- • 跳过循环移除的字符不属于题目定义的有效序列,因此不会改变答案。
- • 每轮比较的正是剩余有效序列最外侧的一对字符。
- • 相等后去掉这一对保持问题等价;不同则必定不是回文。
完整执行过程
短样例只保留 8 个字符,让每一步移动都可见。请重点观察 L/R 的旧位置和新位置,以及被跳过字符为什么不参与比较。
- 1l 指向开头 A,r 从末尾开始跳过无效字符,落在最后的 a。
- 2A 和 a 统一为小写后相等,两侧指针同时向中间移动。
- 3左侧依次跳过空格,右侧跳过标点与空格,再找到下一对有效字符。
- 4重复比较并收缩,所有外侧有效字符对都相等。
- 5指针相遇,未发现冲突,返回 true。
从外到内完成经典示例
完整播放有效字符对逐个匹配直到指针相遇,再切换到 race a car 展示第一次不匹配如何立即结束。
把动画和 Go 代码逐行对应
每一帧使用稳定的语义行 ID,不依赖容易漂移的数字行号。先观察分支为什么成立, 再看对应代码,而不是先背模板。
Go 边界判断如何避免越界
高亮两个带 l<r 的跳过循环、isAlnum 的数字分支和 toLower,让代码状态与指针位置逐步绑定。
题目只保留字母和数字,并忽略大小写。
指针先站在边界,再各自寻找下一位有效字符。
空格和感叹号不是字母数字,不影响处理后的字符序列。
忽略大小写不是跳过大写,而是转换到同一比较形式。
规则不会因字符位于字符串内部而改变,非字母数字始终不参与比较。
中间单个字符无需和自己比较;没有发现任何不匹配字符对。
回文要求所有镜像字符对相等,一处不同足以证明整串失败。
空序列正反相同,而 0 属于有效数字,不能忽略。
每个字符最多被指针经过一次,不创建过滤字符串。
完整 Go 提交代码与最小测试
1func isPalindrome(s string) bool {2 l, r := 0, len(s)-13 for l < r {4 for l < r && !isAlnum(s[l]) {5 l++6 }7 for l < r && !isAlnum(s[r]) {8 r--9 }10 if toLower(s[l]) != toLower(s[r]) {11 return false12 }13 l++14 r--15 }16 return true17}18 19func isAlnum(c byte) bool {20 return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9'21}22 23func toLower(c byte) byte {24 if c >= 'A' && c <= 'Z' {25 return c + ('a' - 'A')26 }27 return c28}// 忽略标点与大小写
fmt.Println(isPalindrome("A man, a plan, a canal: Panama")) // true
// 第一组不匹配字符决定失败
fmt.Println(isPalindrome("race a car")) // false
// 空有效流天然是回文
fmt.Println(isPalindrome(" ")) // true
// 数字必须参与比较
fmt.Println(isPalindrome("0P")) // false正确性与复杂度
左右指针只向中间移动,每个字符最多被检查常数次,不会来回扫描。
只保存 l、r 和少量 byte 变量,不创建与输入长度相关的新字符串。
最容易写错的地方
直接比较原字符串,没有跳过空格和标点。
isAlnum 忘记包含 0-9。
跳过循环漏掉 l < r,在全标点输入上越界。
大小写转换写死 32,降低可读性和可移植性。
把双指针移动误判成 O(log n)。
最后复盘:带走逻辑链
- 1.先分清有效字符,再讨论回文。
- 2.四步模板:左跳过、右跳过、比较、收缩。
- 3.空有效流和单字符天然为 true。
- 4.教学过滤串帮助理解,生产解法保持 O(1) 额外空间。
- 1.先说明判断对象:只保留字母数字并忽略大小写。
- 2.用左右指针在原字符串上扫描,各自跳过无效字符。
- 3.统一小写后比较,不同立即 false,相同一起向中间移动。
- 4.每个字符最多经过一次,时间 O(n),额外空间 O(1)。