LeetCode 第 6 题「Z 字形变换」看着像矩阵操作,但真画个二维数组就掉坑里了------用 StringBuilder 数组按行收集,代码短、逻辑清、O(n) 一把过。
题目
将一个给定字符串
s根据给定的行数numRows,以从上往下、从左到右进行 Z 字形排列,然后按行读出。示例 1 :
s = "PAYPALISHIRING", numRows = 3
P A H N A P L S I I G Y I R输出:
"PAHNAPLSIIGYIR"示例 2 :
s = "PAYPALISHIRING", numRows = 4
P I N A L S I G Y A H R P I输出:
"PINALSIGYAHRPI"
思路分析
别画矩阵,按行装
第一反应可能是开一个 char[numRows][n] 的二维数组,把字符按 Z 字形填进去,再逐行读出来。这当然能做,但有两个问题:
- 列数不好算,得先推公式确定数组宽度
- 大部分格子是空的,浪费空间,很多遍历都是无用功
换个思路:最终答案就是按行拼接 ,那我们干脆只维护每一行装了什么字符。开 numRows 个 StringBuilder,遍历字符串,把每个字符追加到"当前行"里,用一个方向变量控制"当前行"是往上走还是往下走。
方向切换的时机
关键规律:碰到第一行(row == 0)或最后一行(row == numRows-1)时,方向反转。
- 向下走到底是最后一行时,应该掉头向上
- 向上走到顶是第一行时,应该掉头向下
用一个 dir 变量,+1 表示向下,-1 表示向上。每次碰到边界就取反(dir = -dir),然后 row += dir 走一步。
手动模拟
以 s = "PAYPALISHIRING", numRows = 3 为例,看看每个字符落到哪一行:
| 字符 | 当前 row | dir | 碰边界? | 加入行 | 下一步 row |
|---|---|---|---|---|---|
| P | 0 | +1 | row==0,反转→-1 | 第0行: P | 1 |
| A | 1 | -1 | --- | 第1行: A | 2 |
| Y | 2 | -1 | row==2,反转→+1 | 第2行: Y | 1 |
| P | 1 | +1 | --- | 第1行: AP | 0 |
| A | 0 | +1 | row==0,反转→-1 | 第0行: PA | 1 |
| L | 1 | -1 | --- | 第1行: APL | 2 |
| ... | ... | ... | ... | ... | ... |
注意:每次碰边界的那一轮,方向先反转,然后再走。也就是「判断边界 → 反转方向 → 移动行号 → 下一轮追加」。代码里把判断和反转放在追加之后、移动之前,效果一样------当前行追加完再决定下一步往哪走。
三行最终结果:
第0行: P A H N
第1行: A P L S I I G
第2行: Y I R
拼接:"PAHN" + "APLSIIG" + "YIR" = "PAHNAPLSIIGYIR" ✅
完整代码
java
class Solution {
public String convert(String s, int numRows) {
// 只有一行,Z 字形退化成一条直线,直接返回
if (numRows == 1) return s;
// 每行一个 StringBuilder,装属于这一行的字符
StringBuilder[] rows = new StringBuilder[numRows];
for (int i = 0; i < numRows; i++) {
rows[i] = new StringBuilder();
}
int row = 0; // 当前行号
int dir = -1; // 方向:+1 向下,-1 向上(初始无所谓,第一轮就会反转)
for (char c : s.toCharArray()) {
// 1. 当前字符加入当前行
rows[row].append(c);
// 2. 碰到边界就反转方向
if (row == 0 || row == numRows - 1) {
dir = -dir;
}
// 3. 移动到下一行
row += dir;
}
// 拼接所有行
StringBuilder res = new StringBuilder();
for (StringBuilder sb : rows) {
res.append(sb);
}
return res.toString();
}
}
代码要点
-
numRows == 1必须特判 。如果只有一行,row始终是 0,没地方可移动。如果不特判,row == 0和row == numRows - 1同时成立,dir = -dir反复反转,而row += dir永远不改变,row 卡在 0 死循环。直接返回原串就行。 -
dir 初始值任意 。初始
dir = -1,进入循环后第一轮row == 0触发反转,dir变成+1,row 开始往下走。你也可以初始dir = 1,同样第一轮就反转成向下。不影响结果。 -
边界判断的顺序。先追加当前字符,再判断是否反转方向,再移动行号。这个顺序保证每走到边界行(第0行或最后一行),这一行的字符正常追加,然后才掉头。
复杂度分析
| 项目 | 值 | 说明 |
|---|---|---|
| 时间复杂度 | O(n) | 遍历字符串一次,每个字符 O(1) 追加到 StringBuilder |
| 空间复杂度 | O(n) | n 个字符存在 StringBuilder 数组里(不算返回结果的话是 O(n),算的话也是 O(n)) |
常见坑
-
忘判 numRows == 1 。这是这道题最经典的坑。不判的话
dir = -dir反复执行但 row 永远不动,死循环或 TLE。所有涉及"上下移动再折返"的题,numRows == 1 都要单独处理。 -
方向切换的时机写错。应该是追加完当前字符后再判断是否反转。如果顺序反了------先反转再追加------那走到边界行时,字符会被加到错误的行。
-
用二维数组浪费空间 。如果坚持画矩阵,需要算列宽------每
2*numRows-2个字符占numRows-1列。但 Z 字形斜线上的字符你还是要挨个填,徒增复杂度。
扩展:数学公式法
除了按行模拟,还有一种直接算下标 的方法。观察规律:Z 字形排列中,每个完整周期的长度是 cycle = 2 * numRows - 2。
- 第一行:
0, cycle, 2*cycle, ... - 最后一行:
numRows-1, numRows-1+cycle, ... - 中间行:每周期内有两个字符,一个在竖线上,一个在斜线上,下标差
cycle - 2 * row
可以跳过 StringBuilder 数组,直接按行拼结果------时间还是 O(n),但代码难读一些。面试建议用模拟法,直观不容易出错。
总结
Z 字形变换的核心就三句话:
- 开 numRows 个 StringBuilder,按行收集字符
- 用
dir控制上下方向,碰到首行或末行就反转 - numRows == 1 必须特判,否则死循环
这题本质是方向模拟 类题目的母题。类似的思路在 54. 螺旋矩阵 和 59. 螺旋矩阵 II 里也会用到------维护一个方向变量,碰边界就换向。
这道题你踩过 numRows==1 的坑吗?评论区聊聊,回头复习也方便翻。