leetcode hot100 中等难度 day05-刷题
day05 leetcode hot100题单
1. 前言
对应资料
leetcode网站:https://leetcode.cn/
hot100题单:https://leetcode.cn/problem-list/2cktkvj/
说明
本人没有系统的刷过题目,临时抱佛脚,先从hot100开始刷,这是第一次刷。写博客,是强迫自己刷题。当前是第二天刷题,与前一次刷题隔了n天(国庆节放飞自我了)。
- 从简单难度开始往后刷。
- 这是第一遍刷题,主要目的是通过,而不是追求效率。
- 语言采用的python
目录
文章目录
- day05 leetcode hot100题单
- 1. 前言
- 2. 中等 - 不同路径
- 3. 中等 - 最小路径和
- 4. 中等 - 编辑距离
- 5. 中等 - 颜色分类
- 6. 中等 - 子集
- 7. 中等 - 单词搜索
- 8. 中等 - 不同的二叉搜索树
- 9. 中等 - 验证二叉搜索树
- 10. 中等 - 二叉树的层序遍历
- 11. 总结
2. 中等 - 不同路径
题目
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
思考
这非常明显是一个动态规划问题。
思考:无论如何,想要走到右下角,必须向右走n-1步,向下走m-1步,无非就是什么时候向右、向下造成了不同的解法。
因此,可以这么解决:
- 如果选择向右走1步,它还可以选择的就是向右n-2步,向下m-1步
- 如果选择向下走1步,…
这个问题的边界条件是:当走完的时候,维护全局总路径数目+1
实现
- 照着上面这个思路,可以轻松地写下下面的代码,但是运行到后面,超出时间限制了
class Solution:def uniquePaths(self, m: int, n: int) -> int:# 全局变量ans = 0# 定义递归函数def dfs(row,column):# 边界条件if row == 0 and column == 0:nonlocal ansans += 1return None# 选或者不选if row >= 1: # 表示可以选择向下走dfs(row-1,column)if column >= 1:dfs(row,column-1)dfs(m-1,n-1)return ans
- 对上面的代码进行优化,可以不用全局变量来更新,直接用递归去体现,然后用@cache装饰器,可以避免重复计算已经计算出的dfs值,如下:
class Solution:def uniquePaths(self, m: int, n: int) -> int:# 定义递归函数@ cachedef dfs(row,column):# 边界条件if row < 0 or column < 0:return 0if row == 0 and column == 0:return 1return dfs(row-1,column) + dfs(row,column-1)return dfs(m-1,n-1)
3. 中等 - 最小路径和
题目
给定一个包含非负整数的 m x n
网格 grid
,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
**说明:**每次只能向下或者向右移动一步。
思考
这道题和上面很类似,我们尝试用上一道题的思路去解决这个问题。
首先出发点还是在左上角,对于向右走还是向下走,不能单单看右边和下边的哪个值大,还应该判断后续的和哪个大,比如下面:
不能因为1比3小,就选择走1。
应该这么想,如果我走3,后续最小的值是多少,如果我走1,后续最小值是多少。
那么,假设为m*n,dfs(m,n)
- 如果向右走1步,此时路径和temp,递归dfs(m,n-1)
- 如果向下走1步,此时路径和temp,递归dfs(m-1,n)
- 判断两者的大小,取其中小的那个作为return的值
边界条件:如果m=n=0,说明正常结束,返回即可,或者m/n中有小于0的,说明错误路径
实现
class Solution:def minPathSum(self, grid: List[List[int]]) -> int:m = len(grid)n = len(grid[0])@ cachedef dfs(row,column):# 边界条件if row < 0 or column < 0:return infif row == 0 and column == 0:return grid[row][column]# 判断大小return min(dfs(row,column-1),dfs(row-1,column)) + grid[row][column]return dfs(m-1,n-1)
4. 中等 - 编辑距离
题目
给你两个单词 word1
和 word2
, 请返回将 word1
转换成 word2
所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
示例
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
思考
假设两者长度为m、n,我们从末尾开始判断:
- 如果word[m] = word[n],那么,这表示不操作,直接进行下一步dfs(m-1,n-1)
- 如果word[m] != word[n],那么有三种操作方式,插入、删除、替换,但是三者的代价是不同的,比如万一前面有相同的字符呢?因此,我们需要返回三者中代价最小的那个即可。即,min( dfs(m-1,n), dfs(m,n-1) , dfs(m-1,n-1) ) ,这三个分别对应:删除 - 需要判断新的索引是否相等、插入 - word2变化,word1索引不变、 替换 - 两者匹配了,都需要变化
然后,边界条件:
- 如果m<0 或者 n < 0:返回另外一个字符串剩下的+1,比如上面的字符串,匹配结束后,还剩下h,这个无论删除也好,还是替换,都是需要处理它
- 如果两个元素相同,也直接下一步即可,因为不会占次数
实现
class Solution:def minDistance(self, word1: str, word2: str) -> int:# 初始化m,n = len(word1),len(word2)# 递归@cachedef dfs(i,j):# 边界条件if i < 0:return j + 1if j < 0:return i + 1if word1[i] == word2[j]:return dfs(i-1,j-1)# 正常递归,只是每次操作,需要+1return min(dfs(i-1,j),dfs(i,j-1),dfs(i-1,j-1)) + 1return dfs(m-1,n-1)
5. 中等 - 颜色分类
题目
给定一个包含红色、白色和蓝色、共 n
个元素的数组 nums
,原地 对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。
我们使用整数 0
、 1
和 2
分别表示红色、白色和蓝色。
必须在不使用库内置的 sort 函数的情况下解决这个问题。
思考
我看了下提示,数组的最大长度不超过300,然后要求不创建新的数组返回,直接对原数组进行修改。
我现在的想法是,迭代数组,创建一个哈希表,统计0、1、2的个数,然后直接取修改数组,尝试一下。
实现
- 可以正常通过
class Solution:def sortColors(self, nums: List[int]) -> None:"""Do not return anything, modify nums in-place instead."""hs = defaultdict(int)# 统计个数for v in nums:if v in hs:hs[v] += 1else:hs[v] = 1len_0 = hs[0]len_1 = hs[1]+hs[0]len_2 = hs[2]+hs[1]+hs[0]# 直接修改数组for i,v in enumerate(nums):if i < len_0:nums[i] = 0elif i < len_1:nums[i] = 1else:nums[i] = 2
6. 中等 - 子集
题目
给你一个整数数组 nums
,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
思考
这道题和上面的路径问题有点类似,都是选不选的问题,对于这种无法直接找出结果的问题,多半涉及递归。
这么去想:
- 如果选择i作为子集一元,路径更新,就是递归后续的i+1元素是否选择,记得维护现场
- 如果不选择i作为一元,直接递归后续的I+1
边界条件:如果递归到n了,说明路径已经凑齐了,此时全局变量添加此时的路径即可
实现
class Solution:def subsets(self, nums: List[int]) -> List[List[int]]:n = len(nums)ans = []path = []# 递归def dfs(i):# 边界条件if i == n:ans.append(path.copy())return None# 选择或者不选dfs(i+1) # 不选# 选择,记得更新路径和维护现场path.append(nums[i])dfs(i+1)path.pop()dfs(0)return ans
7. 中等 - 单词搜索
题目
给定一个 m x n
二维字符网格 board
和一个字符串单词 word
。如果 word
存在于网格中,返回 true
;否则,返回 false
。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
思考
这道题和上面的路径基本一模一样,只是最终返回的结果是能不能找到满足条件的路径。
但是,这道题的特殊点,在于起点不是左上角,终点也不是右下角这样限制条件,而是开放条件。因此,首先关注起点是啥(函数的输入是啥),终点是啥(函数的边界条件是啥)。
起点:每一个坐标(i,j) + 当前判断到word中的哪个位置了,定为k。(参考灵神题解)
那么算法应该是这样的:
def dfs(i,j,k):# 边界条件1. 如果 board[i][j] != word[k],说明当前这个起点并不是合适的起点,返回False2. 如果k = len(word),说明已经正常走完了程序,找到了路径,返回True# 递归1. 为了防止重复访问某个元素,我们需要标明这个元素以及访问过了board[i][j] = ''2. 子问题:去枚举访问相邻位置3. 如果这个位置合法 and dfs(x,x,k+1),则返回True1. 维护现场board[i][j] = word[k]# 4. 如果上面都执行完了,说明没有找到return False
实现
- 如果能够理清上面的算法,并且想到如何维护现场,写出代码并不难
class Solution:def exist(self, board: List[List[str]], word: str) -> bool:m,n = len(board),len(board[0])# 递归def dfs(i,j,k):# 边界条件if board[i][j] != word[k]:return Falseif k == len(word) - 1:return True# 递归board[i][j] = ''for x,y in [(i,j-1),(i,j+1),(i-1,j),(i+1,j)]:if 0 <= x < m and 0 <= y < n and dfs(x,y,k+1):return Trueboard[i][j] = word[k]return False# 执行函数ans = []for i in range(m):for j in range(n):ans.append(dfs(i,j,0))return any(ans)
8. 中等 - 不同的二叉搜索树
题目
给你一个整数 n
,求恰由 n
个节点组成且节点值从 1
到 n
互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。
二叉搜索树:
-
若任意结点的左子树不空,则左子树上所有结点的值均不大于它的根结点的值。
-
若任意结点的右子树不空,则右子树上所有结点的值均不小于它的根结点的值。
思考
这明显是一个动态规划的问题。按照套路来思考一下。注意:二叉搜索树,左节点小于根节点,右节点大于根节点。
先把它当作一个回溯问题,那么标准流程:
- **初始化:**这里由于只需要考虑数量,因此不需要path变量存储组合形式,因此我们可以通过参数节点个数来表示递归对象,这样节点数的减少,表明当前递归的进行深度,当节点数只有1个的时候说明该结束运行了
- **当前操作是什么?**我觉得是,枚举每一个节点作为根节点
- **子问题是啥?**然后递归去求解左子树的二叉搜索树、递归去求解右子树的二叉搜索树,两者相乘的结果就是总的可能数目
- **边界条件是啥?**如果节点数目小于等于1,返回1.
实现
- 先根据回溯去实现这个问题:可以正常解决,如果想要提高效率,需要改为递推的方式来解决,这里暂时不改,后期再次实现的时候来思考。
class Solution:def numTrees(self, n: int) -> int:# 回溯@cachedef dfs(n):# 边界条件if n <= 1:return 1count = 0# 当前操作for i in range(1,n+1):# 左子树left = dfs(i-1)# 右子树right = dfs(n-i)# 结果count += left*rightreturn countreturn dfs(n)
9. 中等 - 验证二叉搜索树
题目
给你一个二叉树的根节点 root
,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
- 节点的左子树只包含 严格小于 当前节点的数。
- 节点的右子树只包含 严格大于 当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
思考
这就是一个普通递归问题。我们以遍历这个二叉树的方式去思考如何解决它:
- 子问题是啥:左子树是否为有效二叉搜索树,右子树是否为有效二叉搜索树
- 判断操作是啥:当前节点记为node,如果node.left < node < node.right,就正常递归下去即可,否则返回False
- 边界条件是啥:如果遇到叶子节点,说明正常递归完,返回True即可
ok,再考虑一下递归的模板,开始实现。
实现过程中,发现一个问题:上面的判断没用考虑到当前树允许的最大值或者最小值是多少,比如我根节点通过node.left < node < node.right,但是其左子树的左右子树都必须小于根节点的值吗,这一点我没有考虑到。因此上面的思路需要维护一个变量来维护。
实现
- 我最初版本的代码。忽略了最值的维护:
class Solution:def isValidBST(self, root: Optional[TreeNode]) -> bool:# 边界条件if root.left is None and root.right is None:return True# 处理一下只有一个叶子节点情况if root.left is None:return root.right.val > root.valif root.right is None:return root.left.val < root.val# 子问题与判断if root.left.val < root.val and root.right.val > root.val:return self.isValidBST(root.left) and self.isValidBST(root.right)else:return False
- 更新后
class Solution:def isValidBST(self, root: Optional[TreeNode],left=-inf,right=inf) -> bool:# 边界条件if root is None:return True# 子问题与判断# 1. 必须满足 left < node < right,其中left为左边的最大值,right为右边的最小值# 2. 左子树满足条件 + 右子树满足条件(记得维护最值)return (left < root.val < right) and self.isValidBST(root.left,left,root.val) and self.isValidBST(root.right,root.val,right)
10. 中等 - 二叉树的层序遍历
题目
给你二叉树的根节点 root
,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
思考
二叉树的四种遍历方式,基本是必背的题目。
而层序遍历又是最为特殊的,它需要一个对列 + while循环来实现。记住这个代码就行,并且理解它。
这里我分析一下如何实现:
- 初始化:我们需要一个结果列表ans、一个当前层的节点列表cur
- 第一重迭代,只要当前层不为空,我们就需要继续处理,用while cur实现
- 然后,我们需要一个列表存储下一个层,并且在最后赋值给cur,还需要一个列表记录当前层的值
- 接着,二重迭代,去访问cur里面的每个值,然后按照左右子树的先后顺序添加到下一个层中即可
实现
class Solution:def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:# 特殊情况if root is None:return []# 初始化cur = [root] # 第一层只有rootans = []# 第一重while cur:# 初始化nxt = []val = []# 第二重for node in cur:val.append(node.val)# 更新层if node.left: nxt.append(node.left)if node.right: nxt.append(node.right)# 赋值cur = nxtans.append(val)return ans
11. 总结
今日收获:
- 二叉树层序遍历思考路径:
- 初始化:我们需要一个结果列表ans、一个当前层的节点列表cur
- 第一重迭代,只要当前层不为空,我们就需要继续处理,用while cur实现
- 然后,我们需要一个列表存储下一个层,并且在最后赋值给cur,还需要一个列表记录当前层的值
- 接着,二重迭代,去访问cur里面的每个值,然后按照左右子树的先后顺序添加到下一个层中即可
- 动态规划的思考路径:先当作回溯问题,再记忆优化(python直接加个装饰器@cache),最后再变为递推(需要找到公式)