这是我参与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
链接
解释
这题啊,这题是经典找规律。
笔者个人感觉其实题目不是很好理解,这个Z字到底是个怎样的Z字?
看了半天例子之后才理解,原来是先将Z字顺时针旋转90度,之后进行经镜像反转得到的。
老实说,这谁能想得到啊,要不是疯狂看例子再分析,根本想不到好吧,这题真是绝了,看题目就看了得有二十分钟。
弄清楚题意之后看看字母的排列顺寻,总共其实可以拆解为两步:
-
从上到下竖向拍成一列
-
后续从下往上,从左往右形成一个倾斜的直线,个数是列数减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
};
看上去有点不好理解,其实还好,就是按行访问。
怎么个按行访问?
先从第一行开始,从左往右扫,每个元素的区间是多少?也就是第一行或者说每一行的相邻元素之间差了多少?
这里可以分为三种情况讨论:
-
第一行元素
这可以说是最好算出来的了,也就是
2 * numRows - 2
,于是只需要在循环的过程中判断第一行有几个元素,之后添加进去即可 -
最后一行元素
最后一行相差的个数就有点复杂了,是
2 * numRows - 2 + i
和2 * numRows - 2 - i
处,由于第一种情况和第一行元素重复了,也就是i
为0的情况,所以只需要考虑第二种情况即可。 -
其它元素
其它元素相差的个数就都一样了,是
2 * numRows - 2 + numRows - 1
,这么一看其实第一种情况也在这种情况里面,所以也可以合并成一个
综合👆这三种情况,其实可以归为两种情况,也就是代码中分别给res
分别赋值的两个地方。
讲真,这里确实不好理解,而且比较绕,笔者的代码也是看着官方解答中的C++代码一点点翻译出来的,如果还是没法理解的可以点击这里参考官方解答,尽力了~
PS:想查看往期文章和题目可以点击下面的链接:
这里是按照日期分类的👇
经过有些朋友的提醒,感觉也应该按照题型分类
这里是按照题型分类的👇