【日常】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[i1]转化为 w o r d 2 [ j − 1 ] word2[j-1] word2[j1]的操作次数。它有若干种情况:

  • case1: w o r d 1 [ i − 1 ] = = w o r d 2 [ j − 1 ] word1[i-1] == word2[j-1] word1[i1]==word2[j1],此时在这一位不需要做任何操作,直接让 d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] dp[i][j] = dp[i-1][j-1] dp[i][j]=dp[i1][j1]

  • case2: w o r d 1 [ i − 1 ] ! = w o r d 2 [ j − 1 ] word1[i-1] != word2[j-1] word1[i1]!=word2[j1],这时候有三种子情况:

1: w o r d 1 [ i − 2 ] word1[i-2] word1[i2]加一个字符变成 w o r d 2 [ j − 1 ] word2[j-1] word2[j1]
2: w o r d 1 [ i − 2 ] word1[i-2] word1[i2]先变成 w o r d 2 [ j − 1 ] word2[j-1] word2[j1],然后再删除 w o r d 1 [ j − 1 ] word1[j-1] word1[j1]
3: w o r d 1 [ i − 1 ] word1[i-1] word1[i1]先变成 w o r d [ j − 2 ] word[j-2] word[j2],然后再加一位

在上面三者之间取最小值,然后加1就行了。

举个例子:

*ros
*0123
h1123
o2212
r3
s4
e5

我们看 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[i1][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][j1]+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[i1][j1]+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 n1,每遍历一个数 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 i2个位置的最大值,加上当前位置的值。但是这样做每次都要求前面一部分的最大值,然后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[i2]+nums[i],dp[i1]),意思是,从第一家到第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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值