验证回文串:跳过杂质,两头对碰
只比字母和数字,大小写当相同。指针停在逗号或空格上不算失败,先跳过再比。
有效字符是字母与数字,比较前统一成小写。可以先清洗成新串再双指针,也可以在原串上左右夹,遇到非字母数字就移动该侧。一旦两有效字符不等返回 false,指针交错则 true。时间 O(n);先清洗额外 O(n) 空间,原地跳过是 O(1)。
这是 LeetCode 125. Valid Palindrome。判断字符串是不是回文:只看字母和数字,忽略大小写,空格、标点、其它符号都不参与比较。空串、洗完为空,都算回文。
主例 "A man, a plan, a canal: Panama" 是回文——洗完是 amanaplanacanalpanama,正反相同。对照 "race a car" 洗完 racecar 差一个字母,中间的 e 对不上 a,不是回文。
直接把原串当普通回文去夹,会在第一对 "A" 和 "a" 之前先撞上冒号和空格,误判失败。缺的是「什么才算一对」。本文先把有效字符提出来,再两头对碰;原地做法只是把清洗折进移动指针的过程。
先定「有效字符」
题目把回文的字母表收窄了:unicode 字母、十进制数字算有效,其余跳过;大写与小写视为同一个字符。主例里的空格、逗号、冒号全部丢掉,"A" 和 "P" 先转成 "a" 和 "p"。
第一直觉是先建一个只含有效小写字符的新串。主例洗完是 amanaplanacanalpanama,长度 21,演示两帧从原句到这串。空间 O(n),但「要比的序列」变得看得见,后面的双指针不再分叉。
若要求额外空间 O(1),不要建新串:左右指针在原串上走,每次先 while 跳过无效字符,再比 ToLower 之后的值。判定规则与先清洗完全相同,只是跳过发生在比较之前的那一刻。
双指针:两端对碰
清洗之后,left 在 0,right 在最后一格。每一对只问 s[left] 是否等于 s[right]:不等立刻 false;相等则 left++、right--。left ≥ right 时中间已经空或只剩一个字符,回文成立。
回文的本质是沿中点对称。从两端成对验证,比先反转再比整串少一次分配,失败也可以提前停。主例长度 21,中点是下标 10 的 c,它没有配对义务。
用手走主例,对应三帧。第一对下标 0 与 20,a 对 a。跳过中间若干对之后,演示停在 5 与 15,仍是 p 对 p。再收到 9 与 11,n 对 n,下一步 left 与 right 交错,判定 true。"race a car" 洗成 raceacar,第一对 r 对 r,收到 c 对 a 时失败。
对称回文就是沿中点对称。双指针从两端对碰,每步验证一对对称位置。杂质必须先从这对位置里拿掉,否则比的不是题目要的字符。
九行 Go:清洗 + 双指针
func isPalindrome(s string) bool {t := make([]rune, 0, len(s))for _, r := range s {if unicode.IsLetter(r) || unicode.IsDigit(r) {t = append(t, unicode.ToLower(r))}}for i, j := 0, len(t)-1; i < j; i, j = i+1, j-1 {if t[i] != t[j] { return false }}return true}
1按 rune 扫,避免把多字节字母拆开。只收 IsLetter 或 IsDigit。
2收进来立刻 ToLower。主例的 A 和末尾的 a 变成同一个 rune,第一对就能相等。
3i<j 时比 t[i] 与 t[j]。中间那个字符在奇数长度下不会进入比较。
4洗完为空时 j 起手是 −1,循环不进,返回 true,符合空串是回文。
总结
只比小写字母数字,两头对碰。主例洗成 amanaplanacanalpanama,对到中间都相等。
- 空格和标点不是失败,是「这一侧再走一格」。先清洗和原地跳过是同一条规则。
- 大小写必须先统一,否则主例第一对 A 与 a 会错成 false。
- "race a car" 洗完在 c 对 a 处失败,不是因为空格。