目录
四、回溯
4.1 LC 电话号码的字母组合
4.1.1 题求
4.1.2 求解
法一:回溯
# 96.92% - 24ms
class Solution:
def letterCombinations(self, digits: str) -> List[str]:
if not digits:
return []
phone = {'2':"abc", '3':"def", '4':"ghi", '5':"jkl",
'6':"mno", '7':"pqrs", '8':"tuv", '9':"wxyz"}
res = []
n = len(digits)
def recur(beg, cur):
# 长度达标, 记录排列
if beg == n:
res.append(''.join(cur))
return
# 遍历当前数字对应的所有字母
for alp in phone[digits[beg]]:
recur(beg+1, cur+[alp])
recur(0, [])
return res
参考资料:
4.2 LC 括号生成 ☆
4.2.1 题求
4.2.2 求解
法一:动态规划+递归
from functools import lru_cache
class Solution:
@lru_cache(None)
def generateParenthesis(self, n: int) -> List[str]:
if n == 0:
return ['']
res = []
for i in range(n):
# 左、右的所有有效组合, 始终满足括号总数为 i + (n-1-i) = n-1
for left in self.generateParenthesis(i):
for right in self.generateParenthesis(n-1-i):
res.append(f'({left}){right}')
return res
法二:动态规划+迭代
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
# dp[i] 表示 i 组括号的所有有效组合
# dp[i] = "(【dp[p]的所有有效组合】) + 【dp[q]对应的有效组合】"
# p 从 0 遍历到 i-1, 同时 q 则相应从 i-1 到 0, 始终满足 p+q = i-1
# 定义 dp 数组
dp = [[] for _ in range(n+1)] # dp[i] 存放第 i 组括号的所有有效组合
dp[0].append("") # 初始化 dp[0], 共 0 组括号时无组合
dp[1].append("()") # 初始化 dp[1], 共 1 组括号组合唯一
# i 从 2 遍历到 n
for i in range(2, n+1): # 计算 dp[i], 即共有 i 组括号时的所有组合
# p 从 0 遍历到 i-1
for p in range(i):
# 得到 dp[p] 和 dp[q] 的所有有效组合
list1, list2 = dp[p], dp[i-1-p]
# 遍历各组合插入到当前 1 组 ( ) 的中、右侧
for left in list1:
for right in list2:
# "(" + 【i=p时所有括号的排列组合】+ ")" +【i=q时所有括号的排列组合】
# p+q = n-1, 即除了第 1 组 "( )" 外剩下的 n-1 组
dp[i].append(f"({left}){right}") # 各 n 组合 "(" + left + ")" + right
return dp[n]
参考资料:
4.3 LC 全排列
4.3.1 题求
4.3.2 求解
法一:回溯+递归
# 78.16% - 32ms
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
res = []
n = len(nums)
def recur(beg, cur):
if beg >= n:
res.append(cur)
return
for idx in range(beg, n):
cur[beg], cur[idx] = cur[idx], cur[beg]
recur(beg+1, cur.copy())
cur[idx], cur[beg] = cur[beg], cur[idx]
recur(0, nums)
return res
法一改:回溯+递归
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
res = []
n = len(nums)
def recur(beg):
if beg >= n:
res.append(nums[:]) # res.append(nums.copy()) # 优化 - 仅在记录结果时浅拷贝
return
for idx in range(beg, n):
nums[beg], nums[idx] = nums[idx], nums[beg]
recur(beg+1)
nums[idx], nums[beg] = nums[beg], nums[idx]
recur(0)
return res
参考资料:
4.4 LC 子集 ❤
4.4.1 题求
4.4.2 求解
法一:迭代
# 88.68% - 28ms
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
res = [[]]
for i in range(len(nums)):
# 每次弹出 nums[:i+1] 的元素构成的所有组合并加入一个新数 nums[i]
res.extend([r+[nums[i]] for r in res])
return res
参考资料:
4.5 LC 单词搜索
4.5.1 题求
4.5.2 求解
法一:DFS + 回溯
# 70.54% - 3808ms
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
m, n, k = len(board), len(board[0]), len(word)
visited = set() # 已访问坐标集合
def dfs(x, y, idx):
# 越界 + 值不匹配 + 坐标已访问 均为非法情况, 直接返回 False
if x < 0 or x >= m or y < 0 or y >= n or board[x][y] != word[idx] or (x, y) in visited:
return False
idx += 1
visited.add((x, y)) # 加入当前路径到已访问集合 visited 后继续 DFS
# 但凡有一路成功, 即返回 True
if idx == k or dfs(x-1, y, idx) or dfs(x+1, y, idx) or dfs(x, y-1, idx) or dfs(x, y+1, idx):
return True
visited.remove((x, y)) # 当前路径下 DFS 无果, 从已访问集合 visited 移除
# 遍历各起点
for i in range(m):
for j in range(n):
if dfs(i, j, 0):
return True
visited.clear()
return False
参考资料:
五、排序和搜索
5.1 LC 颜色分类 ☆
5.1.1 题求
5.1.2 求解
法一:双指针
class Solution:
def sortColors(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
n = len(nums)
p0, p1, p2 = 0, 0, n - 1 # 左、中、右三指针
while p1 <= p2:
# 中指针与右指针交换
while p1 <= p2 and nums[p1] == 2:
nums[p1], nums[p2] = nums[p2], nums[p1]
p2 -= 1
# 中指针与左指针交换
while p0 <= p1 and nums[p1] == 0: # if nums[p1] == 0:
nums[p1], nums[p0] = nums[p0], nums[p1]
p0 += 1
# 中指针移位
p1 += 1
参考资料:
5.2 LC 前 K 个高频元素
5.2.1 题求
5.2.2 求解
法一:列表排序
# 95.99% - 32ms
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
# num: freq
hashmap = defaultdict(int)
for num in nums:
hashmap[num] += 1
# freq: num
array = [(val, key) for key, val in hashmap.items()]
array.sort(reverse=True)
# freq topk
return [array[j][-1] for j in range(k)]
法二:堆排序
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
# num: freq
hashmap = defaultdict(int)
for num in nums:
hashmap[num] += 1
# heap sort
array = []
for key, val in hashmap.items():
heapq.heappush(array, (-val, key)) # heapq.heapify(array)
return [heapq.heappop(array)[-1] for _ in range(k)]
法三:快速排序
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
# num: freq
hashmap = defaultdict(int)
for num in nums:
hashmap[num] += 1
# quick sort - https://blog.csdn.net/qq_39478403/article/details/119035694
def quick_sort(lhs, rhs):
if lhs >= rhs:
return
# 从大到小排序
i, j = lhs, rhs
while i < j:
# 必须先 j 再 i - 必须先右再左
while i < j and array[j][0] <= array[lhs][0]: # <=
j -= 1
while i < j and array[i][0] >= array[lhs][0]: # >=
i += 1
array[i], array[j] = array[j], array[i]
array[i], array[lhs] = array[lhs], array[i]
quick_sort(lhs, i-1)
quick_sort(i+1, rhs)
return
array = [(val, key) for key, val in hashmap.items()]
quick_sort(0, len(array)-1)
return [array[i][-1] for i in range(k)]
参考资料:
5.3 LC 数组中的第K个最大元素
5.3.1 题求
5.3.2 求解
法一:部分快速排序
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
def quick_sort(lhs, rhs):
'''
从大到小排序版 - 若要从小到大排序, 则令两个 while 的 >= 和 <= 对调
'''
if lhs >= rhs:
return
i, j = lhs, rhs
while i < j:
while i < j and nums[j] <= nums[lhs]:
j -= 1
while i > j and nums[i] >= nums[lhs]:
i += 1
nums[i], nums[j] = nums[j], nums[i]
# 中间位置 i 或 j 和参考位置 lhs 交换
nums[i], nums[lhs] = nums[lhs], nums[i]
# 仅对指定范围内排序
if k <= i:
quick_sort(lhs, i-1)
else:
quick_sort(i+1, rhs)
return
n = len(nums)
quick_sort(0, n-1)
return nums[k-1]
法二:堆排序 API
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
heapq.heapify(nums)
return heapq.nlargest(k, nums)[-1]
法三:堆排序
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
heapq.heapify(nums)
for _ in range(len(nums)-k):
heapq.heappop(nums)
return nums[0] # 小顶堆
参考资料:
5.4 LC 寻找峰值
5.4.1 题求
5.4.2 求解
# 89.72% - 28ms
class Solution:
def findPeakElement(self, nums: List[int]) -> int:
# 数组长度
n = len(nums)
# 左、右边界试探
if n == 1 or nums[0] > nums[1]:
return 0
elif nums[-1] > nums[-2]:
return n - 1
# 中央二分搜索
lhs, rhs = 1, n - 2
while lhs <= rhs:
mid = (lhs + rhs) // 2
if nums[mid] > nums[mid-1] and nums[mid] > nums[mid+1]:
return mid
elif nums[mid-1] > nums[mid] > nums[mid+1]:
rhs = mid - 1
# elif nums[mid-1] < nums[mid] < nums[mid+1]:
# lhs = mid + 1
else:
lhs = mid + 1 # 在谷底时任挑一个方向
参考资料:
5.5 LC 在排序数组中查找元素的第一个和最后一个位置
5.5.1 题求
5.5.2 求解
法一:二分搜索
# 99.97% - 16ms
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
n = len(nums)
lhs, rhs = 0, n - 1
while lhs <= rhs:
mid = (lhs + rhs) // 2
if nums[mid] > target:
rhs = mid - 1
elif nums[mid] < target:
lhs = mid + 1
else:
left = right = mid
while left >= 1 and nums[left-1] == target:
left -= 1
while right <= n-2 and nums[right+1] == target:
right += 1
return [left, right]
return [-1, -1]
参考资料:
5.6 LC 合并区间
5.6.1 题求
5.6.2 求解
法一:排序+遍历
# 94.04% - 32ms
class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
# 排序 - x 从小到大, y 从大到小
new = sorted(intervals, key=lambda x: (x[0], -x[1]))
res = []
x, y = new[0]
for i, j in new:
# 若左边界不同, 分类讨论
if x != i:
# 若区间相交, 取较大者为右边界
if i <= y:
y = max(j, y)
# 否则, 记录当前区间 [x, y], 开始新区间 [i, j]
else:
res.append([x, y])
x, y = i, j
# 收尾
res.append([x, y])
return res
法一改:极致简化
class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
# 排序 - x 从小到大
new = sorted(intervals, key=lambda x: x[0])
res = []
x, y = new[0]
for i, j in new:
# 当且仅当出现新的左边界 i 且当前右边界 y 小于新左边界 i
if x != i and i > y:
res.append([x, y])
x, y = i, j
# 否则, 始终取较大者为当前右边界 y
else:
y = max(j, y)
# 收尾
res.append([x, y])
return res
参考资料:
5.7 LC 搜索旋转排序数组 ❤
5.7.1 题求
5.7.2 求解
法一:二分搜索改
class Solution:
def search(self, nums: List[int], target: int) -> int:
n = len(nums)
left, right = 0, n - 1
while left <= right:
# 中间索引
mid = (left + right) // 2
# 找到目标索引 mid
if nums[mid] == target:
return mid
# nums[0] <= nums[mid] 表明前半段 nums[left:mid+1] 有序 ☆
elif nums[0] <= nums[mid]:
# target 在前半段 nums[:mid+1]
if nums[0] <= target < nums[mid]:
right = mid - 1
# target 在后半段 nums[mid:]
else:
left = mid + 1
# 否则, 去有序的后半段 nums[mid:right+1] 找 ☆
else:
# target 在后半段 nums[mid:]
if nums[mid] < target <= nums[n-1]:
left = mid + 1
# target 在前半段 nums[:mid+1]
else:
right = mid - 1
return -1
参考资料:
5.8 LC 搜索二维矩阵 II
5.8.1 题求
5.8.2 求解
法一:模拟二叉搜索树
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
# 从左下角坐标开始遍历作为根节点
m, n = len(matrix), len(matrix[0])
x, y = m - 1, 0
while x >= 0 and y < n:
if matrix[x][y] == target:
return True
elif matrix[x][y] < target:
y += 1
else:
x -= 1
return False
参考资料:
六、动态规划
6.1 LC 跳跃游戏 ☆
6.1.1 题求
6.1.2 求解
法一:贪心法
class Solution:
def canJump(self, nums) :
# 类似青蛙过河问题
max_idx = 0
for idx, num in enumerate(nums):
# 取当前位置 idx 可达的最大步长 idx + num 为下一可达位置 next_idx
next_idx = idx + num
# 若当前位置 idx 是可达的 且 下一可达位置 next_idx 范围更大
if max_idx >= idx and next_idx > max_idx:
# 最大可达位置 max_idx
max_idx = next_idx
# 最大可达位置 max_idx 能否覆盖终点位置 idx = n - 1
return max_idx >= idx
参考资料:
6.2 LC 不同路径
6.2.1 题求
6.2.2 求解
法一:动态规划 - 2D 数组
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [[0 for _ in range(n)] for _ in range(m)]
dp[0][0] = 1
# 第 0 行第 0 列路径唯一
for i in range(1, m):
dp[i][0] = dp[i-1][0]
for j in range(1, n):
dp[0][j] = dp[0][j-1]
# 第 i 行第 j 列取决于左、上路径之和
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i-1][j] + dp[i][j-1]
return dp[-1][-1]
法二:动态规划 - 1D 数组
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [1 for _ in range(n)]
for _ in range(1, m):
for k in range(1, n):
dp[k] += dp[k-1]
return dp[-1]
参考资料:
6.3 LC 零钱兑换
6.3.1 题求
6.3.2 求解
法一:动态规划(0/1背包问题)
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
# dp[i] 表示凑出 i 元所需的最少硬币数
dp = [float("inf") for _ in range(amount+1)]
dp[0] = 0
# 遍历凑钱数 i
for i in range(1, amount+1):
# 遍历所用硬币面值 c
for c in coins:
# 状态转移 ☆
diff = i - c
if diff >= 0:
dp[i] = min(dp[i], dp[diff]+1)
# 至多 amount, 再多就是凑不齐
return dp[-1] if dp[-1] != float("inf") else -1
参考资料:
6.4 LC 最长上升子序列 ☆
6.4.1 题求
6.4.2 求解
法一:贪心+动态规划
# 96.66% - 40ms
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
# 简单贪心 + 二分查找
# 若要使上升子序列尽可能长,则需让序列上升得尽可能慢,
# 因此希望每次在上升子序列最后加上的数尽可能小
# dp[i] 表示长度为 i 的最长上升子序列的末尾元素的最小值,其关于 i 单调递增
# len(dp) 表示目前最长上升子序列的长度
# 最后,依次遍历数组 nums 的每个元素,并更新数组 dp 和 len(dp) 的值
# 若 num > dp[len(dp)] 则更新 len(dp) += 1,否则在 dp[1 ... len(dp)] 中
# 找满足 dp[i-1] < num < dp[i] 的下标 i,并更新 dp[i] = num
dp = []
for num in nums:
# 二分查找 - 找到插入位置 i
i = bisect_left(dp, num)
# 若插入位置为 dp 数组末,则表示 num 比 dp[-1] 还大,直接加入
if i == len(dp):
dp.append(num) # len(dp) += 1
# 否则,更新 dp[i],因为 dp[i-1] < num < dp[i],可使数组上升更慢
else:
dp[i] = num
return len(dp)
'''
以输入序列 [0,8,4,12,2] 为例:
第一步插入 0, dp = [0] # num=0 > len(dp)=0 新增
第二步插入 8, dp = [0,8] # num=8 > len(dp)=1 新增
第三步插入 4, dp = [0,4] # 4 取代 8 使序列上升更慢
第四步插入 12,dp = [0,4,12] # num=12 > len(dp)=2 新增
第五步插入 2, dp = [0,2,12] # 2 取代 4 使序列上升更慢
最终得到最大递增子序列长度为 len(dp) = 3
'''
法一:实现二
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
dp = []
for num in nums:
if not dp or num > dp[-1]: # 尚无元素 或 num 最大
dp.append(num)
else:
# 二分查找 - num 插入 dp[idx] 左侧
l, r = 0, len(dp) - 1
idx = r
while l <= r:
mid = (l + r) // 2
if num <= dp[mid] : # <=: num 欲插入 dp[mid] 实现更小值替代
idx = mid
r = mid - 1
else:
l = mid + 1
dp[idx] = num
return len(dp)
参考资料: