Z 字形变换

字符串 模拟 数学

题目描述

将一个给定的字符串 s 根据给定的行数 rows,以从上往下、从左到右的 Z 字形排列。之后按照逐行阅读的顺序返回拼接后的字符串。

例如,输入字符串 "PAYPALISHIRING",行数为 4 时,Z 字形排列如下:

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

最终结果为 "PINALSIGYAHRPI"

思路分析

解法一:模拟法

最直观的做法是模拟 Z 字形书写的过程:

  1. 创建一个长度为 rows 的数组,每个元素代表 Z 字形中的一行。
  2. 遍历字符串中的每个字符,将它追加到当前行。
  3. 通过一个 goingDown 变量控制方向:到达首行或末行时掉头。
  4. 最终把所有行拼接起来就是结果。

解法二:数学构造法(进阶)

不需要模拟遍历过程,而是直接通过数学公式计算出每个字符在原字符串中的索引位置。

核心观察:一个完整的 Z 字形周期长度cycle = 2 * (rows - 1)。在每个周期中:

  • 下行阶段:每一行都会被经过一次,索引为 j + row
  • 上行阶段:非首行、非末行会被再经过一次,索引为 j + cycle - row

因此只需逐行遍历,按公式直接取出字符拼接即可。

示例代码

模拟法
数学构造法
/**
 * Z 字形变换
 * @param s - 原始字符串
 * @param rows - Z 字形排列的行数
 * @returns 按 Z 字形排列后,逐行拼接的结果字符串
 */
export function strZConvert(s: string, rows: number): string {
    // 只有 1 行或字符串长度不足以形成 Z 形,直接返回原字符串
    if (rows === 1 || s.length < rows) {
        return s;
    }

    // 创建一个数组,每个元素代表 Z 字形中的一行,初始为空字符串
    const strRows = new Array(rows).fill("");
    // 当前字符应该放到第几行
    let currRow = 0;
    // 标记当前遍历方向:true 表示从上往下,false 表示从下往上
    let goingDown = false;

    for (const char of s) {
        // 把当前字符追加到对应行
        strRows[currRow] += char;

        // 到达第一行或最后一行时,需要掉头(反转方向)
        if (currRow === 0 || currRow === rows - 1) {
            goingDown = !goingDown;
        }

        // 根据方向移动行号:向下则 +1,向上则 -1
        currRow += goingDown ? 1 : -1;
    }

    // 把所有行拼接起来就是最终结果
    return strRows.join("");
}

复杂度分析

两种解法的复杂度相同:

  • 时间复杂度O(n),其中 n 是字符串长度。每个字符恰好被访问一次。
  • 空间复杂度:模拟法为 O(n)(需要存储各行字符串);数学构造法也为 O(n)(结果字符串)。

示例演示

s = "PAYPALISHIRING"rows = 4 为例,周期 cycle = 2 * (4 - 1) = 6

行号下行索引 (j + row)上行索引 (j + cycle - row)收集到的字符
00, 6, 12—(首行无上行)P, I, N
11, 7, 135, 11A, L, S, I, G
22, 84, 10Y, A, H, R
33, 9—(末行无上行)P, I

逐行拼接结果:"PINALSIGYAHRPI"