【PythonCode】力扣Leetcode6~10题Python版
前言
力扣Leetcode是一个集学习、刷题、竞赛等功能于一体的编程学习平台,很多计算机相关专业的学生、编程自学者、IT从业者在上面学习和刷题。
在Leetcode上刷题,可以选择各种主流的编程语言,如C++、JAVA、Python、Go等。还可以在线编程,实时执行代码,如果代码通过了平台准备的测试用例,就可以通过题目。
本系列中的文章从Leetcode的第1题开始,记录我用Python语言提交的代码和思路,受个人能力限制,只是实现功能通过用例,我没有每题都研究最优的实现方法,供Python学习参考。
6. N 字形变换
将一个给定字符串 s 根据给定的行数 numRows ,以从上往下、从左到右进行 Z 字形排列。
比如输入字符串为 “PAYPALISHIRING” 行数为 3 时,排列如下:
之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:“PAHNAPLSIIGYIR”。
请你实现这个将字符串进行指定行数变换的函数:
string convert(string s, int numRows);
示例 1:
输入:s = “PAYPALISHIRING”, numRows = 3
输出:“PAHNAPLSIIGYIR”
示例 2:
输入:s = “PAYPALISHIRING”, numRows = 4
输出:“PINALSIGYAHRPI”
示例 3:
输入:s = “A”, numRows = 1
输出:“A”
提示:
1 <= s.length <= 1000
s 由英文字母(小写和大写)、‘,’ 和 ‘.’ 组成
1 <= numRows <= 1000
代码实现:
class Solution:
def convert(self, s: str, numRows: int) -> str:
if numRows == 1:
return s
zlist = ["" for _ in range(numRows)]
i, flag = 0, 1
for a in s:
zlist[i] += a
if i == 0 or i == numRows-1:
flag = -flag
i -= flag
return "".join(zlist)
解题思路:题目有两个输入,一个是字符串s,一个是行数numRows,最终要求输出的也是一个字符串。第一步,要将字符串s进行z字形排列,最后输出时,要按行来排列字符串,这里可以在代码中巧妙地满足这两个条件。创建一个列表,列表中先初始化三个空字符串,用列表的下标表示z字形排列时的行数,将字符串s中的每一个字符按z字形规则依次拼接到对应的行,最后再将列表中的字符串全部拼接返回。
再来看z字形规则如何排列,第一个字符从第一行开始排列,用一个变量i表示当前的行,用一个变量flag表示现在是向下排列还是向上排列,flag的值为1或-1,每从s中取完一个字符,即将行数减去flag,这样行数就会向下或向上变化。当移动到第一行或最后一行时,flag的值翻转。
当只有一行时,是题目的边界情况,此时z字形排列的结果即字符串本身,直接返回即可。
7. 整数反转
给你一个 32 位的有符号整数 x ,返回将 x 中的数字部分反转后的结果。
如果反转后整数超过 32 位的有符号整数的范围 [−2^31, 2^31 − 1] ,就返回 0。
假设环境不允许存储 64 位整数(有符号或无符号)。
示例 1:
输入:x = 123
输出:321
示例 2:
输入:x = -123
输出:-321
示例 3:
输入:x = 120
输出:21
示例 4:
输入:x = 0
输出:0
提示:
-2^31 <= x <= 2^31 - 1
代码实现:
class Solution:
def reverse(self, x: int) -> int:
y = str(x)
num = int("-" + y[:0:-1]) if y[0] == "-" else int(y[::-1])
return 0 if (num > 2 ** 31 - 1) or (num < -2 ** 31) else num
解题思路:这题虽然难度是中等,但其实实现比较简单。要将一个数字翻转,Python中可以直接将数字转换成字符串,用字符串切片的方式翻转,再转换回数字。因为输入数字可能是负数,所以做一次判断就行,并且题目要求,如果数字超过边界值时返回0,所以要判断一次结果的数字范围。代码可以直接用三元运算的语法来写,非常简洁。三元运算语法参考:详解Python中的三元运算。
8. 字符串转换整数 (atoi)
请你来实现一个 myAtoi(string s) 函数,使其能将字符串转换成一个 32 位有符号整数(类似 C/C++ 中的 atoi 函数)。
函数 myAtoi(string s) 的算法如下:
- 读入字符串并丢弃无用的前导空格。
- 检查下一个字符(假设还未到字符末尾)为正还是负号,读取该字符(如果有)。 确定最终结果是负数还是正数。
- 如果两者都不存在,则假定结果为正。 读入下一个字符,直到到达下一个非数字字符或到达输入的结尾。字符串的其余部分将被忽略。
- 将前面步骤读入的这些数字转换为整数(即,“123” -> 123, “0032” -> 32)。如果没有读入数字,则整数为 0 。
- 必要时更改符号(从步骤 2 开始)。 如果整数数超过 32 位有符号整数范围 [−2^31, 2^31 − 1],需要截断这个整数,使其保持在这个范围内。具体来说,小于 −2^31 的整数应该被固定为 −2^31 ,大于 2^31 − 1 的整数应该被固定为 2^31 − 1 。 返回整数作为最终结果。
注意:
本题中的空白字符只包括空格字符 ’ ’ 。
除前导空格或数字后的其余字符串外,请勿忽略任何其他字符。
示例 1:
输入:s = “42”
输出:42
解释:
第 1 步:“42”(当前没有读入字符,因为没有前导空格)
第 2 步:“42”(当前没有读入字符,因为这里不存在 ‘-’ 或者 ‘+’)
第 3 步:“42”(读入 “42”) 解析得到整数 42 。 由于 “42” 在范围 [-231, 231 - 1] 内,最终结果为 42 。
示例 2:
输入:s = " -42"
输出:-42
解释:
第 1 步:" -42"(读入前导空格,但忽视掉)
第 2 步:" -42"(读入 ‘-’ 字符,所以结果应该是负数)
第 3 步:" -42"(读入 “42”) 解析得到整数 -42 。 由于 “-42” 在范围 [-231, 231 - 1] 内,最终结果为 -42 。
示例 3:
输入:s = “4193 with words”
输出:4193
解释:
第 1 步:“4193 with words”(当前没有读入字符,因为没有前导空格)
第 2 步:“4193 with words”(当前没有读入字符,因为这里不存在 ‘-’ 或者 ‘+’)
第 3 步:“4193 with words”(读入 “4193”;由于下一个字符不是一个数字,所以读入停止) 解析得到整数 4193 。 由于 “4193” 在范围 [-231, 231 - 1] 内,最终结果为 4193 。
提示:
0 <= s.length <= 200
s 由英文字母(大写和小写)、数字(0-9)、’ ‘、’+‘、’-’ 和 ‘.’ 组成
代码实现:
class Solution:
def myAtoi(self, s: str) -> int:
s = s.strip()
if not s:
return 0
flag = '+'
if s[0] == '+':
s = s[1:]
elif s[0] == '-':
s = s[1:]
flag = '-'
istr = ''
for a in s:
if a in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']:
istr += a
else:
break
if not istr:
return 0
num = int(flag+istr)
if num < -2 ** 31:
return -2 ** 31
elif num > 2 ** 31 - 1:
return 2 ** 31 - 1
else:
return num
解题思路:此题在题目中已经完整地描述了步骤,代码基本就是按照这些步骤来写,并且满足每一个步骤中的要求。题目说的字符串中是可以有非数字字符的,如果有非数字字符,则找到连续的最多的数字字符,将这些字符转换成整数,如果没有数字字符,则返回整数0。代码中用一个flag变量来记录数字的正负符号,最终转换整数时拼接上flag。转换完成之后再做一次边界判断。
9. 回文数
给你一个整数 x ,如果 x 是一个回文整数,返回 true ;否则,返回 false 。
回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。
例如,121 是回文,而 123 不是。
示例 1:
输入:x = 121
输出:true
示例 2:
输入:x = -121
输出:false
解释:从左向右读, 为 -121 。 从右向左读, 为 121- 。因此它不是一个回文数。
示例 3:
输入:x = 10
输出:false
解释:从右向左读, 为 01 。因此它不是一个回文数。
提示:
-2^31 <= x <= 2^31 - 1
代码实现:
class Solution:
def isPalindrome(self, x: int) -> bool:
return str(x) == str(x)[-1::-1]
解题思路:本题也比较简单,回文数要求正序和倒序读都一样,直接将数字转换成字符串,比较正序和倒序是否相等。不过这里也可以简单分析下,有两种比较明显的非回文数的情况,一是负数,二是正数中第一个数字不可能为0,所以如果整数的最后一个数字为0(能被10整除),则不是回文数。
10. 正则表达式匹配
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。
‘.’ 匹配任意单个字符
‘*’ 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s 的,而不是部分字符串。
示例 1:
输入:s = “aa”, p = “a”
输出:false
解释:“a” 无法匹配 “aa” 整个字符串。
示例 2:
输入:s = “aa”, p = “a*”
输出:true
解释:因为 ‘*’ 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 ‘a’。因此,字符串 “aa” 可被视为 ‘a’ 重复了一次。
示例 3:
输入:s = “ab”, p = “.*”
输出:true
解释:“.*” 表示可匹配零个或多个(‘*’)任意字符(‘.’)。
提示:
1 <= s.length <= 20
1 <= p.length <= 30
s 只包含从 a-z 的小写字母。
p 只包含从 a-z 的小写字母,以及字符 . 和 *。
保证每次出现字符 * 时,前面都匹配到有效的字符。
代码实现:
class Solution:
def isMatch(self, s: str, p: str) -> bool:
m, n = len(s), len(p)
dp = [[False for _ in range(m+1)] for _ in range(n+1)]
dp[0][0] = True
for i in range(1, n+1):
if p[i-1] == '*':
if i-2 >= 0:
dp[i][0] = dp[i-2][0]
for i in range(1, n+1):
for j in range(1, m+1):
if p[i-1] == s[j-1] or p[i-1] == '.':
dp[i][j] = dp[i-1][j-1]
if p[i-1] == '*':
dp[i][j] = dp[i-2][j]
if p[i-2] == s[j-1] or p[i-2] == '.':
dp[i][j] = dp[i][j] or dp[i][j-1]
return dp[n][m]
解题思路:正则表达式中的 . 和 * 分别表示匹配任意一个字符和匹配前一个字符任意多次,现在需要自己写代码实现这两个规则。题目中的输入是两个字符串s和p,点号和星号是出现在字符串p中,我们的目标是实现正则表达式的规则,然后输出p是否能按正则的规则匹配到s。
每次从字符串p中取出一个字符或者 字符+星号 的组合,并在s中进行匹配。对于p中一个字符而言,它只能在s中匹配一个字符,匹配的方法具有唯一性,而对于p中 字符+星号 的组合而言,它可以在s中匹配任意自然数个字符,并不具有唯一性。因此,应从p的右侧往左侧来分析,如果p的最后一个字符是星号,星号仅能和它的前一个字符组合,如果p的最后一个字符不是星号,则它只能与s中的一个字符比较。这样从右往左分析,要判断s与p是否匹配,就是需要判断最右端是否匹配,以及剩余的子串是否匹配(子问题),这样就可以用动态规划的方法来求解。动态规划可以参考:循序渐进,搞懂什么是动态规划。
按照动态规划的解题步骤,第一步先定义问题的状态,用dp[i][j]表示p的前i个字符和s的前j个字符能否匹配,如果字符串p的长度为n,字符串s的长度为m,则dp[n][m]就是问题的答案。
第二步为列出状态转移方程,如果p[i]==s[j],说明p的第i个字符和s的第j个字符可以匹配,那么就看前面的字符是否可以匹配,状态转移方程:dp[i][j]=dp[i−1][j−1]。如果p[i]==‘.’,因为它可以代表任何字符,所以一定可以和s的第j个匹配上,也只需要看前面的字符是否可以匹配,状态转移方程:dp[i][j]=dp[i−1][j−1]。因为星号可以匹配0次、1次或多次,所以如果p[i]==‘*’,又需要分情况讨论。
如果p的第i个字符是星号,那么能不能与s的第j个字符匹配上,就看星号的前一个字符,也就是第i-1个字符。如果p[i-1]与s[j]匹配不上,那星号可以表示匹配0次,状态转移方程:dp[i][j]=dp[i−2][j]。如果p[i-1]与s[j]匹配,那么可以匹配上,此时星号表示匹配1次,同时,要考虑星号匹配多次的情况,也就是说,s[j]前还可能有与s[j]相等的字符,所以,这里p[i]的星号与p[i-1]的字符组合匹配s[j]后,p[i]的星号与p[i-1]继续保留,重复利用(这样就可以把匹配多次转换成反复匹配1次),直到遇到匹配0次的情况,p[i]的星号才利用完,状态转移方程:dp[i][j]=dp[i][j−1]。当然,这里并不是星号一定要匹配尽可能多的字符,可以介于于正则表达式中的“贪婪模式”与“非贪婪模式”之间,比如点号和星号组合可以把s中的字符全部匹配完,但是字符串p的前面还有其他字符,所以这里点号和星号组合就不能全部匹配完,要留适当的一部分字符给p中前面的这些字符匹配,所以要允许状态不变,即dp[i][j]=dp[i][j]。
第三步为状态初始化,如果字符串s和字符串p都是空字符串,是可以匹配的,初始化为:dp[0][0]=True,并初始化一个长度为n+1乘m+1的dp数组。因为p中的星号可以表示0次,所以还有可能s为空,p不为空,如p=‘a*’, p='a*b*'等,这种情况s为空,在上面的状态转移方程中不存在s[j],所以也要进行初始化。
关于初始化,本题中还有一个注意事项,因为初始化dp[0][0]是字符串s和p都为空,所以字符串非空时,dp[i][j]中的i和j是从1开始的,而字符串的索引是从0开始的,所以索引要比状态编号减一,也就是说,上面的分析中,在代码中s或p的索引要再减1。同时,因为状态编号从1开始,所以代码需要遍历到m+1和n+1,也是dp数组大小为n+1乘m+1的原因。
当然,此题也可以用递归的方式实现,参考下方代码(不再具体分析了)。不过,使用递归时,时间复杂度满足不了力扣的要求,这就需要用到Python中的@cache装饰器,通过缓存来降低时间复杂度。参考:Python中的@cache有什么妙用?。
class Solution:
def isMatch(self, s: str, p: str) -> bool:
@cache
def DFS(i, j):
if j == len(p):
return i == len(s)
firstMatch = i < len(s) and (s[i] == p[j] or p[j] == '.')
if j + 1 < len(p) and p[j + 1] == '*':
return DFS(i, j + 2) or (firstMatch and DFS(i + 1, j))
return firstMatch and DFS(i + 1, j + 1)
return DFS(0, 0)
相关阅读
PythonCode】力扣Leetcode1~5题Python版
📢欢迎 点赞👍 收藏⭐ 评论📝 关注❤ 如有错误敬请指正!
☟ 学Python,点击下方名片关注我。☟