愿旖旎头像
LeetCode 6: Z 字形变换 (模拟) —— 题解封面图

LeetCode 6: Z 字形变换 (模拟) —— 题解

  👋 欢迎阅读

🎯 欢迎来到「Z 字形变换」题解之旅! 本文将带你从"把字符串按 Z 字形排好后逐行读出"这一直观场景出发,深入理解周期性规律的巧妙运用,并掌握如何用公差 d = 2·numRows - 2 直接定位每行字符来在 O(n) 内完成变换。

在开始之前,建议你先:

  • 了解题目背景:这是 LeetCode 6 题,把字符串按 Z 字形(蛇形)排列成 numRows 行后,逐行读取得到新字符串。本质上,Z 字形的字符下标呈固定周期 d = 2·numRows - 2 循环,问题转化为按行找下标规律并直接拼接。

  • 明确学习目标:掌握首行/末行与中间行的下标规律,理解 d = 2·numRows - 2 的推导与 numRows = 1 时 d = 0 的死循环陷阱,并熟练处理字符串短于行数等边界情况。

  • 准备好环境:建议在本地 IDE 或 LeetCode 在线编辑器中打开代码,边看边运行,亲手验证示例(如 s = "PAYPALISHIRING",numRows = 3 输出 "PAHNAPLSIIGYIR")。

本文将从问题转化、周期推导、逐行收集、边界防护到代码实现,层层递进。即使你对找规律题还不熟悉,我们也会从"画一画 Z 字,看看每行下标差多少"这一直觉出发,让你轻松抓住核心思想——一个周期走两列,首末行取首尾。现在,让我们一起按行收集字符,完成 Z 字形变换吧! 🔤🎯

 🔥 愿旖旎 · 个人主页

📘 学习专栏: 《算法专栏》《LangChain学习》《贪心算法》

🌄 钱塘江上潮信来,今日方知我是我

✨当前学习内容:《模拟》


一.题目

6. Z 字形变换 - 力扣(LeetCode)

 ​

二、算法分析

一、问题分析(前置分析)

  • 题目要求:把字符串按 Z 字形排列成 numRows 行,再逐行读取返回新字符串。
  • 关键约束:numRows = 1 时原样返回(无变换);字符串可能短于行数;要求 O(n) 时间。
  • 核心思路:不真正画二维矩阵(那样需要额外空间),而是找下标规律:一个完整周期含 d = 2·numRows - 2 个字符,首行和末行每隔 d 取一个字符,中间行每周期取两个字符(下标相差 d,第二个是 j + d - 2i),按行拼接即得答案。

📌 例子:为什么"下标有规律"

s = "PAYPALISHIRING",numRows = 3,d = 2×3-2 = 4。Z 字形排列为:

复制

P   A   H   N
A P L S I I G
Y   I   R

第 0 行下标 0, 4, 8, 12(公差 4);第 2 行下标 2, 6, 10(公差 4);第 1 行(中间行)下标 1, 3, 5, 7, 9, 11——相邻间隔在 2 和 2 之间交替(1→3 差 2、3→5 差 2)。规律清楚了,就能直接按下标取字符,无需真的画矩阵。

二、算法策略(周期规律 + 按行收集)

核心步骤:

  1. 计算公差:d = 2 * numRows - 2(一个 Z 字周期含 2 列,共 2·numRows - 2 个字符)。
  2. 特判 d == 0:numRows == 1 时 d = 0,会导致下标不前进死循环,强制令 d = 1(此时逐字符原样拼接,等价于无变换)。
  3. 首行(i = 0)与末行(i = numRows-1):下标 i, i+d, i+2d, ...,每隔 d 取一个。
  4. 中间行(0 < i < numRows-1):每周期取两个字符,下标 j 与 k = j + d - 2i,两者都 += d 前进,各自判断越界后拼接。
  5. 返回:按行依次拼接得到的 str。

📊 示例(s = "PAYPALISHIRING",numRows = 3,d = 4):

行 i类型下标序列取到的字符拼接结果
0首行0, 4, 8, 12P, A, H, NPAHN
1中间行(1, 3), (5, 7), (9, 11)A,P / L,S / I,IAPLSIIG
2末行2, 6, 10Y, I, RYIR

三行依次拼接:PAHN + APLSIIG + YIR = "PAHNAPLSIIGYIR" ✅,与题目示例完全一致。

三、正确性说明(简单版本)

  • 周期规律成立:Z 字形每 d = 2·numRows-2 个字符重复一次形状,因此任一行内下标都呈周期性;首末行周期为 d,中间行每周期含 2 个字符——规律由 Z 字几何结构唯一决定,不会错判。
  • 中间行第二个下标推导:设某周期起点为 j(该行字符落在第 i 行第 1 列),沿斜线走到第 2 列时,行号递增 i,而字符下标也随之前进;推到该行第 2 个字符的下标为 j + d - 2i,两字符之间正好相隔 d - 2i,下一个周期再各加 d——公式与图形一一对应。
  • 循环条件覆盖完整:中间行用 j < n || k < n 作为循环条件(任一下标未越界就继续),并在拼接时各自判断 if (j < n) / if (k < n),保证不越界且不遗漏最后一个字符。
  • 按行拼接顺序正确:外层按行号 0 → numRows-1 遍历,正好是"逐行读取"的顺序,结果与题目要求一致。

📌 例子:中间行的两个下标为什么相差 d - 2i

numRows = 4、d = 6,第 1 行(i=1):第一个字符下标 j = 1,第二个下标 k = 1 + 6 - 2×1 = 5——对应字符 1 和 5,间隔 4 = d - 2i;下一周期 j = 7、k = 11。画出 4 行 Z 字可以看到:第 1 行的字符确实落在下标 1, 5, 7, 11, ...,公式精确命中原图位置。

四、实现细节(边界防护)

  • 初始化:n = s.size()、d = 2 * numRows - 2、str 空串。
  • 边界防护:d == 0 必须特判为 1(numRows = 1 时 d = 0,j += 0 会死循环);中间行循环条件用 ||(任一下标有效即继续),拼接时分别判越界;s 短于 numRows 时各循环自然只取有效下标,无需特判。
  • 复杂度:时间 O(n)(每个字符恰好被访问一次),空间 O(1)(除返回字符串外无额外空间,不建二维矩阵)。
  • 关键操作:int d = 2 * numRows - 2;(周期公差)、if (d == 0) d = 1;(防死循环)、for (int j = i, k = d - j; j < n || k < n; j += d, k += d)(中间行双下标遍历)。

📌 例子:numRows = 1 的死循环陷阱

s = "ABC"、numRows = 1:d = 2×1 - 2 = 0。若不特判,i = 0 时走首行分支,for (j = 0; j < n; j += 0) —— j 永远停在 0,"ABC" 会被无限拼接 'A',程序死循环/超时。特判 d = 1 后逐字符取,得到 "ABC"(1 行的 Z 字形就是原串)✅。

五、返回值(目标映射)

  • 返回 str:Z 字形排列后逐行读取的字符串,对应题目"返回变换后的字符串"。

三.代码

class Solution
{
public:
    string convert(string s, int numRows)
    {
        int n = s.size();
        string str;
        int d = 2 * numRows - 2;    // 公差:一个 Z 字周期含 2*numRows-2 个字符

        // 边界:numRows == 1 时 d = 0,下标不前进会死循环,强制设为 1(等价于无变换)
        if (d == 0)
        {
            d = 1;
        }

        // 1. 按行收集字符(外层行号即“逐行读取”的顺序)
        for (int i = 0; i < numRows; i++)
        {
            // 2. 首行:每隔 d 取一个字符
            if (i == 0)
            {
                for (int j = i; j < n; j += d)
                {
                    str += s[j];
                }
            }
            // 3. 中间行:每周期取两个字符,下标相差 d - 2i
            else if (i != 0 && i != numRows - 1)
            {
                for (int j = i, k = d - j; j < n || k < n; j += d, k += d)
                {
                    if (j < n)
                    {
                        str += s[j];    // 周期内第 1 个字符
                    }
                    if (k < n)
                    {
                        str += s[k];    // 周期内第 2 个字符(下标 j + d - 2i)
                    }
                }
            }
            // 4. 末行:同样每隔 d 取一个字符
            else
            {
                for (int j = i; j < n; j += d)
                {
                    str += s[j];
                }
            }
        }
        return str;     // 5. 返回 Z 字形逐行读取的结果
    }
};

四、易错点分析

难点1:numRows == 1 时 d = 0 的死循环

int d = 2 * numRows - 2;
if (d == 0)
{
    d = 1;
}

numRows = 1 时公差 d = 0,而所有内层循环都是 j += d —— 步长为 0,j 永远不前进,循环条件永真,程序死循环(LeetCode 上表现为超时)。特判 d = 1 后逐字符前进,相当于原样返回。这是本题最致命的边界,且只有 numRows = 1 一个输入会触发,容易被忽略。

难点2:中间行第二个下标的推导 k = d - j

for (int j = i, k = d - j; j < n || k < n; j += d, k += d)

初始时 j = i,故 k = d - i。验证:numRows = 3, d = 4, i = 1 时 k = 3——第 1 行第 2 个字符下标正是 3 ✅(对应 "PAYPALISHIRING" 中的 'P')。下一周期 j += 4、k += 4 同时前进,保持"两字符相差 d - 2i"的关系。若把 k 的初始化写成固定值或始终 k = j + 1,中间行就会取错字符。

难点3:中间行的循环条件必须用 || 且内部各自判越界

for (int j = i, k = d - j; j < n || k < n; j += d, k += d)
{
    if (j < n) str += s[j];
    if (k < n) str += s[k];
}

两个下标不同步到达边界(第二个通常更早或更晚越界)。若条件写成 &&,会在其中一个越界时提前退出,漏掉另一下标仍在范围内的字符;若不写内部 if 判越界,则可能在 j >= n 时访问越界(UB)。"条件用 ||、取值各判" 是双下标遍历的标准写法。

难点4:为什么要"找规律"而不是真的建矩阵

// 建矩阵:vector<string> rows(numRows); 按 Z 字走位填充,最后拼接

模拟法(走位填充矩阵)代码更直观但需要 O(numRows) 额外空间,且走位逻辑(方向切换、边界折返)也易错。找规律法直接按公式取字符,空间 O(1)、逻辑更纯粹——"变换类题目优先找下标规律" 是重要经验,尤其当变换具有周期性时。

五、流程图

 

 🎯 闭幕

🎉 恭喜你完成了「Z 字形变换」问题的学习!

为了巩固知识并进一步拓展,建议你:

🚀 动手实践
在 LeetCode 上提交代码,尝试不同的测试用例。

💡 深入思考

  • 代码中定义周期 d = 2 * numRows - 2。为什么一个 Z 字周期的字符数是 2 * numRows - 2? 请结合 numRows = 3 和 numRows = 4 画出周期结构验证。

  • 对于中间行(非首末行),每个周期取两个字符,第二个字符的下标是 j + d - 2 * i。这个偏移量是如何推导出来的? 为什么首行和末行每周期只取一个字符?

  • 当 numRows == 1 时,d = 0,代码强制将 d 设为 1。如果不这样处理,程序会发生什么? 这种边界情况体现了什么编程习惯?

  • 代码采用 按行读取 的方式直接构造结果,而不是模拟 Z 字形填充矩阵。这种“找规律”的方法相比模拟法有什么优势? 在什么情况下模拟法可能更直观?

如果你觉得本文对你有所帮助,欢迎:

👍 点赞 / 收藏
👤 关注作者,获取更多题解
💬 留言交流你的疑问或优化思路


📌 深入思考答案

  • 周期长度 2 * numRows - 2:因为 Z 字形从第一行向下走到第 numRows 行,再斜向上回到第二行,共经历 numRows 个下降步和 numRows - 2 个上升步,合计 2 * numRows - 2 个字符。例如 numRows=3 时周期为 4(如 0,1,2,1),numRows=4 时周期为 6(如 0,1,2,3,2,1)。

  • 中间行第二个字符偏移:设当前周期起始下标为 j,该行在周期中的位置为 i。第一个字符在 j,第二个字符出现在从底部返回的斜线上,其下标为 j + d - 2 * i(因为从 j 到下一个周期起点 j+d 之间,该行被访问两次,间隔为 d - 2*i)。首行和末行在周期中只出现一次,因此只取一个。

  • numRows == 1 必须特殊处理:因为 d = 0,循环中 j += d 会导致 j 永不前进,程序陷入死循环。将 d 设为 1 后,相当于逐字符复制原字符串,符合题意。这体现了对 边界条件 的严谨处理。

  • 找规律法的优势:直接按行拼接,无需模拟二维矩阵,节省了空间(模拟法需要 O(numRows * n) 或至少 O(n) 的矩阵),且代码更简洁、常数更小。模拟法在理解上更直观,但实现更繁琐。

祝你在 算法之路 上越走越稳,早日攻克每一道难题!下次见 🚀✨

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2601_96587588/article/details/166839655

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--