献芹奏曝-Python面试题-算法-动态规划篇
上一篇:献芹奏曝-Python面试题
开篇的话:本文目的是收集和归纳力扣上的算法题,希望用python语言,竭我所能做到思路最清奇、代码最简洁、方法最广泛、性能最高效,了解常见题目,找到最利于记忆的答案,更加从容的应对面试。希望广思集益,共同进步。
动态规划篇
-
70. 爬楼梯(难度系数✯)
class Solution: def climbStairs(self, n: int) -> int: if n == 1: return 1 elif n == 2: return 2 else: return self.climbStairs(n - 1) + self.climbStairs(n - 2)
运行结果:1:超时
知识点/技巧: 递归调用,会有大量重复计算
class Solution: dict_result = {} def climbStairs(self, n: int) -> int: if self.dict_result.get(n): return self.dict_result.get(n) if n == 1: self.dict_result[n] = 1 return 1 elif n == 2: self.dict_result[n] = 2 return 2 else: self.dict_result[n] = self.climbStairs(n - 1) + self.climbStairs(n - 2) return self.dict_result[n]
运行结果:1:耗时超过54%。2:内存超过79%
知识点/技巧:通过字典,将计算过的值保存,减少重复计算
class Solution: def climbStairs(self, n: int) -> int: if n == 1: return 1 a = [1] * (n + 1) a[1] = 1 a[2] = 2 for i in range(3, n+1): a[i] = a[i - 1] + a[i - 2] return a[n]
运行结果:1:耗时超过79%。2:内存超过89%
知识点/技巧:通过数组,求和的速度更快,根据索引取值,比字典查找更快
class Solution: def climbStairs(self, n: int) -> int: if n == 1: return 1 first = 1 second = 2 for i in range(3, n + 1): first, second = second, first + second return second
运行结果:1:耗时超过79%。2:内存超过75%
知识点/技巧:通过数字滚动的方式,但是疑惑的是,并没发现内存空间提升多少
当然还有一些其他方法,矩阵法、通项公式法,这里不一一介绍 -
53. 最大子数组和(难度系数✯)
class Solution: def maxSubArray(self, nums: List[int]) -> int: # 构造和目标数组一样长度的数组 r = nums.copy() for i in range(1, len(nums)): if r[i - 1] > 0: r[i] += r[i - 1] return max(r)
运行结果:1:耗时超过78%。2:内存超过15% 。
知识点/技巧:
为了便于理解记忆:可以想象成一个赌徒赌博,什么时候,手中筹码最多,
我们记录每一把赌牌后的手中筹码数,当然有一点不同的是,
如果上一步赢了,或者还有剩余,我们财富积累,
如果上一步钱输光了,往事随风,另起炉灶,重新开始,从当前记录class Solution: def maxSubArray(self, nums: List[int]) -> int: if len(nums) == 1: return nums[0] top_one = nums[0] for i in range(1, len(nums)): if nums[i - 1] > 0: nums[i] += nums[i - 1] if nums[i] > top_one: top_one = nums[i] return top_one
运行结果:1:耗时超过70%。2:内存超过36%
知识点/技巧:通过数字滚动的方式,节省内存
-
416. 分割等和子集(难度系数✯)
""" 1:要想使得分割成后的两个子集和相等,那么:和必须为偶数 2:我们马上可以转换成0-1背包问题:即每个物品可以出现0次或1次 3: 0 1 2 3 4 5 6 7 8 9 10 11 第一个物品放或者不放 √ √ 第二个物品放或者不放 √ √ √ √ 如果第二个物品5没放,那就是原来的值,如果5放了,那就是在原来的基础上向后移动5个单位 第三个物品放或者不放 √ √ √ √ √ √ 但是需要注意的是放与不放这个动作一气呵成, """ from typing import List class Solution: def canPartition(self, nums: List[int]) -> bool: s = sum(nums) if s % 2: # 1:要想使得分割成后的两个子集和相等,那么:和必须为偶数 return False target = s // 2 a = [False] * (target + 1) a[0] = True for index_n, n in enumerate(nums): if n > target: # 2:如果遍历的元素比目标值大,说明没有找到结果,返回False return False change_index = [] for index, item in enumerate(a): if item: if index + n > target: for c_index in change_index: a[c_index] = True break if index + n == target: return True else: change_index.append(index + n) else: # 为什么只有在退出的时候才统一赋值,[1,2,5] 此时会出错 for c_index in change_index: a[c_index] = True return False
运行结果:1:耗时超过95%。2:内存超过83%
知识点/技巧: 转换成01背包问题,详情看代码中注释
class Solution: def canPartition(self, nums: List[int]) -> bool: s = sum(nums) if s % 2: # 1:要想使得分割成后的两个子集和相等,那么:和必须为偶数 return False target = s // 2 a = [False] * (target + 1) a[0] = True for index_n, n in enumerate(nums): if n > target: # 2:如果遍历的元素比目标值大,说明没有找到结果,返回False return False for i in reversed(range(0, target - n + 1)): if a[i]: if i + n == target: return True a[i + n] = True return False
运行结果:1:耗时超过96%。2:内存超过99%
知识点/技巧: 倒叙遍历,是列表变化(长度、内容)的克星
class Solution: def canPartition(self, nums: List[int]) -> bool: s = sum(nums) if s % 2: # 1:要想使得分割成后的两个子集和相等,那么:和必须为偶数 return False target = s // 2 a = [False] * (target + 1) dir_list = {0: True} for n in nums: if n > target: # 2:如果遍历的元素比目标值大,说明没有找到结果,返回False return False list_keys=[] for key in reversed(list(dir_list.keys())): if dir_list.get(target - n): return True list_keys.append(key + n) else: for l_key in list_keys: dir_list[l_key] = True return False
运行结果:1:耗时超过96%。2:内存超过43%
知识点/技巧:利用哈希表实现,实现快速查找
-
217. 存在重复元素(难度系数✯)
方法一运行结果:1:耗时超过40%。2:内存超过33%。3:代码简洁
知识点/技巧: 利用set去重,比较长度
方法二运行结果:1:耗时超过76%。2:内存超过30%。
知识点/技巧: 利用字典记录字母出现次数
-
198.打家劫舍 (难度系数✯✯)
class Solution: def rob(self, nums: List[int]) -> int: if not nums: return 0 size = len(nums) if size==1: return nums[0] dp = [0] * size dp[0] = nums[0] dp[1] = max(nums[0],nums[1]) for i in range(2,size): dp[i] = max(dp[i-2]+nums[i],dp[i-1]) return dp[size-1]
运行结果:1:耗时超过5.99%。2:内存超过54.25%。
知识点/技巧:由少到多,无限套娃
/** * @param {number[]} nums * @return {number} */ var rob = function(nums) { let n_length = nums.length if (n_length == 0){ return 0 } if (n_length==1){ return nums[0] } var dp = new Array(n_length) dp[0] = nums[0] dp[1] = Math.max(nums[0],nums[1]) for(i=2;i<n_length;i++){ dp[i]=Math.max(dp[i-2]+nums[i],dp[i-1]) } return dp[n_length-1] };
运行结果:1:耗时超过5.99%。2:内存超过54.25%。
class Solution: def rob(self, nums: List[int]) -> int: if not nums: return 0 size = len(nums) if size == 1: return nums[0] _pre = nums[0] _next = max(nums[0], nums[1]) for i in range(2, size): _pre, _next = _next, max(_pre + nums[i], _next) return _next
运行结果:1:耗时超过90%。2:内存超过46%。
知识点/技巧:较方法1,通过滚动字段的方式替换了列表
/** * @param {number[]} nums * @return {number} */ var rob = function(nums) { let n_length = nums.length if (n_length == 0){ return 0 } if (n_length==1){ return nums[0] } var dp = new Array(n_length) _pre = nums[0] _next = Math.max(nums[0],nums[1]) for(i=2;i<n_length;i++){ _pre_pre = _pre _pre = _next _next = Math.max(_pre_pre+nums[i],_next) } return _next };
运行结果:1:耗时超过66%。2:内存超过20%。
知识点/技巧:较方法1,通过滚动字段的方式替换了列表
-
213. 打家劫舍 II (难度系数✯✯)
class Solution: def rob(self, nums: List[int]) -> int: def my_rob(nums): cur, pre = 0, 0 for num in nums: cur, pre = max(pre + num, cur), cur return cur if not nums: return 0 elif len(nums) == 1: return nums[0] else: return max(my_rob(nums[:-1]), my_rob(nums[1:]))
运行结果:1:耗时超过10%。2:内存超过65%。
知识点/技巧:分情况考虑:偷首不偷尾,不偷首偷尾
-
337. 打家劫舍 III (难度系数✯✯)
class Solution: def rob(self, root: TreeNode) -> int: if not root: return 0 money = root.val # 当前层节点 if root.left: money += self.rob(root.left.left) + self.rob(root.left.right) # 孙子层节点 if root.right: money += self.rob(root.right.left) + self.rob(root.right.right) # 孙子层节点 return max(money,self.rob(root.left)+self.rob(root.right))
运行结果:超时。
知识点/技巧:隔一层统计求和(爷+孙 <==> 父)
# 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: dict_result = {} def rob(self, root: TreeNode) -> int: if not root: return 0 if self.dict_result.get(root): return self.dict_result.get(root) money = root.val # 当前层节点 if root.left: money += self.rob(root.left.left) + self.rob(root.left.right) # 孙子层节点 if root.right: money += self.rob(root.right.left) + self.rob(root.right.right) # 孙子层节点 result = max(money,self.rob(root.left)+self.rob(root.right)) self.dict_result[root] = result return result
运行结果:1:耗时超过47.99%。2:内存超过6.21%。
知识点/技巧:引入字典,减少中间结果计算
# 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 rob(self, root: TreeNode) -> int: # 我们使用一个大小为2的元祖表示 偷、不偷 def my_rob(node): if not node: return 0, 0 l = my_rob(node.left) r = my_rob(node.right) selected = node.val + l[1] + r[1] not_selected = max(l[0],l[1]) + max(r[0],r[1]) return selected, not_selected return max(my_rob(root))
运行结果:1:耗时超过87.65%。2:内存超过70.08%。
知识点/技巧:回到最初的起点,每个节点可选择偷或者不偷两种状态