按照https://leetcode.cn/circle/discuss/RvFUtj/顺序刷题
零、经验记录
1. 学会画图分析
2. 学会找终止条件
3. 做一道就高质量完成
一、二分算法
0. 总结
a. 大于某个数的第一个数的位置有固定模板,其中要讨论最后一个数小于等于目标数的情况
b. 当查找范围x确定,关注的目标f(x)是单调的,就可以使用二分法
1. 二分查找
在升序数组中查找某数,如果存在则返回下标,不存在则返回-1
a. 自己的想法:left、mid、right三个指针迭代,注意终止条件,一是mid对应的元素为该数,二是到了只剩两个元素时,那么此时mid=left,如果right对应元素为该数,则返回下表right,如果不是,则代表数组中没有该数,返回-1
b. 存在的问题:判断条件复杂,原因是边界不好,每次判断完大小后,实际上mid对应的元素就可以被排除了。如果nums[mid] > target: right = mid-1。如果nums[mid]<target:left=mid+1。
c. 一点点的疏忽会带来极大的麻烦,要求逻辑足够严谨,充分利用已有信息。
2. 寻找比目标字母大的最小字母
找到一个非递减字符数组中比目标字母大的最小字母
a. 自己的想法:三指针迭代,迭代方式为: if letters[mid] <= target: left= mid+1; if letters[mid] > target: right = mid
b. 存在的问题:没考虑到字母之间可以直接比较(上面是改正后的),没有搞清终止条件。如下图,首先排除target不在该字符数组的两种情况,然后递推即可。
3. 正整数和负整数的最大计数
统计非递减数组的正整数数量和负整数数量中的最大值
a. 自己的想法:需要计算pos和neg的数量,首先如果第一个数大于0或者最后一个数小于0,那么该数组全正或全负,返回数组长度即可。然后分别找到大于0的第一个数的位置和小于0的最后一个数的位置。这里需要注意避免陷入死循环,需要考虑遍历尽头。
b.存在的问题:小于0的最后一个数容易陷入死循环,可以转换为大于等于-1的第一个数,然后这个数的前一个数就必然是最后一个负整数。
4. 两个数组间的距离值
数组1中的元素与数组2中的所有元素距离大于d的数量
a. 自己的想法:
遍历arr1,其中每个数字作为target,去寻找arr2中大于等于和小于target的最后一个数
最小的那个如果距离满足大于d,那么该数字满足距离要求
b.存在的问题:arr1不需要排序,找到了大于等于target的第一个数之后,前一个数即为小于target的最后一个数
c. 时间复杂度分为两部分,arr2排序为n2logn2,后面的二分查找为n1logn2,空间复杂度为n1
5. 两个数组的配对成功数
数组1中的元素与数组2中的元素相乘大于等于给定整数的数量
a. 自己的想法:遍历数组1,在数组1的每个元素下,找到与该元素相乘大于等于success的第一个数。
b. 没有问题
6. 使结果不超过阈值的最小除数
给定nums和threshold,找到最小的正整数,使得nums中的每个元素向上取整除以该整数后的和小于等于threshold
a. 自己的想法:除数存在范围,1到nums的最大值(题目中限定了threshold的最小值)。f(x)是单调的,因此就可以在该范围内进行二分查找搜索。
b. 存在的问题:向上取整方法有问题,不是a//b+1,而是(a+b-1)//b。
7. 完成旅途的最少时间
数组time,为每辆车一趟需要的时间,所有车可以并行运行,完成一趟就能继续该车的第二趟,找到totalTrips所需最短时间。
a. 自己的想法:
目的是找时间,时间是一个序列[0,...,totalTrips*min];
条件是每个时间对应trips,找到刚好大于等于totalTrips的第一个时间
可以二分,因为f(trips)关于x(time)单调递增
b. 存在的问题:时间序列没必要创建,因为它和索引没区别,空占内存。另外等于totalTrips的时候的时间不一定是目标时间,因为求解的是最短时间。
二、动态规划
0.总结
a. 动态规划是一种算法,与之相对的有分治、贪婪等
b. 递归(自顶向下)和循环(自底向上)是实现动态规划的两种方式,递归需要记忆中间结果,即记忆化递归
c. 特点
- 重复子问题:子问题是相同类型的,能够递推
- 最优子结构:母问题的最优解可以由子问题的最优解构建
- 无后效性:子问题的最优解不受后面的问题的影响
d. 简单判断:暴力求解子问题的最优解复杂度高、可能性多,因此转而求递推关系
e. 递推关系是多样的,可以与前面的所有最优解都有关系
1. 爬楼梯
一次可以爬一级或者两级台阶,问:n级台阶有几种方案?
a. 自己的想法:递归,n=1,返回1,n=2,返回2,其他则返回递推公式:fuc(n-1)+fuc(n-2)
b. 存在的问题:由递归树可知,节点数O(2^n),时间复杂度:O(2^n),空间复杂度为递归树的深度:O(n)。这里时间复杂度太大,原因是重复计算。
c. 解决办法:
记忆化递归:这里可以建立一个递归外的数组,用于记录每个节点的方法数,同时在每次递归时判断是否有现成的,有就直接返回,没有就计算并保存到这个数组中,时间复杂度为O(n)。
如何降低空间复杂度呢?可以使用循环,自底向上循环计算目标。每次计算只需要前两个变量即可,因此可以用滚动数组计算。
2. 使用最小花费爬楼梯
整数数组cost,cost[i]代表从第i级台阶向上爬需要的费用,可以选择爬一级,也可以选择爬两级,计算爬到顶部的最小花费。可以选择从下标为0或者1的台阶开始爬。
a. 自己的想法:循环,从底向上,计算不同级台阶下的最小花费,递推公式是dp[i] = min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2])
b. 存在的问题:最后一个条件,可以选择从哪里开始爬,得到的信息是:dp[0]=dp[1]=0
3. 01背包问题
给定几个物品的体积和价值,给定一个背包的容积,找到放置物品的最大价值。
a. 思路:
首先,我们考虑i个物品和j容量的背包的最优解为dp[i][j];
其次,这个最优解可以向前递推,得到等效的结果;
先判断volume[i]和背包容量j的大小,如果放不进去,那就必然没有物品i,那就等于dp[i-1][j];
如果放得进去,就要考虑有物品i和没有物品i的时候的最大价值,即:
max(i-1个物品和j容量的背包的最大价值,i-1个物品和j-volume[i]的背包的最优解+value[i])
4. 最长递增子序列
给一个整数数组,找出最长严格递增子序列的长度
a. 思路
为什么用动态规划?单个子问题最优解难求
要实现递推关系,需要将最后一个元素作为递推的判别条件,因此dp[i]不是前i个元素的最长子序列,而是包含nums[i]的最长子序列。递推关系是dp[i]之前的所有子问题中,nums[i]大于nums[j]则在该子问题基础上加1,并取这些中的最大值。
最终返回dp的最大值。
b. 代码
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
n = len(nums)
dp = [1]*n
for i in range(1,n):
for j in range(i):
if nums[i] > nums[j]:
dp[i] = max(dp[i],dp[j]+1)
return max(dp)
三、回溯
0. 总结
回溯的关键在于撤销处理操作,即在没有递归函数的情况下,上下的操作是抵消的。
1. 电话号码的字母组合
给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
a. 思路:每个位置都有多个选择,并且有多位,此时用回溯。回溯的关键在于撤销处理操作,即在没有递归函数的情况下,上下的操作是抵消的。如下:
s += dic[num][i]
dfs(l+1,s)
s = s[:-1]
没有dfs,那么上下操作是抵消的
代码
class Solution:
def letterCombinations(self, digits: str) -> List[str]:
if not digits:
return []
res = []
dic = {"2":"abc","3":"def","4":"ghi","5":"jkl","6":"mno","7":"pqrs","8":"tuv","9":"wxyz"}
def dfs(l,s):
if l == len(digits):
res.append(s)
else:
num = digits[l]
for i in range(len(dic[num])):
s += dic[num][i]
dfs(l+1,s)
s = s[:-1]
dfs(0,"")
return res