没有按照顺序做,因为第4题是困难题,虽然代码通过了测试,但解法存在两层进阶,需要仔细整理,待后面专门抽时间整理;又因为把第6题想简单了,所以也临时跳过了第5题。
LeetCode刷题笔记(Python3)——6. Z 字形变换
(点击查看题目)
(点击查看官方题解)
注意:此题的官方题解没有Python代码,但提供了两种解题思路:按行排序和按列排序。
LeetCode刷题笔记(Python3)——6. Z 字形变换
一、解题思想和算法
此题有两种解题思路——按列排序和按行访问。(参考官方题解)
1.1 按列排序
思路
通过从左向右迭代字符串,我们可以轻松地确定字符位于 Z 字形图案中的哪一行。
算法
我们可以使用
m
i
n
(
n
u
m
R
o
w
s
,
l
e
n
(
s
)
)
min(numRows,len(s))
min(numRows,len(s)) 个列表来表示 Z 字形图案中的非空行。
从左到右迭代 s s s,将每个字符添加到合适的行。可以使用当前行和当前方向这两个变量对合适的行进行跟踪。
只有当我们向上移动到最上面的行或向下移动到最下面的行时,当前方向才会发生改变。
1.2 按行访问
思路
按照与逐行读取 Z 字形图案相同的顺序访问字符串。
算法
首先访问 行 0 中的所有字符,接着访问 行 1,然后 行 2,依此类推。
对于所有整数k,
- 行 0 中的字符位于索引 k ( 2 ⋅ n u m R o w s − 2 ) k(2⋅numRows−2) k(2⋅numRows−2) 处;
- 中间行 i i i中的字符位于索引 k ( 2 ⋅ n u m R o w s − 2 ) + i k(2⋅numRows−2)+i k(2⋅numRows−2)+i 以及 ( k + 1 ) ( 2 ⋅ n u m R o w s − 2 ) − i (k+1)(2⋅numRows−2)−i (k+1)(2⋅numRows−2)−i 处;
- 行 n u m R o w s − 1 numRows−1 numRows−1 中的字符位于索引 k ( 2 ⋅ n u m R o w s − 2 ) + n u m R o w s − 1 k(2⋅numRows−2)+numRows−1 k(2⋅numRows−2)+numRows−1 处;
二、几种解法及代码
初始解法
# 时间复杂度:O(N) 空间复杂度:O(N)
class Solution:
def convert(self, s: str, numRows: int) -> str:
if numRows == 1:
return s
else:
step = 2 * numRows - 2
r = s[::step]
for i in range(1, numRows - 1):
a = s[i::step]
b = s[step-i::step]
# 将a和b交错合并(a和b的长度不一定相同)
min_len = min(len(a), len(b))
res = [''] * min_len * 2
res[::2] = a[:min_len]
res[1::2] = b[:min_len]
r += ''.join(res) + a[min_len:] + b[min_len:]
return r + s[numRows-1::step]
初始解法采用了按行访问的思路(直觉认为按列排序会很慢,所以直接选择了找规律的按行访问),但是由于中间行事实上存在两个有规律的子串a和b,将二者交错合并在python里面的实现不使用for循环会较为困难(具体实现的办法还是网上现搜的),所以实际运行速度很慢。
优化解法
查看了官方的按行访问思路后,选择用for循环读取中间行字符
class Solution:
def convert(self, s: str, numRows: int) -> str:
if numRows == 1:
return s
else:
len_s = len(s)
step = 2 * numRows - 2
r = s[::step]
for i in range(1, numRows - 1):
for j in range(len_s):
pos_1 = step * j + i
if pos_1 < len_s:
r += s[pos_1]
else:
break
pos_2 = step * (j+1) - i
if pos_2 < len_s:
r += s[pos_2]
else:
break
return r + s[numRows-1::step]
该算法仍使用按行访问的思路,但运行速度明显比初始算法更快。
1. 切片用法的注意事项
a = "abcdefgh"
b = [::2] # "aceg"
切片的两个冒号分开的三者为 start: stop: step,start为起始位置,如果是0,可以省略;stop为终止位置+1(注意不是结束位置),如果直接到结尾,可以省略;step为间隔大小。
range函数的三个参数也是上述顺序,即 range( start, stop[, step] )
最快解法
参考了运行时间最短的代码,其使用的思路就是按列排序后连接。
class Solution:
def convert(self, s: str, numRows: int) -> str:
if numRows < 2:
return s
n = len(s)
lst = ['' for _ in range(numRows)]
down = -1 # down = 1代表行数增加;反之,行数减少
row = 0
for ch in s:
lst[row] += ch
if row == numRows-1 or row == 0:
down *= -1
row += down
return ''.join(lst)
2. 多行字符串的生成方法
个人之所以写不出来(包括在写初始算法还需要查询子算法),很重要的一个原因是不知道如何生成需要的多行字符串。事实上,生成方法很简单,只需要借助列表即可:
lst = ['' for _ in range(numRows)] # numRows为行数
另外,还可以从上述代码中了解到,如果只需要占位时,可以直接使用“_”,而无需定义变量。
3. join()方法的使用
(参考菜鸟教程)
join方法用于将序列中的元素以指定的字符连接生成一个新的字符串。其语法为
str.join(sequence)
其中,sequence为要连接的元素序列(列表或字典均可,使用字典时连接的是键而非值),str为用于连接sequence中各字符的中间字符。例如,
'*'.join(['a','b','c']) # 'a*b*c'
'-'.join(['a','b','c']) # 'a-b-c'
''.join(['a','b','c']) # 'abc'
4. 加快程序速度的小技巧:“等于”和“大于/小于”
如果要判断一个数是否是某个值,而可能的其余所有值都大于(或小于)该值,那么此时使用“<”(或“>”)的速度要快于使用“==”的速度。例如,在最快算法中正式代码的第一行中,如果将
if numRows < 2:
替换为
if numRows == 1:
将会明显降低运算速度。(在本题的连续两次测试中,前者的运行时间为52ms,而后者为60ms。)这其中的原理其实与“==”和“>”、“<”的计算原理有关,后面两者明显更快(请读者自己直观想想)。