阅读视图

发现新文章,点击刷新页面。

LeetCode 6. Z 字形变换:两种解法深度解析与优化

在字符串处理类算法题中,Z 字形变换是一道经典的「规律提炼 + 代码优化」双重点题型,频繁出现在面试与算法练习中。它的核心难点不在于实现功能,而在于如何精准捕捉 Z 字形排列的周期性规律,同时兼顾代码的可读性与执行效率。本文将从题目理解出发,详细拆解两种主流解法,对比其优劣,并补充关键注意事项,帮助大家彻底掌握这道题。

一、题目核心解析

题目描述

给定一个字符串 s 和一个整数 numRows,将字符串按从上往下、从左到右的顺序排列成 Z 字形,再从左到右逐行读取字符,返回拼接后的新字符串。

示例演示

输入:s = "PAYPALISHIRING", numRows = 3

Z 字形排列效果:


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

逐行读取结果:"PAHNAPLSIIGYIR"

核心要求

实现函数 convert(s: string, numRows: number): string,高效完成字符串变换,需重点关注边界场景与性能。

二、解法一:数学周期法(公式推导型)

核心思路

Z 字形排列的本质是「周期性重复」,我们先提炼其周期规律:

  1. 一个完整的 Z 字周期包含 2*(numRows-1) 个字符(下行阶段占 numRows 个,上行阶段占 numRows-2 个,不含首尾重复行);

  2. 首行与末行:每个周期内仅存在「垂直列」字符,无斜列字符;

  3. 中间行(非首非末):每个周期内同时存在「垂直列」和「斜列」两个字符,需分别计算位置。

基于此规律,我们可通过数学公式直接定位每个字符在结果中的位置,无需模拟完整 Z 字形排列。

代码实现与逐行解读


function convert_1(s: string, numRows: number): string {
  // 边界条件1:行数为1时,无法形成Z字,直接返回原字符串
  if (numRows === 1) return s;

  const sL = s.length;
  // 计算一个完整Z字周期的字符数(核心变量)
  const groupNum = (numRows - 1) * 2;
  let res = '';

  // 外层循环:逐行读取结果
  for (let i = 0; i < numRows; i++) {
    // 内层循环:逐周期遍历,定位当前行的字符
    for (let j = 0; j < Math.ceil(sL / groupNum); j++) {
      // 1. 定位当前周期内当前行的垂直列字符索引
      const verticalIdx = i + j * groupNum;
      if (verticalIdx >= sL) break; // 超出字符串长度,终止当前行遍历
      res += s[verticalIdx];

      // 2. 仅中间行需要补充斜列字符
      if (i > 0 && i < numRows - 1) {
        const diagonalIdx = groupNum - i + j * groupNum;
        if (diagonalIdx >= sL) break; // 斜列字符超出范围,跳过
        res += s[diagonalIdx];
      }
    }
  }
  return res;
}

关键注意事项

  • 边界处理:必须优先判断 numRows === 1,否则 groupNum会计算为 0,导致循环逻辑异常;

  • 索引合法性:每次计算垂直列、斜列索引后,需判断是否超出字符串长度,避免越界报错;

  • 斜列字符范围:仅中间行存在斜列字符,首行与末行需跳过此步骤,否则会重复拼接字符。

三、解法二:方向模拟法(直观优化型)

优化思路

数学周期法虽空间效率高,但公式推导对新手不友好,且 JavaScript 中字符串不可变,频繁使用res += 会创建大量临时字符串,存在性能损耗。方向模拟法通过「模拟字符移动轨迹」简化逻辑,同时用数组优化字符串拼接:

  1. 用数组 rows 存储每一行的字符,避免频繁字符串拼接;

  2. 定义 currentRow 跟踪当前字符所在行,step 控制移动方向(1 向下,-1 向上);

  3. 当到达首行或末行时,反转移动方向,实现 Z 字形的「下落-上升」循环。

代码实现与逐行解读


function convert_2(s: string, numRows: number): string {
  // 边界条件优化:行数为1 或 行数≥字符串长度,直接返回原字符串
  if (numRows === 1 || numRows >= s.length) return s;

  // 初始化每行存储容器,用数组避免频繁字符串拼接
  const rows: string[] = new Array(numRows).fill('');
  const cycleLen = 2 * (numRows - 1); // 周期长度(仅作逻辑参考,非必需)
  let currentRow = 0; // 当前字符所在行
  let step = -1; // 移动方向:-1向上,1向下

  // 遍历每个字符,分配到对应行
  for (const char of s) {
    rows[currentRow] += char;

    // 到达首行/末行,反转移动方向
    if (currentRow === 0 || currentRow === numRows - 1) {
      step = -step;
    }
    currentRow += step;
  }

  // 拼接所有行字符,得到最终结果
  return rows.join('');
}

关键注意事项

  • 边界条件补充:新增 numRows >= s.length 判断,此时字符串无法形成 Z 字,直接返回原字符串,减少无效计算;

  • 方向初始化:step 初始值设为 -1,是因为首次进入循环后,currentRow 为 0,会触发方向反转,使第一步移动方向为向下(step=1);

  • 数组优化:JavaScript 中数组拼接效率远高于字符串累加,尤其适合长字符串场景,这是该解法的核心性能优势。

四、两种解法对比与场景选择

维度 数学周期法 方向模拟法
时间复杂度 O(n)(每个字符仅访问一次) O(n)(每个字符仅访问一次)
空间复杂度 O(1)(仅用临时变量,无额外存储) O(n)(需数组存储每行字符)
可读性 较差,需理解公式推导逻辑 优秀,模拟过程直观易懂
性能表现 长字符串场景下,因频繁字符串拼接略逊 数组优化拼接,性能更稳定
适用场景 低内存环境、对空间效率要求极高的场景 日常开发、面试答题(兼顾可读性与性能)

五、测试验证与常见问题

测试用例覆盖


// 测试用例1:基础场景
console.log(convert_1("PAYPALISHIRING", 3)); // 输出:PAHNAPLSIIGYIR
console.log(convert_2("PAYPALISHIRING", 3)); // 输出:PAHNAPLSIIGYIR

// 测试用例2:行数为4
console.log(convert_1("PAYPALISHIRING", 4)); // 输出:PINALSIGYAHRPI
console.log(convert_2("PAYPALISHIRING", 4)); // 输出:PINALSIGYAHRPI

// 测试用例3:边界场景(行数=1)
console.log(convert_1("A", 1)); // 输出:A
console.log(convert_2("A", 1)); // 输出:A

// 测试用例4:边界场景(行数≥字符串长度)
console.log(convert_2("ABCDE", 5)); // 输出:ABCDE

常见问题排查

  1. 字符重复/缺失:大概率是斜列字符判断逻辑错误,需检查 i > 0 && i < numRows - 1 条件是否遗漏;

  2. 越界报错:未判断索引合法性,需在拼接字符前校验 verticalIdxdiagonalIdx 是否小于字符串长度;

  3. 方向异常:step 初始化错误或方向反转逻辑缺失,需确保到达首行/末行时反转方向。

六、总结

Z 字形变换的核心是「抓住周期性」,两种解法均基于这一核心规律,只是实现路径不同:数学周期法偏重于公式推导,追求极致空间效率;方向模拟法偏重于直观模拟,用合理的空间换可读性与性能。

在面试中,推荐优先使用方向模拟法,其逻辑清晰、不易出错,能快速向面试官展现思路;若面试官追问空间优化,可再补充数学周期法的思路。同时,边界条件的全面覆盖的是这道题的得分关键,需牢记行数为 1、行数≥字符串长度这两个特殊场景。

掌握这道题的思考方式,能为后续解决字符串、数组类的规律型题目提供借鉴,核心是学会从复杂排列中提炼简化规律,再通过代码优化平衡效率与可读性。

❌