前端刷题路-Day96:Z 字形变换(题号6)

502 阅读1分钟

这是我参与8月更文挑战的第30天,活动详情查看:8月更文挑战

Z 字形变换(题号6)

题目

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

比如输入字符串为 "PAYPALISHIRING" 行数为 3 时,排列如下:

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

之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"PAHNAPLSIIGYIR"

请你实现这个将字符串进行指定行数变换的函数:

string convert(string s, int numRows);

示例 1:

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

示例 2:

输入:s = "PAYPALISHIRING", numRows = 4
输出:"PINALSIGYAHRPI"
解释:
P     I    N
A   L S  I G
Y A   H R
P     I

示例 3:

输入:s = "A", numRows = 1
输出:"A"

提示:

  • 1 <= s.length <= 1000
  • s 由英文字母(小写和大写)、',''.' 组成
  • 1 <= numRows <= 1000

链接

leetcode-cn.com/problems/zi…

解释

这题啊,这题是经典找规律。

笔者个人感觉其实题目不是很好理解,这个Z字到底是个怎样的Z字?

看了半天例子之后才理解,原来是先将Z字顺时针旋转90度,之后进行经镜像反转得到的。

老实说,这谁能想得到啊,要不是疯狂看例子再分析,根本想不到好吧,这题真是绝了,看题目就看了得有二十分钟。

弄清楚题意之后看看字母的排列顺寻,总共其实可以拆解为两步:

  1. 从上到下竖向拍成一列

  2. 后续从下往上,从左往右形成一个倾斜的直线,个数是列数减1,也就是第二个参数减1。每走一步往上挪一个位置,同时往右挪一个位置

这两步其实可以构成一个循环,从第一步开始,走完第一步后走到第二步,第二部走到最后其实就是第一步的开始,之后继续走第一步,直到走完位置。

弄清楚这两步之后其实就比较简单了,这题比较容易理解的解法也就出来了。

弄两个变量,一个用来存储当前是哪一列,另外一个用来存储方向,因为第一步的方向是从上往下,第二步的方向是从下往上。第二步走到头的时候更新方向,进行反向操作,并且在第二步的每一步更新列的数据,保证在第二步的过程中数据是从下往上,从左往右走的。

那怎么存储数据呢?搞一个数组,长度就是第二个参数,也就是行的数目,之后不断往里面插入数据即可,最后可以形成跟解释中一样的数据结构。

完事后一join,就是最后的答案了。

当然了,这是比较简单的解法,这种方法的空间复杂度是O(n),如果之后使用一个值来不断当前字符串,也就是通过位置来访问字母,可以用O(1)的空间复杂度来解决问题,具体的放到答案中详细解释。

自己的答案(按行排序)

var convert = function(s, numRows) {
  if (s.length === 1) return s
  const rows = new Array(numRows).fill('')
  let curRow = 0
  let goingDown = false
  for (const char of s) {
    rows[curRow] += char
    if (curRow === 0 || curRow === numRows - 1) {
      goingDown = !goingDown
    }
    curRow += goingDown ? 1 : -1
  }
  return rows.join('')
};

代码就是按照思路来写的,不需要过多的介绍。

由于循环的时候没用到index,直接for...of就行了。

到第一步结尾和第二步开始直接取goingDown的反值就行,不用赋值成true或者false

别的基本上也就这样了,没啥了。

更好的方法(按行访问)

说实话,笔者是有想到这种解法的,可惜没写出来,最后还是参考的官方答案。

先看看代码👇:

var convert = function(s, numRows) {
  if (numRows === 1) return s
  let res = ''
  const len = s.length
  const cycleLen = 2 * numRows - 2
  for (let i = 0; i <numRows; i++) {
    for (let j = 0; j + i < len; j+=cycleLen) {
      res += s[j + i]
      if (i && i !== (numRows - 1) && (j + cycleLen - i) < len) {
        res += s[j + cycleLen - i]
      }      
    }    
  }
  return res
};

看上去有点不好理解,其实还好,就是按行访问。

怎么个按行访问?

先从第一行开始,从左往右扫,每个元素的区间是多少?也就是第一行或者说每一行的相邻元素之间差了多少?

这里可以分为三种情况讨论:

  1. 第一行元素

    这可以说是最好算出来的了,也就是2 * numRows - 2,于是只需要在循环的过程中判断第一行有几个元素,之后添加进去即可

  2. 最后一行元素

    最后一行相差的个数就有点复杂了,是2 * numRows - 2 + i2 * numRows - 2 - i处,由于第一种情况和第一行元素重复了,也就是i为0的情况,所以只需要考虑第二种情况即可。

  3. 其它元素

    其它元素相差的个数就都一样了,是2 * numRows - 2 + numRows - 1,这么一看其实第一种情况也在这种情况里面,所以也可以合并成一个

综合👆这三种情况,其实可以归为两种情况,也就是代码中分别给res分别赋值的两个地方。

讲真,这里确实不好理解,而且比较绕,笔者的代码也是看着官方解答中的C++代码一点点翻译出来的,如果还是没法理解的可以点击这里参考官方解答,尽力了~



PS:想查看往期文章和题目可以点击下面的链接:

这里是按照日期分类的👇

前端刷题路-目录(日期分类)

经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的👇

前端刷题路-目录(题型分类)