125. Valid Palindrome
題目 / Problem
中文:
給定一個字串 s,把所有大寫字母轉成小寫,並移除所有非字母與數字(non-alphanumeric)的字元之後,如果剩下的字串從左讀和從右讀完全相同,就稱為回文(palindrome)。請判斷 s 經過上述處理後是否為回文,是則回傳 true,否則回傳 false。
English:
A string s is a palindrome if, after lowercasing all letters and removing every non-alphanumeric character, it reads the same forwards as backwards. Alphanumeric characters are letters (a–z, A–Z) and digits (0–9). Return true if s is a palindrome under this rule, false otherwise.
Constraints:
- 1 <= s.length <= 2 * 10^5
- s consists only of printable ASCII characters.
Worked example:
- Input: s = "A man, a plan, a canal: Panama"
- After cleaning: "amanaplanacanalpanama"
- Reversed: "amanaplanacanalpanama" — same string → Output: true.
名詞解釋 / Glossary
- Palindrome / 回文:A string that reads the same forwards and backwards, e.g.
"aba","1221". 從左讀和從右讀完全一樣的字串。 - Alphanumeric / 字母數字字元:The 62 characters
a–z,A–Z,0–9. Anything else (space, punctuation, symbols) is non-alphanumeric. C 提供isalnum()函式來檢查。 - Two pointers / 雙指針:A technique where two index variables walk through the array/string — usually one from the left and one from the right, moving toward each other. It lets us compare or process pairs in O(n) without extra memory. 比起先建立新字串再比較,雙指針可以原地(in-place)完成檢查。
- In-place / 原地處理:Solving the problem without allocating an additional array/string proportional to the input size. 我們只用幾個 index 變數,不另外配置 O(n) 的記憶體。
isalnum(c)/tolower(c):Standard library helpers from<ctype.h>(C) and<cctype>(C++).isalnumreturns non-zero ifcis a letter or digit;tolowerconverts an uppercase letter to lowercase (and leaves other characters unchanged). 注意:傳入的字元最好先轉成unsigned char,否則對某些非 ASCII 值會有 undefined behavior。- ASCII:A 7-bit character encoding where each character is a number 0–127.
'A' = 65,'a' = 97,相差 32,所以小寫化也可以用位元運算c | 32,但本題用tolower較清楚。
思路
中文:
最直觀(brute force)的做法是:先用一個迴圈把 s 中所有非字母數字字元濾掉、並把大寫轉成小寫,得到一個乾淨字串 t;接著再把 t 反轉得到 r,比較 t == r。這個做法是對的,但需要額外 O(n) 的空間存 t 和 r,對學習雙指針技巧也沒有幫助。
更好的做法是用雙指針:設 left = 0、right = n - 1,兩個指針同時往中間移動。每次比較前,先讓 left 一直右移、跳過所有非字母數字字元,再讓 right 一直左移、跳過所有非字母數字字元——這樣兩個指針指到的就一定是「有效字元」。然後把兩個字元都轉成小寫並比較:若不相等就立刻回傳 false;若相等就 left++、right--,繼續下一輪。當 left >= right 時,所有對稱位置都檢查完畢,回傳 true。
為什麼這樣是正確的?因為「跳過非字母數字字元」等價於先建立乾淨字串 t,而「比較 t[i] 和 t[n-1-i]」正好就是雙指針在做的事,只是我們不另外開一塊記憶體。時間仍然是 O(n),但空間降到 O(1)。
English: The brute-force idea is to build a cleaned-up copy of the string — keep only alphanumeric characters, lowercase them — and then check whether that cleaned string equals its reverse. It works but uses O(n) extra space and doesn't teach the core technique this problem is meant to teach.
The optimal approach is two pointers. Start with left = 0 and right = n - 1. On each step, first skip non-alphanumeric characters by advancing left rightwards and right leftwards until both point at valid characters. Then lowercase both characters and compare them. If they differ, the string can't be a palindrome — return false immediately. If they match, move both pointers inwards (left++, right--) and repeat. When left meets or passes right, every symmetric pair has been verified, so return true.
This is correct because skipping non-alphanumeric characters is logically the same as constructing the cleaned string, and comparing characters from both ends is exactly the palindrome check on that cleaned string — we just avoid materializing it. Time stays O(n) (each character is visited at most once by each pointer), and space drops to O(1).
逐步走查 / Walkthrough
Input: s = "A man, a plan, a canal: Panama", length n = 30.
Indices 0..29. L = left, R = right.
| Step | L | R | s[L] | s[R] | Action / 動作 |
|---|---|---|---|---|---|
| 1 | 0 | 29 | 'A' |
'a' |
Both alnum. tolower → 'a' == 'a'. ✓ L++, R-- |
| 2 | 1 | 28 | ' ' |
'm' |
s[L] not alnum → L++ |
| 3 | 2 | 28 | 'm' |
'm' |
'm' == 'm'. ✓ L++, R-- |
| 4 | 3 | 27 | 'a' |
'a' |
'a' == 'a'. ✓ L++, R-- |
| 5 | 4 | 26 | 'n' |
'n' |
'n' == 'n'. ✓ L++, R-- |
| 6 | 5 | 25 | ',' |
'a' |
s[L] not alnum → L++ |
| 7 | 6 | 25 | ' ' |
'a' |
s[L] not alnum → L++ |
| 8 | 7 | 25 | 'a' |
'a' |
'a' == 'a'. ✓ L++, R-- |
| 9 | 8 | 24 | ' ' |
'P' |
s[L] not alnum → L++ |
| 10 | 9 | 24 | 'p' |
'P' |
tolower → 'p' == 'p'. ✓ L++, R-- |
| ... | ... | ... | ... | ... | (the pattern continues — every matched pair shrinks the window) |
| final | 15 | 14 | — | — | L > R → loop ends → return true |
In total each character is touched at most twice (once when a pointer arrives, once when it leaves), so the work is linear in n.
Solution — C
// Algorithm / 演算法:
// Two pointers from both ends. Skip non-alphanumeric characters,
// compare lowercase forms, and shrink inward.
// 雙指針從兩端往中間走,跳過非字母數字,比較小寫後相等就繼續,遇到不等立即回傳 false。
// Time O(n), Space O(1).
#include <stdbool.h> // bool, true, false 型別 / boolean type
#include <ctype.h> // isalnum, tolower 字元判斷與轉換 / char classification helpers
#include <string.h> // strlen 取得字串長度 / string length
bool isPalindrome(char* s) {
int left = 0; // 左指針從字串開頭 / left pointer at start
int right = (int)strlen(s) - 1; // 右指針從字串結尾 (strlen 不含 '