6. Zigzag Conversion
題目 / Problem
中文:
將字串 s 以鋸齒形(zigzag)方式寫在 numRows 列上,然後一列一列地讀取,回傳結果字串。
例如 "PAYPALISHIRING",numRows = 3 時排列如下:
P A H N
A P L S I I G
Y I R
一列一列讀取得到 "PAHNAPLSIIGYIR"。
請實作函式:
string convert(string s, int numRows);
English:
Write the string s in a zigzag pattern on numRows rows, then read it line by line to produce the output.
For s = "PAYPALISHIRING", numRows = 3, the zigzag layout is shown above, and reading row by row gives "PAHNAPLSIIGYIR".
Constraints / 限制:
- 1 <= s.length <= 1000
- s 由大小寫英文字母、','、'.' 組成
- 1 <= numRows <= 1000
Worked example / 範例:
- Input: s = "PAYPALISHIRING", numRows = 4
- Output: "PINALSIGYAHRPI"
- Layout:
P I N
A L S I G
Y A H R
P I
名詞解釋 / Glossary
- Zigzag pattern / 鋸齒形排列:字元先由上往下填入每一列,到最後一列後再斜向上回到第一列,如此循環。Characters fill rows top-down, then diagonally back up to the top, repeating.
- Row buffer / 列緩衝區:為每一列準備一個獨立的字串容器,用來收集屬於該列的字元,最後再依序串接起來。An independent string container for each row that collects its characters; concatenated at the end.
- Direction flag / 方向旗標:一個布林或整數值(例如
+1/-1),用來記錄目前是「往下走」還是「往上走」,遇到頂列或底列時翻轉。A boolean/integer (+1/-1) tracking whether we are moving down or up; flipped at the top or bottom row. - Edge case / 邊界情況:輸入特別小或退化時的特殊處理,例如
numRows == 1時根本沒有鋸齒,應直接回傳原字串。Special handling for degenerate inputs (e.g.numRows == 1means no zigzag at all). malloc/free(C):C 語言中向作業系統申請與釋放動態記憶體的函式。LeetCode 的 C 題目通常要求回傳malloc出來的字串。Standard C calls to dynamically allocate / release heap memory; LeetCode C problems usually expect amalloc-returned string.std::string/std::vector(C++):C++ 標準函式庫中的可變長度字串與動態陣列,會自動管理記憶體。C++ standard-library types for resizable strings and arrays that manage memory automatically.- Amortized O(n):雖然單一字串拼接可能擴容,但整體加起來仍是線性時間。Although individual appends may resize, the total work across all appends is still linear.
思路
最直觀的暴力法是真的開一個二維字元矩陣,模擬鋸齒形把字元一個個放進去,最後再從上到下、從左到右掃描矩陣、跳過空格輸出。這當然會得到正確答案,但需要 numRows * n 的空間(其中 n = s.length()),而且大部分格子都是空白,浪費很多記憶體,也得處理「哪些格子是空的」這種噁心的細節。觀察輸出規律可以發現:每個字元最終一定屬於某一列,而它屬於哪一列只取決於我們走到它時的「目前列索引」。所以我們其實只要為每一列各自維護一個字串緩衝區,依序遍歷 s,把每個字元 append 到對應列的緩衝區,再把所有緩衝區依序串起來即可。關鍵在於「目前列」是怎麼變化的:從第 0 列開始,先一路往下走到第 numRows - 1 列,再翻轉方向往上走到第 0 列,如此往復——這正是鋸齒的本質。我們用一個方向變數(+1 表示往下、-1 表示往上)來追蹤,每當碰到頂列或底列就翻轉方向。特別注意 numRows == 1 時方向永遠不會翻轉(頂底是同一列),會卡死,所以要先特判直接回傳原字串。
The brute-force idea is to literally allocate a 2D character grid, simulate the zigzag walk to fill characters cell by cell, then scan the grid row by row skipping blanks. It works but uses numRows * n space (where n = s.length()), wastes most cells on blanks, and forces you to handle "is this cell empty?" logic. The key insight is that every character ends up in exactly one row, and which row it lands in depends only on the current row index as we walk the zigzag. So we maintain one string buffer per row, iterate s once, append each character to its row's buffer, and finally concatenate all buffers in order. The current-row index moves like a bouncing ball: it starts at 0, increases by 1 until it hits numRows - 1, then decreases by 1 until it returns to 0, and repeats. We track this with a direction variable (+1 for down, -1 for up) and flip it whenever we touch the top or bottom row. Watch out for numRows == 1: the top and bottom rows coincide, the direction would never flip meaningfully, and we'd index out of range — so we short-circuit and return s unchanged.
逐步走查 / Walkthrough
Trace s = "PAYPALISHIRING", numRows = 3. We keep three row buffers R0, R1, R2, a current row cur, and a direction dir (+1 = down, -1 = up).
| step | char | cur before | dir before | action / 動作 | R0 | R1 | R2 | cur after | dir after |
|---|---|---|---|---|---|---|---|---|---|
| 0 | P | 0 | +1 | append P to R0; cur==0 → set dir=+1 | P | 1 | +1 | ||
| 1 | A | 1 | +1 | append A to R1 | P | A | 2 | +1 | |
| 2 | Y | 2 | +1 | append Y to R2; cur==numRows-1 → flip dir=-1 | P | A | Y | 1 | -1 |
| 3 | P | 1 | -1 | append P to R1 | P | AP | Y | 0 | -1 |
| 4 | A | 0 | -1 | append A to R0; cur==0 → flip dir=+1 | PA | AP | Y | 1 | +1 |
| 5 | L | 1 | +1 | append L to R1 | PA | APL | Y | 2 | +1 |
| 6 | I | 2 | +1 | append I to R2; flip dir=-1 | PA | APL | YI | 1 | -1 |
| 7 | S | 1 | -1 | append S to R1 | PA | APLS | YI | 0 | -1 |
| 8 | H | 0 | -1 | append H to R0; flip dir=+1 | PAH | APLS | YI | 1 | +1 |
| 9 | I | 1 | +1 | append I to R1 | PAH | APLSI | YI | 2 | +1 |
| 10 | R | 2 | +1 | append R to R2; flip dir=-1 | PAH | APLSI | YIR | 1 | -1 |
| 11 | I | 1 | -1 | append I to R1 | PAH | APLSII | YIR | 0 | -1 |
| 12 | N | 0 | -1 | append N to R0; flip dir=+1 | PAHN | APLSII | YIR | 1 | +1 |
| 13 | G | 1 | +1 | append G to R1 | PAHN | APLSIIG | YIR | 2 | +1 |
Concatenate R0 + R1 + R2 → "PAHN" + "APLSIIG" + "YIR" → "PAHNAPLSIIGYIR". ✅
Solution — C
// 演算法 / Algorithm:
// 為每一列開一個字串緩衝區,沿著鋸齒走訪字串 s,
// 把每個字元 append 到「目前列」的緩衝區,最後把所有列依序串接。
// Keep one buffer per row; walk s in zigzag order; append each char to
// the current row's buffer; concatenate all rows at the end.
#include <stdlib.h> // malloc, free — 動態記憶體 / dynamic memory
#include <string.h> // strlen, strcpy, memcpy — 字串操作 / string ops
char* convert(char* s, int numRows) {
int n = (int)strlen(s); // n 是輸入長度 / n is input length
// 邊界情況:只有一列時不會有鋸齒,直接回傳一份拷貝即可。
// Edge case: with 1 row there is no zigzag — return a copy of s.
// 也處理 numRows >= n 的情況(每個字元自己一列,順序不變)。
// Also covers numRows >= n (each char alone in its own row, order unchanged).
if (numRows == 1 || numRows >= n) {
char* copy = (char*)malloc((size_t)n + 1); // +1 給結尾的 '