2022年4月4日 - 2022年4月10日
72. 编辑距离(Hard)
编辑距离算是动态规划里面难度比较高的一道题了,但是也并不是很难,只要知道了思路。
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
# if not word1: return len(word2)
# if not word2: return len(word1)
m, n = len(word1)+1, len(word2)+1
dp = [[0 for i in range(n)] for j in range(m)]
for i in range(m):
dp[i][0] = i
for j in range(n):
dp[0][j] = j
for i in range(1, m):
for j in range(1, n):
dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1
if word1[i-1] == word2[j-1]:
dp[i][j] = min(dp[i][j], dp[i-1][j-1])
return dp[-1][-1]
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
m, n = len(word1), len(word2)
dp = [[0] * (n+1) for _ in range(m+1)] # 横轴表示*ros,*表示什么也没有。纵轴同理
# 初始化行
for i in range(n+1):
dp[0][i] = i
# 初始化列
for j in range(m+1):
dp[j][0] = j
# 动态规划
for i in range(1, m+1):
for j in range(1, n+1):
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1
return dp[-1][-1]
代码很简单,要注意的点有:
-
初始化:初始化需要对第一行和第一列进行初始化。初始化的值等于他的行下标或者列下标。
-
递推公式:对于 d p [ i ] [ j ] dp[i][j] dp[i][j],表示的是 w o r d 1 [ i − 1 ] word1[i-1] word1[i−1]转化为 w o r d 2 [ j − 1 ] word2[j-1] word2[j−1]的操作次数。它有若干种情况:
-
case1: w o r d 1 [ i − 1 ] = = w o r d 2 [ j − 1 ] word1[i-1] == word2[j-1] word1[i−1]==word2[j−1],此时在这一位不需要做任何操作,直接让 d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] dp[i][j] = dp[i-1][j-1] dp[i][j]=dp[i−1][j−1]
-
case2: w o r d 1 [ i − 1 ] ! = w o r d 2 [ j − 1 ] word1[i-1] != word2[j-1] word1[i−1]!=word2[j−1],这时候有三种子情况:
1:
w
o
r
d
1
[
i
−
2
]
word1[i-2]
word1[i−2]加一个字符变成
w
o
r
d
2
[
j
−
1
]
word2[j-1]
word2[j−1]
2:
w
o
r
d
1
[
i
−
2
]
word1[i-2]
word1[i−2]先变成
w
o
r
d
2
[
j
−
1
]
word2[j-1]
word2[j−1],然后再删除
w
o
r
d
1
[
j
−
1
]
word1[j-1]
word1[j−1]
3:
w
o
r
d
1
[
i
−
1
]
word1[i-1]
word1[i−1]先变成
w
o
r
d
[
j
−
2
]
word[j-2]
word[j−2],然后再加一位
在上面三者之间取最小值,然后加1就行了。
举个例子:
* | r | o | s | |
---|---|---|---|---|
* | 0 | 1 | 2 | 3 |
h | 1 | 1 | 2 | 3 |
o | 2 | 2 | 1 | 2 |
r | 3 | |||
s | 4 | |||
e | 5 |
我们看 d p [ 2 ] [ 3 ] dp[2][3] dp[2][3],他的意思是把 h o ho ho变成 r o ro ro,因为 o o o相等,所以,把 h o ho ho变成 r o ro ro就相当于把 h h h变成 r r r。所以 d p [ 2 ] [ 3 ] = d p [ 1 ] [ 2 ] dp[2][3] = dp[1][2] dp[2][3]=dp[1][2]
再看 d p [ 2 ] [ 4 ] dp[2][4] dp[2][4],他的意思是把 h o ho ho变成 r o s ros ros,因为 o , s o, s o,s不相等,所以,有三种变法:
- h o ho ho -> h h h -> r o s ros ros: 先删除 o o o,然后再变成 r o s ros ros,中间有一步删除操作, d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + 1 dp[i][j] = dp[i-1][j] + 1 dp[i][j]=dp[i−1][j]+1
- h o ho ho -> r o ro ro -> r o s ros ros:先变成 r o ro ro,再添加 s s s,最后有一步添加操作, d p [ i ] [ j ] = d p [ i ] [ j − 1 ] + 1 dp[i][j] = dp[i][j-1] + 1 dp[i][j]=dp[i][j−1]+1
- h h h -> r o ro ro -> h o ho ho -> r o s ros ros:也就是先把 h o ho ho的 h h h变成 r o ro ro,再把 o o o变成 s s s,对应修改操作。 d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + 1 dp[i][j] = dp[i-1][j-1] + 1 dp[i][j]=dp[i−1][j−1]+1
53. 最大子数组和(Easy)
很直接可以想到动态规划。
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
n = len(nums)
dp = [0] * n
# 初始化
dp[0] = nums[0]
for i in range(1, n):
dp[i] = max(dp[i-1] + nums[i], nums[i])
return max(dp)
动态规划需要用到do数组,其实可以进一步简化,仅占用 O ( 1 ) O(1) O(1)的空间
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
res = -float('inf')
cur = 0
for i in range(len(nums)):
cur = max(nums[i] + cur, nums[i])
res = max(cur, res)
return res
15. 三数之和
很笨的方法,使用哈希表存储每一个数字出现的次数,然后两层循环去遍历。其中需要注意维护一个哈希表 s e e n seen seen时候,里面的元素应该是不可变的。
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
n = len(nums)
if n < 3: return []
# 统计每一个数字出现的次数
dic = collections.Counter(nums)
seen = set()
res = []
for k1 in dic.keys():
for k2 in dic.keys():
# 如果k1,k2一样,但是只有一个k1,那么说明不会存在[k1, k1, 0-2k1]。这个条件可以保证没有一个数被复用
if k1 == k2 and dic[k1] == 1:
continue
dic[k1] -= 1
dic[k2] -= 1
if dic[0 - k1 - k2] >= 1:
# 去重
if tuple(sorted([k1, k2, 0-k1-k2])) not in seen:
seen.add(tuple(sorted([k1, k2, 0-k1-k2])))
res.append([k1, k2, 0-k1-k2])
# 再恢复到原来的数量
dic[k1] += 1
dic[k2] += 1
return res
这是跟高级的写法,循环加二分。
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
n = len(nums)
if not nums or n < 3:
return []
nums.sort() # 排序
res = []
for i in range(n):
if nums[i] > 0:
return res
if i > 0 and nums[i] == nums[i-1]:
continue
L = i+1
R = n-1
while L < R:
if nums[i] + nums[L] + nums[R] == 0:
res.append([nums[i], nums[L], nums[R]])
# 如果有重复的
while(L<R and nums[L] == nums[L+1]):
L += 1
while(L<R and nums[R] == nums[R-1]):
R -= 1
L += 1
R -= 1
elif (nums[i] + nums[L] + nums[R]) > 0:
R -= 1
elif (nums[i] + nums[L] + nums[R]) < 0:
L += 1
return res
204. 计数质数
暴力法很简单,遍历每一个数字,然后判断这个数字是不是质数,这样显而易见,复杂度很高,几乎是 O ( n 2 ) O(n^2) O(n2)的,这样是会超时的。另外一种解法是:
- 将n个数字存起来
- 从2往后遍历到 n − 1 n-1 n−1,每遍历一个数 i i i,把所有 i i i的倍数都置成0(从二倍开始),最终剩下的就是非0元素就是所有小于 n n n的质数了。因为答案只要求个数,所以为了降低空间复杂度,只需要维护n个布尔值即可。
下面有两种将 i i i的倍数置零的方法,一种while循环,一种for循环,提交发现for循环效率更高,可能是因为都是加法。
class Solution:
def countPrimes(self, n: int) -> int:
if n < 2: return 0
matrix = [True] * n
matrix[0] = False
matrix[1] = False
count = 0
for i in range(2, n):
if matrix[i]:
count += 1
# while的方法
# m = 2
# while i * m < n:
# matrix[i * m] = False
# m += 1
# for的方法
for j in range(i+i, n, i):
matrix[j] = False
return count
198. 打家劫舍
d p [ i ] dp[i] dp[i]可以等于前面 i − 2 i-2 i−2个位置的最大值,加上当前位置的值。但是这样做每次都要求前面一部分的最大值,然后dp数组的最后一位村的还不是题目的解,而是dp数组中的最大值才是题目的解。整体来是,复杂度还是高,只不过有max函数,所以写出来还hi是比较简单的。
class Solution:
def rob(self, nums: List[int]) -> int:
n = len(nums)
if n < 3: return max(nums)
if n == 3: return max(nums[0] + nums[2], nums[1])
dp = [0] * n
dp[0] = nums[0]
dp[1] = nums[1]
dp[2] = dp[0] + nums[2]
for i in range(3, n):
dp[i] = max(dp[:i-1]) + nums[i]
return max(dp)
另外一种写法就是按照 d p [ i ] dp[i] dp[i]存储到第 i i i个位置能偷的最大值,第 i i i位不一定需要偷。
那么这种情况的递推公式是什么呢?
d
p
[
i
]
=
m
a
x
(
d
p
[
i
−
2
]
+
n
u
m
s
[
i
]
,
d
p
[
i
−
1
]
)
dp[i] = max(dp[i-2]+nums[i], dp[i-1])
dp[i]=max(dp[i−2]+nums[i],dp[i−1]),意思是,从第一家到第i家,能偷的最大数等于到第i-1家能偷的最大值或者等于第i-2家能偷的最大值加上第i家。就看谁更大了。
class Solution:
def rob(self, nums: List[int]) -> int:
n = len(nums)
if n == 0: return 0
if n == 1: return nums[0]
# 定义dp数组
dp = [0] * n
# 初始化dp数组
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
# 递推
for i in range(2, n):
dp[i] = max(dp[i-2] + nums[i], dp[i-1])
return dp[n-1]
442. 数组中重复的数据
方法一:比较简单,但是需要额外占用空间。空间复杂度为 O ( n ) O(n) O(n)
class Solution:
def findDuplicates(self, nums: List[int]) -> List[int]:
visited = set()
res = []
for i in range(len(nums)):
if nums[i] not in visited:
visited.add(nums[i])
else:
res.append(nums[i])
return res
class Solution:
def findDuplicates(self, nums: List[int]) -> List[int]:
res = []
for i in nums:
i = abs(i)
if nums[i-1] > 0:
nums[i-1] *= -1
else:
res.append(i)
return res
99. 恢复二叉搜索树
笨方法思路很简单,因为二叉搜索树的中序遍历是递增序列,于是我们把二叉搜索树进行中序遍历,然后遍历得到的nums数组,找到两个异常值。之后再恢复二叉搜索树即可。
注意的点:
-
怎么找到两个异常值,比如 [ 3 , 2 , 1 ] [3,2,1] [3,2,1],其中3和1是异常的,当便利的时候,第一次遇到遇到后者比前者小的,那么前者肯定是异常值,但是此时后者不一定也是异常值。在这个例子中,第一次遇到前者比后者大的情况是 [ 3 , 2 ] [3, 2] [3,2],此时3是异常值,记录 p 1 = 3 p1 = 3 p1=3但是2还不知道是不是异常值,不过因为是第一次遇到异常情况,所以3肯定是异常值,先把3记下来,也暂时把2认为是异常值 p 2 = 2 p2 = 2 p2=2。然后继续遍历 [ 2 , 1 ] [2,1] [2,1],又是后者比前者小,此时第二次遇见异常情况,那么1这时候肯定是异常值了,于是我们更新之前记录的异常值 p 2 = 1 p2 = 1 p2=1。因为第二次遇到异常情况,所以 p 1 ≠ − 1 p1 \neq -1 p1=−1,这时就不再更新 p 1 p1 p1了。
-
试想 [ 3 , 2 ] [3,2] [3,2],第一次遇到异常就是 [ 3 , 2 ] [3,2] [3,2],此时直到3是异常值,如果不记录3后面的数字,那么循环就结束了。所以必须每次遇到异常值都记录两个数字,只不过第一次是都记录,如果有第二次的话,再去更新 p 2 p2 p2,如果没有第二次,那么之前记录的就是不正常的。
然后是恢复二叉搜索树,一种做法是对每一个节点重新赋值,另一种解法是只考虑异常的两个节点,首先要了解每个结点的值都是唯一的,那么我们知道了两个异常的值是 x , y x, y x,y,只需要找到 x x x,把它换成 y y y,找到 y y y,把它换成 x x x不就好了吗。不需要对每一个节点都进行一次赋值。
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def recoverTree(self, root: Optional[TreeNode]) -> None:
"""
Do not return anything, modify root in-place instead.
"""
# 搜索树的中序遍历是递增序列
nums = []
def backtrack(root):
if not root: return
backtrack(root.left)
nums.append(root.val)
backtrack(root.right)
backtrack(root)
# 找到nums中的异常元素
p1, p2 = -1, -1
for i in range(0, len(nums)-1):
if nums[i] > nums[i+1]:
p2 = i + 1
if p1 == -1:
p1 = i
else:
break
# 重构二叉搜索树
def recover(root, x, y):
if root:
if root.val == x or root.val == y:
root.val = y if root.val == x else x
recover(root.left, x, y)
recover(root.right, x, y)
recover(root, nums[p1], nums[p2])
这个做法很耗时,你需要遍历两次树,还需要对树上所有值进行一次遍历。虽然每一次的复杂度都不算高,整体也不会增加量级,但是还是会慢的。
上面的做法就是显式的后序遍历,其实也可以不把书上所有节点拿下来,而是在便利的过程中进行修改。不过比较难。更高级的做法是 M o r r i s Morris Morris中序遍历,但是据说这样做难度就到 H a r d Hard Hard了。
114. 二叉树展开为链表
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def flatten(self, root: TreeNode) -> None:
"""
Do not return anything, modify root in-place instead.
"""
# 那就先序遍历呗
nums = []
def backtrack(root):
if not root: return
nums.append(root.val)
backtrack(root.left)
backtrack(root.right)
backtrack(root)
# 有了nums数组之后,开始对树进行修改
# 因为只要根节点不动,就不会影响结果。所以就把他当作一个root开头的链表,每次往后面追加一个新节点就完了。不需要考虑树的插入什么的。
i = 1
for i in range(1, len(nums)):
if root.left:
root.left = None
root.right = TreeNode(nums[i])
root = root.right