LeetCode 6 Z 字形变换 - 按行模拟

LeetCode 第 6 题「Z 字形变换」看着像矩阵操作,但真画个二维数组就掉坑里了------用 StringBuilder 数组按行收集,代码短、逻辑清、O(n) 一把过。


题目

🟡 6. Z 字形变换

将一个给定字符串 s 根据给定的行数 numRows,以从上往下、从左到右进行 Z 字形排列,然后按行读出。

示例 1s = "PAYPALISHIRING", numRows = 3

复制代码
P   A   H   N
A P L S I I G
Y   I   R

输出:"PAHNAPLSIIGYIR"

示例 2s = "PAYPALISHIRING", numRows = 4

复制代码
P     I    N
A   L S  I G
Y A   H R
P     I

输出:"PINALSIGYAHRPI"


思路分析

别画矩阵,按行装

第一反应可能是开一个 char[numRows][n] 的二维数组,把字符按 Z 字形填进去,再逐行读出来。这当然能做,但有两个问题:

  1. 列数不好算,得先推公式确定数组宽度
  2. 大部分格子是空的,浪费空间,很多遍历都是无用功

换个思路:最终答案就是按行拼接 ,那我们干脆只维护每一行装了什么字符。开 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();
    }
}

代码要点

  1. numRows == 1 必须特判 。如果只有一行,row 始终是 0,没地方可移动。如果不特判,row == 0row == numRows - 1 同时成立,dir = -dir 反复反转,而 row += dir 永远不改变,row 卡在 0 死循环。直接返回原串就行。

  2. dir 初始值任意 。初始 dir = -1,进入循环后第一轮 row == 0 触发反转,dir 变成 +1,row 开始往下走。你也可以初始 dir = 1,同样第一轮就反转成向下。不影响结果。

  3. 边界判断的顺序。先追加当前字符,再判断是否反转方向,再移动行号。这个顺序保证每走到边界行(第0行或最后一行),这一行的字符正常追加,然后才掉头。


复杂度分析

项目 说明
时间复杂度 O(n) 遍历字符串一次,每个字符 O(1) 追加到 StringBuilder
空间复杂度 O(n) n 个字符存在 StringBuilder 数组里(不算返回结果的话是 O(n),算的话也是 O(n))

常见坑

  1. 忘判 numRows == 1 。这是这道题最经典的坑。不判的话 dir = -dir 反复执行但 row 永远不动,死循环或 TLE。所有涉及"上下移动再折返"的题,numRows == 1 都要单独处理。

  2. 方向切换的时机写错。应该是追加完当前字符后再判断是否反转。如果顺序反了------先反转再追加------那走到边界行时,字符会被加到错误的行。

  3. 用二维数组浪费空间 。如果坚持画矩阵,需要算列宽------每 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 的坑吗?评论区聊聊,回头复习也方便翻。

相关推荐
fkyyly2 小时前
hermes解读
算法·code_agent
过期的秋刀鱼!2 小时前
使用都热编码的分类特征
人工智能·算法·决策树·机器学习·分类·数据挖掘
wabs6662 小时前
关于哈希表【力扣383.赎金信的思考】
算法·leetcode·散列表
love_muming3 小时前
二叉树操作全解析:从递归到层序遍历
java·数据结构·算法·二叉树
.道阻且长.11 小时前
2.LeetCode算法习题讲解--双指针--复写零
算法·leetcode·职场和发展
To_OC13 小时前
LC 438 找到所有字母异位词:暴力超时后,我靠滑动窗口一招搞定
javascript·算法·leetcode
Forever Nore16 小时前
学完C语言力扣第一题做不来正常吗
数据结构·算法
hansang_IR16 小时前
【题解】LC:倍增 / 区间并查集(Range Parallel Unionfind)
c++·算法·并查集