python3算法题笔记
欢迎来到我的频道
这是我个人学习算法刷题的笔记,仅供参考交流。
1.组合总和
题目:找出所有相加之和为n的k个数的组合,且满足条件:
1.只使用数字1~9
2.每个数最多使用一次
返回所有可能的有效组合列表
方法1:组合型回溯+剪枝
分析:
设还需要选d=k-m(m:已选数字数量)
设还需要选和为 c 的数字(初始为n,没选一个数字j,则下一个为c-j)
边界条件:
1.剩余数字i 数目不够:i<d
2. c < 0
3. 剩余数字选最大的,和也不够 c。例如:i=5,还需数字d=2,当c > i +…+ (i-d+1)时,直接返回; 公式可简化为: i +…+ (i-d+1) = i + (i-d+1) / 2 * d
class CombinedSum:
def conbined_sum(self, n: int, k: int) -> list[int]:
ans = []
path = []
def dfs(i, c):
d = k - len(path) # 获取仍需选数字,注意:当 d=0 时,边界条件3的公式等于0,即c=0的情况下,才不会return,结合判断len(path)==k同时成立,则结果正确
if c < 0 or c > (i + i - d + 1) / 2 * d: # 过滤边界条件2,3
return
if len(path) == k: # 判定数字数量 k;
ans.append(path.copy())
return
for j in range(i, d - 1, -1): # 过滤边界条件1
path.append(j)
dfs(j - 1, c - j)
path.pop()
dfs(9, n)
return ans
2.括号生成
题目:数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
分析:
n对括号,即一共有2n个括号,其中左括号n个;
拆解为将n个左括号插入2n个位置中,其余位置为右括号;
第一步:枚举path[i] 是是左括号还是右括号
第二步:构造>=i 的部分
第三步:构造>=i+1 的部分
设 左括号个数为left,只要 left<n 就可以选择左括号;
则右括号数量为:i - left,只要i - left < left,就可以选择右括号
当i=2n时,导入结果集
class Solution:
def parenthesisGeneration(self, n: int) -> list:
m = n * 2
ans = []
path = [''] * m # 可以默认为[]
def dfs(i, left):
if i == m: # 判断为true则导入结果集
ans.append(''.join(path))
return
if left < n: # left<n 就可以选择左括号
path[i] = '('
dfs(i + 1, left + 1)
if i - left < left: # i - left < left,就可以选择右括号
path[i] = ')'
dfs(i + 1, left)
dfs(0, 0)
return ans
3.全排列
题目:给定一个不含重复数字的数组nums,返回所有可能的全排列组合
分析:
加入nums的长度是n,则每个组合长度也是:n=len(nums)
设path记录已选数字,s记录未选数组
边界条件:当len(path)==len(nums)时,导入结果集
方法一(回溯算法1)
第一步:从s中获取枚举填入path的数字num
第二步:构造未选集合s,s = s- {num}
class Solution:
def full_permutation(self, nums: list[int]) -> list:
n = len(nums)
ans = []
path = []
def dfs(s):
if len(path) == n:
ans.append(path.copy())
return
for num in s:
path.append(num)
dfs(s - {num})
path.pop()
dfs(set(nums))
return ans
方法二(回溯算法2)
第一步:从s中获取枚举path[i]填入的数字num
第二步:构造排列>=i的数,未选集合为s
第三步:构造排列>=i+1的数,未选集合为s-{num}
class Solution:
def full_permutation(self, nums: list[int]) -> list:
n = len(nums)
ans = []
path = [0] * n
def dfs(i, s): # i:当前枚举指针, s:未选数字集合
if i == n: # 即len(path) == len(nums)
ans.append(path.copy())
return
for num in s:
path[i] = num
dfs(i+1, s-{num})
dfs(0, set(nums))
return ans
方法三:
用一个布尔数组onPath记录在path中的数据
如果num[i]在path中,则onPath[i]为真
def full_permutation_1(self, nums: list[int]) -> list:
n = len(nums)
ans = []
path = [0] * n
onPath = [False] * n
def dfs(i):
if i == n:
ans.append(path.copy())
return
for j in range(n):
if not onPath[j]:
path[j] = nums[j]
onPath[j] = True
dfs(i+1)
onPath[j] = False
dfs(0)
return ans
4.N皇后
题目:按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
分析:
不同行,不同列=》每行每列都只有一个皇后
用一个长为n的数组记录皇后的位置,即第i行的皇后在col[i]列
枚举col所有的排列,即每行只选一个,每列只选一个
最后判断右上和左上是否右其他皇后:
"""
通过for循环遍历所有小于i的行:for j in range(i)右上:i + col[i] == i+1 + col[i+1]; 左上:i - col[i] == i-1 - col[i-1],相同则有其他皇后,不同则没有
"""
def valid(r, c): # r:行,c:行位置的值(对应棋盘的列)
for R in range(r):
C = col[R]
if r + c == R + C or r - c == R - C:
return False
return True
全部代码:
class Queen:
def chess_queen(self, n: int) -> list[[str]]:
ans = []
col = [0] * n
def valid(r, c): # 用于判断左上和右上都没有皇后
for R in range(r):
C = col[R]
if r + c == R + C or r - c == R - C:
return False
return True
def dfs(r, s):
if r == n:
ans.append(['.' * c + 'q' + '.' * (n - c - 1) for c in col])
return
for c in s:
if valid(r, c):
col[r] = c
dfs(r+1, s-{c})
dfs(0, set(range(n)))
return ans
可以把valid方法去掉,判断加入dfs方法中的
class Queen:
def chess_queen(self, n: int) -> list[[str]]:
ans = []
col = [0] * n
def dfs(i, s):
if i == n:
ans.append(['.' * c + 'q' + '.' * (n - c - 1) for c in col])
return
for c in s:
if all(r+c != R+col[R] and r-c != R-col[R] for R in range(r)) # 通过python的all方法更简便,但是也会更难以阅读
col[i] = c
dfs(i+1, s-{c})
dfs(0, set(range(n)))
return ans
5.分糖果(贪心算法)
题目:n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。
你需要按照以下要求,给这些孩子分发糖果:
*每个孩子至少分配到 1 个糖果。
*相邻两个孩子评分更高的孩子会获得更多的糖果。
请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。
分析:
设两个参数,左边:left,右边:right。
当 left > right 时,ratings[left] = ratings[right] + 1;
当 right > left 时,ratings[right] = ratings[left] + 1;
步骤:
第一步:初始化数组,长度为len(ratings),数组内所有数为1;
第二步:循环所有 left > right 的情况;
第三步:循环所有 right > left 的情况;
最后:通过sum()方法求糖果总数并返回
class Solution:
def candy(self, ratings: List[int]) -> int:
n = len(ratings)
path = [1] * n
for i in range(1, n):
if ratings[i] > ratings[i-1]:
path[i] = path[i-1] + 1
else:
continue
for i in range(1, n):
if ratings[i] < ratings[i-1]:
path[i-1] = path[i] + 1
else:
continue
ans = sum(path)
return ans
6.盛最多水的容器
题目:
给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
方法一:双指针
分析:
本题结果为求两数之间的容积,宽为:right - left;高为:min(height[left], height[right])
则面积:s = min(height[left], height[right]) * (right - left));
以数组左右两边为指针,每向内缩一步,则宽:right-left 必然-1;
设向内移动长板,则 min(height[left], height[right]) 可能变小或不变,面积必然减小;
设向内移动短板,则 min(height[left], height[right]) 可能变大,面积有可能增大。
步骤:
第一步:设置左,右和最大值的初始值;
第二步:判断 height[left] 和 height[right] 大小,并更新面积最大值,同时短板向内移(left+1 or right-1) ;
第三步:判断 left=right 时结束,并返回最大值
class Solution:
def maxArea(self, height: List[int]) -> int:
res, left, right = 0, 0, len(height)-1
while left < right:
wide = right - left
if height[left] <= height[right]:
res = max(res, height[left] * wide)
left = left + 1
else:
res = max(res, height[right] * wide)
right = right - 1
return res
方法二:暴力
分析:
求面积等于两边中的短边,乘以两边的距离;
用一个数组,记录所有的面积,最后返回最大值即可
步骤:
第一步:声明一个空列表:path = [];
第二步:通过镶套for循环遍历所有两边的可能性,并将面积添加到path;
第三步:返回最大值,max(path)。
# 注:全是暴力,没有一点点技巧,参考意义不大。
def maxArea(self, height: list[int]) -> int:
path = []
for i in range(len(height)):
for j in range(i + 1, len(height)):
wide = j - i
if height[i] >= height[j]:
hei = height[j]
else:
hei = height[i]
path.append(wide * hei)
ans = max(path)
return ans
7.数组中的第K个最大元素
题目:
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。
方式一:python内置排序
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
nums.sort(reverse=True)
return nums[k-1]
方法二:递归
分析:
在nums中随机一个数standard为基准,将nums中数字划分为三部分:small, equal, big ;
当k <= len(big) 时,目标在big列表中,递归big;
当k > len(nums) - len(samll)时,目标在small列表中,递归small;
当不属于以上两种情况,目标在equal列表中,返回standard。
def findKthLargest(self, nums, k):
def quich_select(nums, k):
pivot = random.choice(nums)
small, equal, big = [], [], []
for num in nums:
if num < pivot:
small.append(num)
elif num > pivot:
big.append(num)
else:
equal.append(num)
if k <= len(big):
return quich_select(big, k)
if len(nums) - len(small) < k:
return quich_select(small, k-len(nums)+len(small))
return pivot
return quich_select(nums, k)
8.最长公共子序列
题目:
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
方式一:回溯
分析:
要获取两个字符串的公共序列,先通过回溯的递归方法(见上面全排类,方法类似),获取两个字符串各自的子序列,最后通过集合的intersection获取公共部分,设最大数ans = 0,遍历公共集合,有长度大于ans时更新即可。
步骤:
第一步:设置初始值:字符串子序列res,子序列参数:path ,结果:ans;
第二步:确定子序列回溯的边界值,当字符串长度为0时返回;
第三步:循环text字符串,并赋值path;
第四步:构造剩余 i 部分text内容,dfs(text[ i: ])
最后:比较公共部分,更新字符串长度即可
"""
纯发疯作品,没什么优化,也没什么参考价值。
"""
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
res = []
path = []
def all_arrange(text):
res.append(''.join(path.copy()))
if len(text) == 0:
return
for i in range(len(text)):
path.append(text[i])
all_arrange(text[i+1:]) # 因为要保持相对顺序,因此下一个必须在当前枚举后面
path.pop()
return set(res) # 返回集合,方便后续取交集
text_path_1 = all_arrange(text1) # text1字符串的子序列
res.clear()
text_path_2 = all_arrange(text2) # text2字符串的子序列
pub = text_path_1.intersection(text_path_2) # 获取公共子序列
ans = 0 # 初始化最大长度
for i in pub:
if len(i) > ans:
ans = len(i)
return ans
方式二:回溯-2
分析:
求字符串的子序列,本质是选或不选的问题;设字符串s和t。
当前操作:考虑s[i] 和 t[j] 选或不选;
子问题:s的前i个字符 和 t的前j个字符的LCS(两个序列中最长公共子序列的长度);
下一子问题:
s的前 i-1 个字符 和 t的前 j 个字符的LCS;
or s的前 i-1 个字符 和 t的前 j-1 个字符的LCS;
or s的前 i 个字符 和 t的前 j-1 个字符的LCS;
步骤:
第一步:判断边界值:当任意字符串为空时,返回 0;
第二步:判断 s[i] == t[j] 时,递归 (i-1 , j-1) +1;(注:+1是为了记录LCS长度+1)
第三步:判断 s[i] != t[j] 时,在(i-1, j)和 (i, j-1)中递归LCS大的一个。
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
n = len(text1)
m = len(text2)
def dfs(i, j):
if i<0 or j<0:
return 0
if text1[i] == text2[j]:
return dfs(i-1, j-1) + 1
return max(dfs(i-1, j), dfs(i, j-1))
return dfs(n-1, m-1)
方法三:动态规划
分析:
设一个二维数组f[n+1][m+1](注:n/m分别是两个字符串长度,+1是为了防止循环超出范围);
遍历text1[i] 是否等于text2[j],当等于时,更新二维数组f[i+1][j+1] = f[i][j] + 1;
不等于时,f[i+1][j+1] 更新为附近数字的最大值
初始状态:当n=0 时,f[0][j] =0 ;当m=0 时,f[i][0] =0;因此f[i][j]默认为0;
遍历方向:因为f[i][j]依赖于f[i-1][j-1],f[i-1][j],f[i][j-1];所以是从小到大;
最终结果:由于 f[i][j] 的含义是 text1[0:i-1] 和 text2[0:j-1] 的最长公共子序列。我们最终希望求的是 text1 和 text2 的最长公共子序列。因此返回结果f[n][m]
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
n = len(text1)
m = len(text2)
f = [[0] * (m+1) for a in range(n+1)]
for i, x in enumerate(text1):
for j, y in enumerate(text2):
if x == y:
f[i+1][j+1] = f[i][j] + 1
else:
f[i+1][j+1] = max(f[i+1][j], f[i][j+1] )
return f[n][m]
9.最大子数组和
题目:
给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
子数组是数组中的一个连续部分。
方法1:动态规划
分析:
设一个数组db[],长度为len(nums),与目标数组保持一致;
db[i]是nums[i]为结尾的最大连续和
转移方程:当db[i-1] < 0 时,说明db[i-1]对nums[i]是负影响,不如仅保留nums[i]
得到判断:
当 db[i-1] <= 0 时, db[i] = nums[i];
当 db[i-1] > 0,db[i] = db[i-1] + nums[i]
结果:返回db数组中最大值
可以压缩在nums中直接修改,把空间复杂度压缩为O(1)
class Solution:
def maxSubArray(self, nums: list[int]) -> int:
for i in range(1, len(nums)):
nums[i] += max(nums[i - 1], 0)
return max(nums)
后续待添加,刷到哪加到哪!