👋 欢迎阅读

🎯 欢迎来到「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学习》《贪心算法》
🌄 钱塘江上潮信来,今日方知我是我
✨当前学习内容:《模拟》
一.题目
二、算法分析
一、问题分析(前置分析)
- 题目要求:把字符串按 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)。规律清楚了,就能直接按下标取字符,无需真的画矩阵。
二、算法策略(周期规律 + 按行收集)
核心步骤:
- 计算公差:
d = 2 * numRows - 2(一个 Z 字周期含 2 列,共2·numRows - 2个字符)。 - 特判
d == 0:numRows == 1时d = 0,会导致下标不前进死循环,强制令d = 1(此时逐字符原样拼接,等价于无变换)。 - 首行(i = 0)与末行(i = numRows-1):下标
i, i+d, i+2d, ...,每隔 d 取一个。 - 中间行(0 < i < numRows-1):每周期取两个字符,下标
j与k = j + d - 2i,两者都+= d前进,各自判断越界后拼接。 - 返回:按行依次拼接得到的
str。
📊 示例(s = "PAYPALISHIRING",numRows = 3,d = 4):
| 行 i | 类型 | 下标序列 | 取到的字符 | 拼接结果 |
|---|---|---|---|---|
| 0 | 首行 | 0, 4, 8, 12 | P, A, H, N | PAHN |
| 1 | 中间行 | (1, 3), (5, 7), (9, 11) | A,P / L,S / I,I | APLSIIG |
| 2 | 末行 | 2, 6, 10 | Y, I, R | YIR |
三行依次拼接: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




