来源:LeetCode
难度:中等
问题详情:
将一个给定字符串 s
根据给定的行数 numRows
,以从上往下、从左到右进行 Z 字形排列。
比如输入字符串为 “PAYPALISHIRING” 行数为 3 时,排列如下:
P A H N
A P L S I I G
Y I R
之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"PAHNAPLSIIGYIR"
。
在真正开始介绍各种算法前,先以表格形式展示各自的时间复杂度和空间复杂度, n n n 表示字符串 s s s 的长度。
算法 | 时间复杂度 | 空间复杂度 |
---|---|---|
二维矩阵 | O ( n u m R o w s × n ) O(numRows×n) O(numRows×n) | O ( n u m R o w s × n ) O(numRows×n) O(numRows×n) |
二维矩阵改进 | O( n n n) | O ( n ) O(n) O(n) |
构造法 | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) |
使用二维矩阵依次按照 第1节 中那样的顺序存进二维数组,没有字符的用空字符串代替。在竖线中的元素是行不断增加,列保持不变;在斜线中的元素依次是行不断减少,列不断增大;
首先分析该字符串写成Z字形后,一共有几列。
我们以这个倒N字形的竖线和斜线上的元素为一个单元。 竖线上的元素个数= n u m R o w s numRows numRows;斜线上的元素个数为 n u m R o w s − 2 numRows-2 numRows−2;因此一个单元中的元素个数总共 r = n u m R o w s × 2 − 2 r=numRows×2-2 r=numRows×2−2;每一个单元的列数: 1 + n u m R o w s − 2 = n u m R o w s − 1 1+numRows-2=numRows-1 1+numRows−2=numRows−1
则共有 ⌊ n r ⌋ \lfloor \dfrac{n}{r}\rfloor ⌊rn⌋个单元,而那些组不成一个单元的元素个数,也要看是否 > n u m R o w s >numRows >numRows。如果小于,则余下的元素构成一列(因为一个竖线有 n u m R o w s numRows numRows个元素);如果大于,超过numRows的部分(即斜线部分)每一个元素独占一列。
则求列数的公式如下:
c
o
l
=
⌊
n
r
⌋
×
(
n
u
m
R
o
w
s
−
1
)
r
e
=
n
%
r
c
o
l
1
=
⌊
r
e
n
u
m
R
o
w
s
⌋
+
r
e
%
n
u
m
R
o
w
s
c
o
l
=
c
o
l
+
c
o
l
1
而这个矩阵的行数为
n
u
m
R
o
w
s
numRows
numRows。
因此该矩阵的尺寸
(
n
u
m
R
o
w
s
,
c
o
l
)
(numRows,col)
(numRows,col)。‘
而官方解法则是选择了一个偷懒的方法,让那些构不成一个单元的,通过填补空白使其构成一个单元。这样的解法需要 ⌈ n r ⌉ \lceil \dfrac{n}{r}\rceil ⌈rn⌉ 列,是向上取整的。虽然这样做代码比较简洁,但是会比我的解法占用更多的空间。
为了判断当前元素是在竖线还是在斜线上,我们需要构建一个判断标准。
官方的解法在前面已经略有提及,通过当前字符在字符串中的索引 i n d e x index index 对一个单元的元素个数 r r r 求余,其余数小于 n u m R o w s numRows numRows,则表明在竖线上;大于等于numRows则表明在斜线上;
我的解法则是:
该问题的边界问题就是,转换后只有一行或者一列的状态。当只有一行时,那就是 n u m R o w s = 1 numRows=1 numRows=1;如果只有一列,那就是 n u m R o w s > = n numRows >= n numRows>=n;
def convert(s: str, numRows: int) -> str:
length = len(s)
# 计算一共占了多少列
# 当排列后为一行或者一列的情况,则直接返回原字符串
if length <= numRows or numRows == 1:
return s
else:
# 以Z字形中的一个横线和对角线为一个单元,计算s中能有多少个这样的单元
t = length // (numRows + numRows - 2)
temp = length % (numRows + numRows - 2)
col1 = temp // numRows
col1 += temp % numRows
# 一个单元占了 (numRows-2 + 1)列,而剩下的不成单元的占col1列
col = (numRows - 2 + 1) * t + col1
# 先创建一个空字符列表,一共numRows行col列
result = [[""] * col for i in range(numRows)]
index = 0
i = 0 # 第几行
j = 0 # 第几列
diag = False # 表示是否在对角线上
# 通过分析,可以得知当行标走到numRows-1时,下一步就要走到对角线处了,此时行不断往上,列不断往右
# 当行标再次回到0的时候,就是走出了对角线,此时行不断往下,列保持不变
while index <= length - 1:
if i == 0:
diag = False
if i == numRows - 1:
diag = True
result[i][j] = s[index]
index += 1
if not diag:
i += 1
else:
i -= 1
j += 1
result_2 = ''
for row_item in result:
for col_item in row_item:
if col_item != '':
result_2 += col_item
return result_2
对于时间复杂度,创建和遍历 r e s u l t result result的消耗较高,为 O ( 行数 × 列数 ) O(行数×列数) O(行数×列数),行数 = n u m R o w s =numRows =numRows,列数可以粗略地看作 O ( n ) O(n) O(n),时间复杂度为 O ( n u m R o w s × n ) O(numRows×n) O(numRows×n), w h i l e while while内部时间复杂度为 O ( n ) O(n) O(n),因此总的时间复杂度为 O ( n u m R o w s × n ) O(numRows×n) O(numRows×n)
空间复杂度,则同样是 O ( n u m R o w s × n ) O(numRows×n) O(numRows×n)
上述的二维矩阵存在大量空白,浪费了许多空间。
因此我们可以将每一行作为一个列表,每次移动到该行时,就将该元素添加至这一行对应的列表中。其余则与第一种解法相同。
def convert2(s: str, numRows: int) -> str:
"""使用多个空白列表存储每一行的字符"""
length = len(s)
if length <= numRows or numRows == 1:
return s
else:
# 生成一个numRows行的列表存储每一行的字符
result = [[] for i in range(numRows)]
i = 0 # 表示行
diag = False
for item in s:
result[i].append(item)
if i == 0:
diag = False
elif i == numRows - 1:
diag = True
if diag:
i -= 1
else:
i += 1
from itertools import chain
return ''.join(chain(*result))
对于时间复杂度, w h i l e while while和列表的创建和遍历时间复杂度都是 O ( n ) O(n) O(n),因此算法的时间复杂度为 O ( n ) O(n) O(n)
对于空间复杂度,列表中存储了所有的字符串,空间复杂度 O ( n ) O(n) O(n)
我们可以直接去探究某一下标的字符应该放在第几行。
如上图所示,假设
n
u
m
R
o
w
s
=
4
numRows=4
numRows=4.
这里的 t = r t=r t=r(2.2.1节提到,每个单元的元素个数)。
其中的 j j j是值为 0 t , 1 t , 2 t , 3 t . . . 0t, 1t,2t,3t... 0t,1t,2t,3t...的序列;而 i i i表示第 i i i行, 再次提示这里的 r = t r=t r=t.
def convert4(s: str, numRows: int) -> str:
"""在方法1的基础上,分析字符串下标对应二维数组索引"""
length = len(s)
if length <= numRows or numRows == 1:
return s
else:
ans = []
r = 2 * numRows - 2 # 一个单元拥有的字符数
for i in range(numRows):
# 因为每一行最少有一个元素,因此-i,可以避免i与j相加后下标越界
# j = 0r, 1r, 2r, 3r, s.t. j < length-i
for j in range(0, length - i, r):
# 添加每个单元中竖线中的元素
# j + i:
# 第0行元素下标,0r+0, 1r+0,2r+0
# 第1行元素下标,0r+1, 1r+1, 2r+1
# 第i行元素下标,j+i
ans.append(s[j + i]) # 第一个元素
# 因为斜线中的元素下标为
# 第0行没有斜线
# 第1行元素下标,1r-1, 2r-1
# 第i行元素下标,j+r-i
if 0 < i < numRows - 1 and j + r - i < length:
ans.append(s[j + r - i]) # 第二个元素
return ''.join(ans)
对于时间复杂度,是将所有字符串中的元素都添加到列表中,所以时间复杂度为 O ( n ) O(n) O(n);
对于空间复杂度,ans是返回值,不计入在空间复杂度的统计当中,因此空间复杂度为O(1)。