LeetCode HOT100 中等题
LeetCode HOT100 中等题详细学习版
目录
- LC 2 两数相加
- LC 3 无重复字符的最长子串
- LC 5 最长回文子串
- LC 11 盛最多水的容器
- LC 15 三数之和
- LC 17 电话号码的字母组合
- LC 19 删除链表的倒数第 N 个结点
- LC 22 括号生成
- LC 31 下一个排列
- LC 33 搜索旋转排序数组
- LC 34 在排序数组中查找元素的第一个和最后一个位置
- LC 39 组合总和
- LC 46 全排列
- LC 48 旋转图像
- LC 49 字母异位词分组
- LC 53 最大子数组和
- LC 55 跳跃游戏
- LC 56 合并区间
- LC 62 不同路径
- LC 64 最小路径和
- LC 75 颜色分类
- LC 78 子集
- LC 79 单词搜索
- LC 96 不同的二叉搜索树
- LC 98 验证二叉搜索树
- LC 102 二叉树的层序遍历
- LC 105 从前序与中序遍历序列构造二叉树
- LC 114 二叉树展开为链表
- LC 128 最长连续序列
- LC 139 单词拆分
- LC 142 环形链表 II
- LC 146 LRU 缓存
- LC 148 排序链表
- LC 152 乘积最大子数组
- LC 155 最小栈
- LC 198 打家劫舍
- LC 200 岛屿数量
- LC 207 课程表
- LC 208 实现 Trie 前缀树
- LC 215 数组中的第 K 个最大元素
- LC 221 最大正方形
- LC 236 二叉树的最近公共祖先
- LC 238 除自身以外数组的乘积
- LC 240 搜索二维矩阵 II
- LC 279 完全平方数
- LC 287 寻找重复数
- LC 300 最长递增子序列
- LC 309 最佳买卖股票时机含冷冻期
- LC 322 零钱兑换
- LC 337 打家劫舍 III
- LC 347 前 K 个高频元素
- LC 394 字符串解码
- LC 399 除法求值
- LC 406 根据身高重建队列
- LC 416 分割等和子集
- LC 437 路径总和 III
- LC 438 找到字符串中所有字母异位词
- LC 494 目标和
- LC 538 把二叉搜索树转换为累加树
- LC 560 和为 K 的子数组
- LC 581 最短无序连续子数组
- LC 621 任务调度器
- LC 647 回文子串
- LC 739 每日温度
1. LC 2 两数相加
难度:中等
标签:链表、数学
原题简述
两个逆序链表表示整数,返回相加后的逆序链表。
知识点介绍
竖式加法:每一位相加后只保留个位,十位作为进位 carry。
标准思路
同时遍历两个链表,空节点按 0 处理;循环条件要包含最后可能剩下的 carry。
最优标准解
from typing import Optional
class Solution:
def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
dummy = ListNode() # 虚拟头节点,方便把每一位结果依次接上
cur = dummy # cur 指向结果链表尾部,后面新节点都接在它后面
carry = 0 # carry 保存上一位加法产生的进位
while l1 or l2 or carry: # 链表没走完或还有进位时,都要继续计算下一位
x = l1.val if l1 else 0 # 取 l1 当前位;l1 走完时按 0 处理
y = l2.val if l2 else 0 # 取 l2 当前位;l2 走完时按 0 处理
total = x + y + carry # 当前位总和 = 两个节点值 + 进位
carry = total // 10 # 十位部分作为下一轮进位
cur.next = ListNode(total % 10) # 当前节点只能存个位数
cur = cur.next # 结果链表尾指针后移
if l1: # 当前链表还有节点时,指针才能后移
l1 = l1.next # l1 移到下一位
if l2: # 当前链表还有节点时,指针才能后移
l2 = l2.next # l2 移到下一位
return dummy.next # 返回虚拟头节点后面的真实链表头
复杂度
时间 O(max(m,n)),空间 O(max(m,n))。
易错点
循环条件必须包含 carry;空链表节点按 0 处理。
多解补充
多解 1:递归
取舍说明: 同样正确,但递归栈更占空间。
from typing import Optional
class Solution:
def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
def dfs(a, b, carry):
if not a and not b and carry == 0:
return None # 没有可返回的节点或结果
x = a.val if a else 0 # 取 l1 当前位;l1 走完时按 0 处理
y = b.val if b else 0 # 取 l2 当前位;l2 走完时按 0 处理
total = x + y + carry # 当前位总和 = 两个节点值 + 进位
node = ListNode(total % 10) # 当前节点只能存个位数
node.next = dfs(a.next if a else None, b.next if b else None, total // 10) # 递归处理下一位,并把结果接到当前节点后面
return node # 返回当前构造出的节点
return dfs(l1, l2, 0)
快速复习
链表加法:个位建节点,十位进位。
2. LC 3 无重复字符的最长子串
难度:中等
标签:字符串、滑动窗口、哈希表
原题简述
返回不含重复字符的最长连续子串长度。
知识点介绍
窗口表示当前没有重复字符的一段,字典记录字符最近出现位置。
标准思路
右指针遍历;如果字符上次出现位置在窗口内,left 跳到上次位置后一位。
最优标准解
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
last = {} # 记录每个字符最近一次出现的位置
left = 0 # 窗口左边界,保证窗口内没有重复字符
ans = 0 # 记录目前最长无重复子串长度
for right, ch in enumerate(s): # right 是窗口右边界,ch 是当前加入窗口的字符
if ch in last and last[ch] >= left: # 当前字符上次出现位置仍在窗口内,说明重复了
left = last[ch] + 1 # 左边界跳到重复字符上次出现位置后一位
last[ch] = right # 更新当前字符的最新位置
ans = max(ans, right - left + 1) # 用当前合法窗口长度更新答案
return ans # 返回最终答案
复杂度
时间 O(n),空间 O(k)。
易错点
判断重复要看上次位置是否 >= left。
多解补充
多解 1:set 窗口
取舍说明: 更直观,但遇到重复时需要一步步删除。
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
window = set()
left = ans = 0 # 记录目前找到的最长无重复子串长度
for right, ch in enumerate(s): # right 是窗口右边界,ch 是当前加入窗口的字符
while ch in window:
window.remove(s[left])
left += 1
window.add(ch)
ans = max(ans, right - left + 1) # 用当前合法窗口长度更新答案
return ans # 返回最终答案
快速复习
无重复子串:滑动窗口 + 最近下标。
3. LC 5 最长回文子串
难度:中等
标签:字符串、中心扩展、动态规划
原题简述
返回字符串中的最长回文连续子串。
知识点介绍
回文串有中心,可能是一个字符,也可能是两个字符之间。
标准思路
枚举每个中心,向左右扩展,记录最长合法范围。
最优标准解
class Solution:
def longestPalindrome(self, s: str) -> str:
start = end = 0 # 记录当前最长回文子串左边界
def expand(l, r):
while l >= 0 and r < len(s) and s[l] == s[r]:
l -= 1
r += 1
return l + 1, r - 1
for i in range(len(s)):
l1, r1 = expand(i, i) # 以单个字符为中心,找奇数长度回文
l2, r2 = expand(i, i + 1) # 以两个字符中间为中心,找偶数长度回文
if r1 - l1 > end - start:
start, end = l1, r1 # 记录当前最长回文子串左边界
if r2 - l2 > end - start:
start, end = l2, r2 # 记录当前最长回文子串左边界
return s[start:end + 1] # 根据最终边界切出最长回文子串
复杂度
时间 O(n²),空间 O(1)。
易错点
偶数中心不能漏;切片右边界要 end+1。
多解补充
多解 1:动态规划
取舍说明: 能体现标签,但空间 O(n²),不如中心扩展省。
class Solution:
def longestPalindrome(self, s: str) -> str:
n = len(s)
dp = [[False] * n for _ in range(n)] # dp[i][j] 表示 s[i:j+1] 是否为回文
start = max_len = 0 # 记录当前最长回文子串长度
for length in range(1, n + 1):
for i in range(n - length + 1):
j = i + length - 1
if s[i] == s[j] and (length <= 2 or dp[i + 1][j - 1]): # 两端字符相同,才有可能形成回文
dp[i][j] = True
if length > max_len:
start, max_len = i, length # 记录当前最长回文子串长度
return s[start:start + max_len] # 根据最终边界切出最长回文子串
快速复习
最长回文子串:枚举中心向外扩。
4. LC 11 盛最多水的容器
难度:中等
标签:数组、双指针
原题简述
给定柱子高度,选两根柱子和 x 轴组成容器,求最大盛水面积。
知识点介绍
面积由宽度和短板决定;宽度缩小时,只有移动短板才可能提高高度。
标准思路
左右指针从两端开始,每次计算面积,移动较短的一边。
最优标准解
from typing import List
class Solution:
def maxArea(self, height: List[int]) -> int:
left, right = 0, len(height) - 1
ans = 0 # 记录目前能装的最大水量
while left < right:
area = (right - left) * min(height[left], height[right]) # 当前面积 = 左右距离 × 较短柱子高度
ans = max(ans, area) # 用当前容器面积更新最大值
if height[left] < height[right]: # 左边是短板,移动它才可能找到更高水位
left += 1 # 移动左短板,尝试提高容器高度
else:
right -= 1 # 移动右短板,尝试提高容器高度
return ans # 返回最终答案
复杂度
时间 O(n),空间 O(1)。
易错点
移动长板不能突破短板限制;循环条件是 left<right。
多解补充
多解 1:暴力枚举
取舍说明: 能做但 O(n²),会超时。
from typing import List
class Solution:
def maxArea(self, height: List[int]) -> int:
ans = 0 # 记录目前能装的最大水量
for i in range(len(height)):
for j in range(i + 1, len(height)):
ans = max(ans, (j - i) * min(height[i], height[j])) # 用当前容器面积更新最大值
return ans # 返回最终答案
快速复习
容器问题:移动短板。
5. LC 15 三数之和
难度:中等
标签:数组、排序、双指针
原题简述
找出所有不重复三元组,使三数之和为 0。
知识点介绍
排序后固定一个数,问题转成两数之和;相同元素相邻方便去重。
标准思路
遍历 i 固定第一个数,在右侧用 left/right 找两数;找到答案后跳过重复。
最优标准解
from typing import List
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
nums.sort() # 先排序,方便双指针移动和去重
ans = [] # 保存所有符合条件的不重复三元组
n = len(nums) # 数组长度,后面循环和边界判断会用到
for i in range(n - 2): # 固定第一个数,后面至少还要留两个数
if nums[i] > 0: # 固定数已大于 0,后面也不小于它,不可能凑成 0
break
if i > 0 and nums[i] == nums[i - 1]: # 跳过重复固定数,避免重复三元组
continue
left, right = i + 1, n - 1 # 左右指针分别从固定数右侧和数组末尾开始
while left < right: # 左右指针没有相遇时,继续寻找另外两个数
total = nums[i] + nums[left] + nums[right] # 计算当前三元组和,用它决定指针移动方向
if total == 0:
ans.append([nums[i], nums[left], nums[right]]) # 当前三数之和为 0,记录这一组三元组
left += 1 # 左指针右移,继续寻找下一组可能答案
right -= 1 # 右指针左移,继续寻找下一组可能答案
while left < right and nums[left] == nums[left - 1]: # 跳过左侧重复值,避免重复答案
left += 1 # 左指针右移,继续寻找下一组可能答案
while left < right and nums[right] == nums[right + 1]: # 跳过右侧重复值,避免重复答案
right -= 1 # 右指针左移,继续寻找下一组可能答案
elif total < 0: # 总和太小,需要更大的数
left += 1 # 左指针右移,继续寻找下一组可能答案
else: # 总和太大,需要更小的数
right -= 1 # 右指针左移,继续寻找下一组可能答案
return ans # 返回最终答案
复杂度
时间 O(n²),空间 O(1) 不算答案。
易错点
固定数、left、right 都要去重;排序是关键。
多解补充
多解 1:哈希两数之和思路
取舍说明: 可做但去重更麻烦,不如排序双指针清晰。
from typing import List
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
ans = set()
n = len(nums) # 数组长度,后面循环和边界判断会用到
for i in range(n):
seen = set() # 记录固定 nums[i] 后,已经见过的第二个数
for j in range(i + 1, n):
need = -nums[i] - nums[j] # 计算第三个数需要是多少,才能让三数之和为 0
if need in seen: # 需要的第三个数已经出现过,得到一组三元组
ans.add(tuple(sorted((nums[i], nums[j], need)))) # 排序后放入集合,借助集合去重
seen.add(nums[j]) # 把当前第二个数记录下来,供后面匹配
return [list(t) for t in ans]
快速复习
三数之和:排序 + 固定一数 + 双指针。
6. LC 17 电话号码的字母组合
难度:中等
标签:回溯、字符串
原题简述
给定数字 2-9,返回它们能表示的所有字母组合。
知识点介绍
每个数字对应多个字母,组合问题适合回溯。
标准思路
按 digit 位置递归选择一个字母,路径长度等于 digits 长度时加入答案。
最优标准解
from typing import List
class Solution:
def letterCombinations(self, digits: str) -> List[str]:
if not digits: # 没有输入数字时,没有任何字母组合
return []
mp = {'2':'abc','3':'def','4':'ghi','5':'jkl','6':'mno','7':'pqrs','8':'tuv','9':'wxyz'} # 数字到字母的映射表
ans, path = [], [] # ans 保存所有组合,path 保存当前正在构造的组合
def backtrack(i):
if i == len(digits): # 每个数字都选完一个字母,得到完整组合
ans.append(''.join(path)) # 把路径中的字母拼成字符串加入答案
return # 结束当前递归分支
for ch in mp[digits[i]]: # 枚举当前数字能对应的所有字母
path.append(ch) # 加入当前路径、节点或中间结果
backtrack(i + 1) # 递归处理下一个数字
path.pop() # 撤销当前选择,尝试下一个字母
backtrack(0)
return ans # 返回最终答案
复杂度
时间 O(4^n),空间 O(n) 不算答案。
易错点
空字符串返回 [];回溯后要 pop。
多解补充
多解 1:队列迭代
取舍说明: 同样正确,按层扩展组合。
from typing import List
class Solution:
def letterCombinations(self, digits: str) -> List[str]:
if not digits: # 没有输入数字时,没有任何字母组合
return []
mp = {'2':'abc','3':'def','4':'ghi','5':'jkl','6':'mno','7':'pqrs','8':'tuv','9':'wxyz'} # 数字到字母的映射表
ans = ['']
for d in digits:
ans = [prefix + ch for prefix in ans for ch in mp[d]]
return ans # 返回最终答案
快速复习
组合问题:路径 + 选择 + 撤销。
7. LC 19 删除链表的倒数第 N 个结点
难度:中等
标签:链表、双指针
原题简述
删除链表倒数第 n 个节点,返回头节点。
知识点介绍
快慢指针保持 n+1 距离,让 slow 停在待删节点前一个。
标准思路
dummy 防止删除头节点特殊处理;fast 先走 n+1 步,再同步走。
最优标准解
from typing import Optional
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
dummy = ListNode(0, head) # 虚拟头节点,方便统一处理链表头部变化
fast = slow = dummy # 虚拟头节点,方便统一处理链表头部变化
for _ in range(n + 1): # fast 先走 n+1 步,让 slow 停在待删节点前一个
fast = fast.next
while fast: # 快慢指针保持距离,同时向后走
fast = fast.next
slow = slow.next # slow 每次走一步
slow.next = slow.next.next # 跳过 slow 后面的节点,完成删除
return dummy.next # 返回虚拟头节点后面的真实链表头
复杂度
时间 O(n),空间 O(1)。
易错点
用 dummy 处理删除头节点;fast 先走 n+1。
多解补充
多解 1:两遍扫描
取舍说明: 更直观但不是一趟。
from typing import Optional
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
dummy = ListNode(0, head) # 虚拟头节点,方便统一处理链表头部变化
length, cur = 0, head
while cur:
length += 1
cur = cur.next
cur = dummy # 虚拟头节点,方便统一处理链表头部变化
for _ in range(length - n):
cur = cur.next
cur.next = cur.next.next
return dummy.next # 返回虚拟头节点后面的真实链表头
快速复习
倒数第 n 个:快慢指针拉开距离。
8. LC 22 括号生成
难度:中等
标签:回溯
原题简述
生成 n 对括号的所有合法组合。
知识点介绍
合法括号构造过程:左括号数量不能超过 n,右括号数量不能超过左括号。
标准思路
回溯时记录 left/right 已用数量,合法时继续添加。
最优标准解
from typing import List
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
ans, path = [], [] # ans 保存所有合法结果,path 保存当前括号串
def backtrack(left, right):
if len(path) == 2 * n: # 长度达到 2n,说明形成一个完整括号串
ans.append(''.join(path))
return # 结束当前递归分支
if left < n: # 左括号还没用完,可以继续放左括号
path.append('(') # 加入当前路径、节点或中间结果
backtrack(left + 1, right)
path.pop() # 撤销刚才放入的括号,尝试其他选择
if right < left: # 右括号少于左括号时,才能放右括号保持合法
path.append(')') # 加入当前路径、节点或中间结果
backtrack(left, right + 1)
path.pop() # 撤销刚才放入的括号,尝试其他选择
backtrack(0, 0)
return ans # 返回最终答案
复杂度
时间约 O(Catalan(n)),空间 O(n)。
易错点
右括号数量不能超过左括号。
多解补充
多解 1:暴力生成后过滤
取舍说明: 能做但大量无效字符串,不推荐。
from typing import List
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
ans = []
def valid(s):
bal = 0
for ch in s:
bal += 1 if ch == '(' else -1
if bal < 0:
return False # 当前情况不符合题意
return bal == 0
def dfs(s):
if len(s) == 2*n:
if valid(s):
ans.append(s)
return # 结束当前递归分支
dfs(s + '(')
dfs(s + ')')
dfs('')
return ans # 返回最终答案
快速复习
生成括号:左可加、右不超过左。
9. LC 31 下一个排列
难度:中等
标签:数组、双指针
原题简述
把数组改成字典序下一个更大的排列;若不存在则变最小排列。
知识点介绍
从右往左找第一个下降点,再找右侧刚好比它大的数交换,最后反转后缀。
标准思路
后缀从右看是非递增的,交换后反转成最小后缀。
最优标准解
from typing import List
class Solution:
def nextPermutation(self, nums: List[int]) -> None:
i = len(nums) - 2 # 从右往左找第一个下降位置
while i >= 0 and nums[i] >= nums[i + 1]: # 当前位置还不是需要变大的位置,继续向左找
i -= 1
if i >= 0:
j = len(nums) - 1 # 从右侧找第一个比 nums[i] 大的数
while nums[j] <= nums[i]: # 这个数不够大,继续向左找
j -= 1
nums[i], nums[j] = nums[j], nums[i] # 交换后,排列刚好变大一点
left, right = i + 1, len(nums) - 1
while left < right:
nums[left], nums[right] = nums[right], nums[left]
left += 1
right -= 1
复杂度
时间 O(n),空间 O(1)。
易错点
找下降点;交换后必须反转后缀。
多解补充
多解 1:用排序后缀
取舍说明: 逻辑相同但后缀排序 O(n log n),不如反转。
from typing import List
class Solution:
def nextPermutation(self, nums: List[int]) -> None:
i = len(nums) - 2 # 从右往左找第一个下降位置
while i >= 0 and nums[i] >= nums[i+1]:
i -= 1
if i >= 0:
j = len(nums) - 1 # 从右侧找第一个比 nums[i] 大的数
while nums[j] <= nums[i]: # 这个数不够大,继续向左找
j -= 1
nums[i], nums[j] = nums[j], nums[i] # 交换后,排列刚好变大一点
nums[i+1:] = sorted(nums[i+1:])
快速复习
下一个排列:找下降点、换更大、反转后缀。
10. LC 33 搜索旋转排序数组
难度:中等
标签:数组、二分
原题简述
在旋转后的升序数组中搜索 target。
知识点介绍
旋转数组任意二分后,至少有一半是有序的。
标准思路
每次判断左半还是右半有序,再决定 target 在哪边。
最优标准解
from typing import List
class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1 # 二分查找的左右边界
while left <= right:
mid = (left + right) // 2 # 取中点,判断目标落在哪一侧
if nums[mid] == target: # 中点就是目标值
return mid
if nums[left] <= nums[mid]: # 左半段有序,可以判断 target 是否在左半段
if nums[left] <= target < nums[mid]:
right = mid - 1 # 排除右侧不可能区间
else:
left = mid + 1 # 排除左侧不可能区间
else:
if nums[mid] < target <= nums[right]:
left = mid + 1 # 排除左侧不可能区间
else:
right = mid - 1 # 排除右侧不可能区间
return -1
复杂度
时间 O(log n),空间 O(1)。
易错点
判断哪半有序;边界用闭区间。
多解补充
多解 1:线性查找
取舍说明: 简单但 O(n),不是题目要求。
from typing import List
class Solution:
def search(self, nums: List[int], target: int) -> int:
for i, x in enumerate(nums):
if x == target:
return i
return -1
快速复习
旋转数组搜索:二分找有序半边。
11. LC 34 在排序数组中查找元素的第一个和最后一个位置
难度:中等
标签:数组、二分
原题简述
在升序数组中返回 target 的起止下标,不存在返回 [-1,-1]。
知识点介绍
找左右边界本质是两次二分。
标准思路
一次找第一个 >= target 的位置,一次找第一个 > target 的位置减一。
最优标准解
from typing import List
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
def lower_bound(x):
left, right = 0, len(nums) # 二分查找的左右边界
while left < right:
mid = (left + right) // 2 # 取中点,判断目标落在哪一侧
if nums[mid] < x:
left = mid + 1 # 排除左侧不可能区间
else:
right = mid # 排除右侧不可能区间
return left
l = lower_bound(target)
r = lower_bound(target + 1) - 1
if l <= r and l < len(nums) and nums[l] == target:
return [l, r]
return [-1, -1]
复杂度
时间 O(log n),空间 O(1)。
易错点
注意空数组和不存在情况。
多解补充
多解 1:线性扫描
取舍说明: 能做但 O(n)。
from typing import List
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
ans = [-1, -1]
for i, x in enumerate(nums):
if x == target:
if ans[0] == -1:
ans[0] = i
ans[1] = i
return ans # 返回最终答案
快速复习
边界查找:lower_bound 两次。
12. LC 39 组合总和
难度:中等
标签:回溯
原题简述
给定候选数和目标,返回所有和为 target 的组合,每个数可重复使用。
知识点介绍
组合问题用回溯;为了不重复组合,递归传入 start 限制选择顺序。
标准思路
每次从 start 开始选数,选中后 target 减小;因为可重复使用,下一层仍从当前 i 开始。
最优标准解
from typing import List
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
ans, path = [], []
candidates.sort() # 排序后方便利用有序性
def dfs(start, remain):
if remain == 0: # 剩余目标为 0,当前路径就是一个合法组合
ans.append(path[:]) # 把当前路径拷贝进答案,避免后续回溯修改
return # 结束当前递归分支
for i in range(start, len(candidates)): # 从 start 开始选择,避免组合顺序不同造成重复
x = candidates[i]
if x > remain:
break
path.append(x) # 加入当前路径、节点或中间结果
dfs(i, remain - x)
path.pop() # 撤销选择,继续尝试下一个候选
dfs(0, target)
return ans # 返回最终答案
复杂度
时间与答案数量相关,最坏指数级;空间 O(target/min)。
易错点
可重复使用时下一层传 i,不是 i+1。
多解补充
多解 1:不排序版本
取舍说明: 也能做,但无法剪枝,效率差些。
from typing import List
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
ans, path = [], []
def dfs(start, remain):
if remain == 0: # 剩余目标为 0,当前路径就是一个合法组合
ans.append(path[:]) # 把当前路径拷贝进答案,避免后续回溯修改
return # 结束当前递归分支
if remain < 0:
return # 结束当前递归分支
for i in range(start, len(candidates)): # 从 start 开始选择,避免组合顺序不同造成重复
path.append(candidates[i]) # 加入当前路径、节点或中间结果
dfs(i, remain - candidates[i])
path.pop() # 撤销选择,继续尝试下一个候选
dfs(0, target)
return ans # 返回最终答案
快速复习
组合总和:回溯 + start 去重。
13. LC 46 全排列
难度:中等
标签:回溯
原题简述
返回数组所有可能排列。
知识点介绍
排列问题每个位置都要尝试一个未使用元素。
标准思路
用 used 标记已选元素,路径长度等于 n 时收集答案。
最优标准解
from typing import List
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
ans, path = [], []
used = [False] * len(nums) # 记录当前排列中哪些数字已经被使用
def dfs():
if len(path) == len(nums):
ans.append(path[:]) # 把当前路径拷贝进答案,避免后续回溯修改
return # 结束当前递归分支
for i, x in enumerate(nums):
if used[i]:
continue
used[i] = True # 记录当前排列中哪些数字已经被使用
path.append(x) # 加入当前路径、节点或中间结果
dfs()
path.pop() # 撤销选择,继续尝试下一个候选
used[i] = False # 记录当前排列中哪些数字已经被使用
dfs()
return ans # 返回最终答案
复杂度
时间 O(n·n!),空间 O(n)。
易错点
每层回溯后必须撤销 used 和 path。
多解补充
多解 1:交换法
取舍说明: 同样优秀,原地交换确定每个位置。
from typing import List
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
ans = []
def dfs(pos):
if pos == len(nums):
ans.append(nums[:])
return # 结束当前递归分支
for i in range(pos, len(nums)):
nums[pos], nums[i] = nums[i], nums[pos]
dfs(pos + 1)
nums[pos], nums[i] = nums[i], nums[pos]
dfs(0)
return ans # 返回最终答案
快速复习
全排列:路径 + used。
14. LC 48 旋转图像
难度:中等
标签:矩阵、原地操作
原题简述
将 n×n 矩阵顺时针旋转 90 度,要求原地。
知识点介绍
顺时针旋转 = 先上下翻转,再沿主对角线翻转。
标准思路
先交换第 i 行和第 n-1-i 行,再交换 matrix[i][j] 与 matrix[j][i]。
最优标准解
from typing import List
class Solution:
def rotate(self, matrix: List[List[int]]) -> None:
n = len(matrix)
for i in range(n // 2):
matrix[i], matrix[n - 1 - i] = matrix[n - 1 - i], matrix[i]
for i in range(n):
for j in range(i + 1, n):
matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j] # 沿主对角线交换,先完成矩阵转置
复杂度
时间 O(n²),空间 O(1)。
易错点
对角线交换 j 从 i+1 开始;不要新建矩阵。
多解补充
多解 1:四点轮换
取舍说明: 同样原地,但下标更容易写错。
from typing import List
class Solution:
def rotate(self, matrix: List[List[int]]) -> None:
n = len(matrix)
for i in range(n // 2):
for j in range(i, n - 1 - i):
tmp = matrix[i][j]
matrix[i][j] = matrix[n-1-j][i]
matrix[n-1-j][i] = matrix[n-1-i][n-1-j]
matrix[n-1-i][n-1-j] = matrix[j][n-1-i]
matrix[j][n-1-i] = tmp
快速复习
旋转图像:上下翻转 + 转置。
15. LC 49 字母异位词分组
难度:中等
标签:哈希表、字符串
原题简述
把由相同字母组成的字符串分到同一组。
知识点介绍
异位词排序后相同;也可用 26 个字母计数作为 key。
标准思路
遍历每个字符串,用排序后的字符串作为哈希 key 分组。
最优标准解
from typing import List
from collections import defaultdict
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
groups = defaultdict(list) # 按同一种特征把异位词放进同一组
for s in strs:
key = ''.join(sorted(s)) # 异位词排序后字符串相同,可作为分组 key
groups[key].append(s) # 加入当前路径、节点或中间结果
return list(groups.values())
复杂度
设字符串总长度 L,排序法时间约 O(L log L),空间 O(L)。
易错点
key 必须可哈希;列表不能直接做 key。
多解补充
多解 1:字母计数 key
取舍说明: 更接近 O(L),适合只含小写字母。
from typing import List
from collections import defaultdict
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
groups = defaultdict(list) # 按同一种特征把异位词放进同一组
for s in strs:
cnt = [0] * 26
for ch in s:
cnt[ord(ch) - ord('a')] += 1
groups[tuple(cnt)].append(s) # 加入当前路径、节点或中间结果
return list(groups.values())
快速复习
异位词分组:排序字符串做 key。
16. LC 53 最大子数组和
难度:中等
标签:数组、动态规划
原题简述
返回连续子数组的最大和。
知识点介绍
如果前面的和为负,继续接上只会拖累当前数。
标准思路
cur 表示以当前位置结尾的最大子数组和,ans 表示全局最大。
最优标准解
from typing import List
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
cur = ans = nums[0] # cur 表示以当前位置结尾的最大子数组和
for x in nums[1:]:
cur = max(x, cur + x) # 要么接上前面的子数组,要么从当前数重新开始
ans = max(ans, cur) # 用当前位置结尾的最大和更新全局答案
return ans # 返回最终答案
复杂度
时间 O(n),空间 O(1)。
易错点
全负数时不能把答案初始化为 0。
多解补充
多解 1:分治
取舍说明: 同样可做,但代码更长;适合理解线段树思想。
from typing import List
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
def solve(l, r):
if l == r:
return nums[l]
m = (l + r) // 2
left_best = solve(l, m)
right_best = solve(m + 1, r)
s = 0
best_l = float('-inf')
for i in range(m, l - 1, -1): s += nums[i]
best_l = max(best_l, s)
s = 0
best_r = float('-inf')
for i in range(m + 1, r + 1): s += nums[i]
best_r = max(best_r, s)
return max(left_best, right_best, best_l + best_r)
return solve(0, len(nums) - 1)
快速复习
最大子数组:当前要么重开,要么延续。
17. LC 55 跳跃游戏
难度:中等
标签:贪心
原题简述
判断能否从下标 0 跳到最后一个下标。
知识点介绍
维护目前能到达的最远位置。若当前下标超过最远位置,说明到不了。
标准思路
遍历每个位置,更新 far=max(far,i+nums[i])。
最优标准解
from typing import List
class Solution:
def canJump(self, nums: List[int]) -> bool:
far = 0 # far 记录当前最远能到达的位置
for i, step in enumerate(nums):
if i > far: # 当前位置都到不了,说明无法到达终点
return False # 当前情况不符合题意
far = max(far, i + step) # far 记录当前最远能到达的位置
if far >= len(nums) - 1: # far 记录当前最远能到达的位置
return True # 当前检查通过
return True # 当前检查通过
复杂度
时间 O(n),空间 O(1)。
易错点
不是每一步跳最远,而是维护覆盖范围。
多解补充
多解 1:DP 可达性
取舍说明: 能做但 O(n²),不如贪心。
from typing import List
class Solution:
def canJump(self, nums: List[int]) -> bool:
n = len(nums)
dp = [False] * n
dp[0] = True
for i in range(n):
if not dp[i]:
continue
for j in range(i + 1, min(n, i + nums[i] + 1)): # 从当前位置起跳,尝试更新最远可达位置
dp[j] = True
return dp[-1]
快速复习
跳跃游戏:维护最远可达。
18. LC 56 合并区间
难度:中等
标签:排序、数组
原题简述
合并所有重叠区间。
知识点介绍
按左端点排序后,重叠区间会相邻。
标准思路
维护结果最后一个区间;若当前左端点 <= 最后右端点,就合并右端点。
最优标准解
from typing import List
class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
intervals.sort(key=lambda x: x[0]) # 排序后方便利用有序性
ans = []
for l, r in intervals:
if not ans or l > ans[-1][1]:
ans.append([l, r])
else:
ans[-1][1] = max(ans[-1][1], r) # 当前区间和上一个区间重叠,合并右端点
return ans # 返回最终答案
复杂度
时间 O(n log n),空间 O(n) 不算排序内部。
易错点
先排序;判断重叠用当前左端点和最后右端点。
多解补充
多解 1:暴力合并
取舍说明: 能做但复杂且低效,不推荐。
from typing import List
class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
intervals.sort() # 排序后方便利用有序性
changed = True
while changed:
changed = False
res = []
for itv in intervals:
if res and itv[0] <= res[-1][1]:
res[-1][1] = max(res[-1][1], itv[1])
changed = True
else:
res.append(itv) # 加入当前路径、节点或中间结果
intervals = res
return intervals
快速复习
合并区间:按左端点排序。
19. LC 62 不同路径
难度:中等
标签:动态规划
原题简述
机器人只能向右或向下,从左上到右下共有多少路径。
知识点介绍
到某格只能从上方或左方来。
标准思路
dp[j] 表示当前行第 j 列路径数,dp[j]+=dp[j-1]。
最优标准解
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [1] * n # 创建 DP 表,保存子问题答案
for _ in range(1, m):
for j in range(1, n):
dp[j] += dp[j - 1] # 创建 DP 表,保存子问题答案
return dp[-1]
复杂度
时间 O(mn),空间 O(n)。
易错点
第一行第一列都是 1。
多解补充
多解 1:组合数学
取舍说明: 同样优秀,代码短但需要理解组合数。
import math
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
return math.comb(m + n - 2, m - 1)
快速复习
路径 DP:上 + 左。
20. LC 64 最小路径和
难度:中等
标签:动态规划、矩阵
原题简述
从左上到右下,只能向右或向下,求路径数字和最小值。
知识点介绍
到某格最小代价来自上方或左方的较小者。
标准思路
原地修改 grid,让 grid[i][j] 表示到该格的最小路径和。
最优标准解
from typing import List
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
m, n = len(grid), len(grid[0])
for i in range(m):
for j in range(n):
if i == 0 and j == 0:
continue
up = grid[i - 1][j] if i > 0 else float('inf')
left = grid[i][j - 1] if j > 0 else float('inf')
grid[i][j] += min(up, left) # 在不同选择中取代价最小的方案
return grid[-1][-1]
复杂度
时间 O(mn),空间 O(1)。
易错点
首行首列边界处理;可以原地改。
多解补充
多解 1:额外 DP 表
取舍说明: 不修改原 grid,但空间 O(mn)。
from typing import List
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
m, n = len(grid), len(grid[0])
dp = [[0]*n for _ in range(m)] # 创建 DP 表,保存子问题答案
for i in range(m):
for j in range(n):
if i == 0 and j == 0:
dp[i][j] = grid[i][j] # 创建 DP 表,保存子问题答案
else:
up = dp[i-1][j] if i else float('inf') # 创建 DP 表,保存子问题答案
left = dp[i][j-1] if j else float('inf') # 创建 DP 表,保存子问题答案
dp[i][j] = grid[i][j] + min(up, left) # 创建 DP 表,保存子问题答案
return dp[-1][-1]
快速复习
最小路径和:当前值 + min(上,左)。
21. LC 75 颜色分类
难度:中等
标签:数组、双指针
原题简述
原地排序只包含 0、1、2 的数组。
知识点介绍
荷兰国旗问题:左边放 0,中间放 1,右边放 2。
标准思路
p0 指向下一个 0 位置,p2 指向下一个 2 位置,i 扫描。
最优标准解
from typing import List
class Solution:
def sortColors(self, nums: List[int]) -> None:
p0, i, p2 = 0, 0, len(nums) - 1
while i <= p2:
if nums[i] == 0: # 当前是 0,交换到左侧 0 区
nums[p0], nums[i] = nums[i], nums[p0]
p0 += 1
i += 1 # 当前位置已经处理好,继续扫描下一位
elif nums[i] == 2: # 当前是 2,交换到右侧 2 区
nums[p2], nums[i] = nums[i], nums[p2]
p2 -= 1
else:
i += 1 # 当前位置已经处理好,继续扫描下一位
复杂度
时间 O(n),空间 O(1)。
易错点
遇到 2 交换后 i 不加,因为换来的数未知。
多解补充
多解 1:计数排序
取舍说明: 简单但需要两遍。
from typing import List
class Solution:
def sortColors(self, nums: List[int]) -> None:
cnt = [0, 0, 0]
for x in nums: cnt[x] += 1
idx = 0
for color in range(3):
for _ in range(cnt[color]):
nums[idx] = color
idx += 1
快速复习
颜色分类:三指针荷兰国旗。
22. LC 78 子集
难度:中等
标签:回溯、位运算
原题简述
返回数组所有子集。
知识点介绍
每个元素都有选或不选两种选择。
标准思路
回溯从 start 往后选择,每个路径都是一个子集。
最优标准解
from typing import List
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
ans, path = [], []
def dfs(start):
ans.append(path[:]) # 把当前路径拷贝进答案,避免后续回溯修改
for i in range(start, len(nums)): # 从 start 开始选择,避免组合顺序不同造成重复
path.append(nums[i]) # 加入当前路径、节点或中间结果
dfs(i + 1)
path.pop() # 撤销选择,继续尝试下一个候选
dfs(0)
return ans # 返回最终答案
复杂度
时间 O(n·2^n),空间 O(n) 不算答案。
易错点
每个路径都要加入答案,不只叶子节点。
多解补充
多解 1:位掩码
取舍说明: 同样经典,用二进制位表示选不选。
from typing import List
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
ans = []
n = len(nums)
for mask in range(1 << n):
cur = []
for i in range(n):
if mask & (1 << i):
cur.append(nums[i]) # 加入当前路径、节点或中间结果
ans.append(cur)
return ans # 返回最终答案
快速复习
子集:每层都收集路径。
23. LC 79 单词搜索
难度:中等
标签:回溯、矩阵 DFS
原题简述
在字符网格中判断是否存在一条相邻路径拼出 word。
知识点介绍
路径不能重复使用同一个格子,适合 DFS 回溯。
标准思路
从每个格子尝试作为起点,匹配一个字符后向四周扩展,走完后恢复标记。
最优标准解
from typing import List
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
m, n = len(board), len(board[0])
def dfs(i, j, k):
if k == len(word):
return True # 当前检查通过
if i < 0 or i >= m or j < 0 or j >= n or board[i][j] != word[k]: # 当前格子字符不匹配,当前路径失败
return False # 当前情况不符合题意
ch = board[i][j]
board[i][j] = '#' # 临时标记当前格子已使用,防止本路径重复走
ok = dfs(i+1,j,k+1) or dfs(i-1,j,k+1) or dfs(i,j+1,k+1) or dfs(i,j-1,k+1)
board[i][j] = ch
return ok
for i in range(m):
for j in range(n):
if dfs(i, j, 0):
return True # 当前检查通过
return False # 当前情况不符合题意
复杂度
时间 O(mn·4^L),空间 O(L)。
易错点
访问标记要恢复;越界和字符不等要先排除。
多解补充
多解 1:额外 visited
取舍说明: 不修改 board,但需要 O(mn) 空间。
from typing import List
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
m,n=len(board),len(board[0])
vis=set()
def dfs(i,j,k):
if k==len(word):
return True # 当前检查通过
if i<0 or i>=m or j<0 or j>=n or (i,j) in vis or board[i][j]!=word[k]:
return False # 当前情况不符合题意
vis.add((i,j))
ok=dfs(i+1,j,k+1) or dfs(i-1,j,k+1) or dfs(i,j+1,k+1) or dfs(i,j-1,k+1)
vis.remove((i,j))
return ok
return any(dfs(i,j,0) for i in range(m) for j in range(n))
快速复习
单词搜索:矩阵回溯。
24. LC 96 不同的二叉搜索树
难度:中等
标签:动态规划、树
原题简述
给定 n,求由 1..n 能组成多少种不同 BST。
知识点介绍
选 k 为根时,左边有 k-1 个数,右边有 n-k 个数,左右组合数相乘。
标准思路
dp[i] 表示 i 个节点能组成的 BST 数量。
最优标准解
class Solution:
def numTrees(self, n: int) -> int:
dp = [0] * (n + 1)
dp[0] = dp[1] = 1 # 空树和单节点树是递推基础
for nodes in range(2, n + 1):
for left in range(nodes):
right = nodes - 1 - left
dp[nodes] += dp[left] * dp[right]
return dp[n]
复杂度
时间 O(n²),空间 O(n)。
易错点
空树数量 dp[0]=1;左右数量相乘。
多解补充
多解 1:递归记忆化
取舍说明: 同样思路,写法更接近定义。
from functools import lru_cache
class Solution:
def numTrees(self, n: int) -> int:
@lru_cache(None)
def f(nodes):
if nodes <= 1:
return 1
return sum(f(left) * f(nodes - 1 - left) for left in range(nodes))
return f(n)
快速复习
BST 计数:枚举根,左右组合相乘。
25. LC 98 验证二叉搜索树
难度:中等
标签:树、DFS、中序遍历
原题简述
判断二叉树是否满足 BST 性质。
知识点介绍
BST 的中序遍历严格递增;也可以递归维护上下界。
标准思路
每个节点必须处于合法区间 (low, high) 内。
最优标准解
from typing import Optional
class Solution:
def isValidBST(self, root: Optional[TreeNode]) -> bool:
def dfs(node, low, high):
if not node:
return True # 当前检查通过
if not (low < node.val < high): # 用上下界限制当前节点值的合法范围
return False # 当前情况不符合题意
return dfs(node.left, low, node.val) and dfs(node.right, node.val, high) # 用上下界限制当前节点值的合法范围
return dfs(root, float('-inf'), float('inf'))
复杂度
时间 O(n),空间 O(h)。
易错点
不能只比较直接左右孩子;要带上下界。
多解补充
多解 1:中序遍历
取舍说明: 同样优秀,利用中序严格递增。
from typing import Optional
class Solution:
def isValidBST(self, root: Optional[TreeNode]) -> bool:
prev = None
def inorder(node):
nonlocal prev
if not node:
return True # 当前检查通过
if not inorder(node.left):
return False # 当前情况不符合题意
if prev is not None and node.val <= prev:
return False # 当前情况不符合题意
prev = node.val
return inorder(node.right)
return inorder(root)
快速复习
验证 BST:递归上下界。
26. LC 102 二叉树的层序遍历
难度:中等
标签:树、BFS
原题简述
按层返回二叉树节点值。
知识点介绍
层序遍历用队列,关键是每轮固定当前层节点数。
标准思路
队列中先放根节点;每次处理一整层并收集该层值。
最优标准解
from typing import List, Optional
from collections import deque
class Solution:
def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
if not root:
return []
q = deque([root]) # 队列用于按层遍历二叉树
ans = []
while q:
level = [] # 保存当前这一层的节点值
for _ in range(len(q)): # 固定当前层节点数量,避免和下一层混在一起
node = q.popleft()
level.append(node.val) # 把当前节点值加入这一层结果
if node.left:
q.append(node.left) # 加入当前路径、节点或中间结果
if node.right:
q.append(node.right) # 加入当前路径、节点或中间结果
ans.append(level) # 当前层遍历完成,加入总结果
return ans # 返回最终答案
复杂度
时间 O(n),空间 O(n)。
易错点
每层循环前要固定 len(q)。
多解补充
多解 1:DFS 按深度收集
取舍说明: 也能做,但 BFS 更自然。
from typing import List, Optional
class Solution:
def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
ans = []
def dfs(node, depth):
if not node: # 空节点是递归边界
return # 结束当前递归分支
if depth == len(ans):
ans.append([])
ans[depth].append(node.val) # 加入当前路径、节点或中间结果
dfs(node.left, depth + 1)
dfs(node.right, depth + 1)
dfs(root, 0)
return ans # 返回最终答案
快速复习
层序遍历:队列按层处理。
27. LC 105 从前序与中序遍历序列构造二叉树
难度:中等
标签:树、递归、哈希表
原题简述
根据 preorder 和 inorder 构造二叉树。
知识点介绍
前序第一个是根;中序中根左边是左子树,右边是右子树。
标准思路
用哈希表快速定位根在中序中的位置,递归构建左右区间。
最优标准解
from typing import List, Optional
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
pos = {v:i for i,v in enumerate(inorder)}
pre_i = 0
def build(l, r):
nonlocal pre_i
if l > r:
return None # 没有可返回的节点或结果
val = preorder[pre_i]
pre_i += 1
root = TreeNode(val)
mid = pos[val]
root.left = build(l, mid - 1) # 递归构造左子树
root.right = build(mid + 1, r) # 递归构造右子树
return root # 返回处理后的根节点
return build(0, len(inorder) - 1)
复杂度
时间 O(n),空间 O(n)。
易错点
先构建左子树再右子树,因为前序顺序是根左右。
多解补充
多解 1:切片递归
取舍说明: 好理解但切片会产生额外 O(n²) 开销。
from typing import List, Optional
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
if not preorder:
return None # 没有可返回的节点或结果
root_val = preorder[0] # 前序区间第一个值就是当前子树根节点
idx = inorder.index(root_val) # 记录中序遍历中每个值的位置,方便切分左右子树
root = TreeNode(root_val) # 前序区间第一个值就是当前子树根节点
root.left = self.buildTree(preorder[1:1+idx], inorder[:idx]) # 记录中序遍历中每个值的位置,方便切分左右子树
root.right = self.buildTree(preorder[1+idx:], inorder[idx+1:]) # 记录中序遍历中每个值的位置,方便切分左右子树
return root # 返回处理后的根节点
快速复习
建树:前序定根,中序分左右。
28. LC 114 二叉树展开为链表
难度:中等
标签:树、DFS、链表
原题简述
将二叉树原地展开成前序遍历顺序的右链表。
知识点介绍
展开结果是前序:根、左、右。可以后序处理,把左右子树先展开再拼接。
标准思路
记录左子树和右子树,左子树接到右边,再把原右子树接到左子树尾部。
最优标准解
from typing import Optional
class Solution:
def flatten(self, root: Optional[TreeNode]) -> None:
cur = root
while cur:
if cur.left:
pre = cur.left
while pre.right:
pre = pre.right
pre.right = cur.right
cur.right = cur.left
cur.left = None
cur = cur.right
复杂度
时间 O(n²) 最坏,空间 O(1);若用后序可 O(n)。
易错点
原地修改;左指针必须置空。
多解补充
多解 1:反向前序递归
取舍说明: 更简洁,时间 O(n),但递归栈 O(h)。
from typing import Optional
class Solution:
def flatten(self, root: Optional[TreeNode]) -> None:
prev = None
def dfs(node):
nonlocal prev
if not node: # 空节点是递归边界
return # 结束当前递归分支
dfs(node.right) # 递归处理右子树
dfs(node.left) # 递归处理左子树
node.right = prev
node.left = None
prev = node
dfs(root)
快速复习
展开树:前序右链表。
29. LC 128 最长连续序列
难度:中等
标签:哈希表
原题简述
返回数组中最长连续整数序列长度,要求 O(n)。
知识点介绍
只有当 x-1 不存在时,x 才可能是一段连续序列的起点。
标准思路
把所有数放入 set,从每个起点向后数长度。
最优标准解
from typing import List
class Solution:
def longestConsecutive(self, nums: List[int]) -> int:
s = set(nums) # 集合支持 O(1) 判断某个数字是否存在
ans = 0
for x in s:
if x - 1 not in s: # 只有连续序列的起点才开始扩展,避免重复计算
y = x
while y in s:
y += 1
ans = max(ans, y - x)
return ans # 返回最终答案
复杂度
时间 O(n),空间 O(n)。
易错点
必须只从起点扩展,否则会退化。
多解补充
多解 1:排序
取舍说明: 可做但 O(n log n),不满足最优要求。
from typing import List
class Solution:
def longestConsecutive(self, nums: List[int]) -> int:
if not nums:
return 0
nums = sorted(set(nums)) # 集合支持 O(1) 判断某个数字是否存在
ans = cur = 1
for i in range(1, len(nums)):
cur = cur + 1 if nums[i] == nums[i-1] + 1 else 1
ans = max(ans, cur)
return ans # 返回最终答案
快速复习
连续序列:set + 找起点。
30. LC 139 单词拆分
难度:中等
标签:动态规划、字符串
原题简述
判断字符串能否被字典中的单词拼接出来。
知识点介绍
dp[i] 表示 s[:i] 能否被拆分。
标准思路
枚举结尾 i,再枚举上一个切分点 j,若 dp[j] 且 s[j:i] 在字典中则 dp[i]=True。
最优标准解
from typing import List
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
words = set(wordDict) # 词典转集合,快速判断子串是否是单词
dp = [False] * (len(s) + 1) # 创建 DP 表,保存子问题答案
dp[0] = True # 创建 DP 表,保存子问题答案
for i in range(1, len(s) + 1):
for j in range(i):
if dp[j] and s[j:i] in words: # 前半段可拆分且后半段是单词,则 s[:i] 可拆分
dp[i] = True # 创建 DP 表,保存子问题答案
break
return dp[-1]
复杂度
时间 O(n²·切片成本),空间 O(n)。
易错点
dp[0]=True 表示空串可拆;字典用 set。
多解补充
多解 1:DFS 记忆化
取舍说明: 同样正确,适合从递归角度理解。
from typing import List
from functools import lru_cache
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
words = set(wordDict) # 词典转集合,快速判断子串是否是单词
@lru_cache(None)
def dfs(start):
if start == len(s):
return True # 当前检查通过
for end in range(start+1, len(s)+1):
if s[start:end] in words and dfs(end):
return True # 当前检查通过
return False # 当前情况不符合题意
return dfs(0)
快速复习
单词拆分:dp[i] 看前一个切分点。
31. LC 142 环形链表 II
难度:中等
标签:链表、双指针
原题简述
返回链表入环节点,无环返回 None。
知识点介绍
快慢指针相遇后,从头和相遇点同时走,会在入环点相遇。
标准思路
先判环,再用两个指针找入口。
最优标准解
from typing import Optional
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
slow = fast = head # slow 找中点或入环点,fast 用来制造速度差
while fast and fast.next: # fast 还能走两步时,继续推进快慢指针
slow = slow.next # slow 每次走一步
fast = fast.next.next # fast 每次走两步
if slow is fast: # 快慢指针相遇,说明存在环
p = head
while p is not slow:
p = p.next
slow = slow.next # slow 每次走一步
return p
return None # 没有可返回的节点或结果
复杂度
时间 O(n),空间 O(1)。
易错点
相遇不一定是入口;第二阶段找入口。
多解补充
多解 1:哈希集合
取舍说明: 直观但空间 O(n)。
from typing import Optional
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
seen = set()
while head:
if head in seen:
return head
seen.add(head)
head = head.next
return None # 没有可返回的节点或结果
快速复习
环入口:相遇后头指针和相遇点同步走。
32. LC 146 LRU 缓存
难度:中等
标签:设计、哈希表、双向链表
原题简述
实现 get/put,最近最少使用的键在容量满时淘汰。
知识点介绍
哈希表负责 O(1) 找节点,双向链表负责 O(1) 调整使用顺序。
标准思路
每次访问或更新节点都移到链表尾部,头部是真正最久未使用。
最优标准解
class Node:
def __init__(self, key=0, val=0):
self.key, self.val = key, val
self.prev = self.next = None
class LRUCache:
def __init__(self, capacity: int):
self.cap = capacity
self.map = {}
self.head, self.tail = Node(), Node()
self.head.next = self.tail
self.tail.prev = self.head
def _remove(self, node):
node.prev.next = node.next
node.next.prev = node.prev
def _add_last(self, node):
prev = self.tail.prev
prev.next = node
node.prev = prev
node.next = self.tail
self.tail.prev = node
def get(self, key: int) -> int:
if key not in self.map:
return -1
node = self.map[key]
self._remove(node)
self._add_last(node)
return node.val
def put(self, key: int, value: int) -> None:
if key in self.map:
node = self.map[key]
node.val = value
self._remove(node)
self._add_last(node)
else:
node = Node(key, value)
self.map[key] = node
self._add_last(node)
if len(self.map) > self.cap:
old = self.head.next
self._remove(old)
del self.map[old.key]
复杂度
get/put 时间 O(1),空间 O(capacity)。
易错点
删除哈希表时要用节点 key;哨兵头尾能减少边界判断。
多解补充
多解 1:OrderedDict
取舍说明: Python 简洁实现,但面试常要求手写双向链表。
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity: int):
self.cap = capacity
self.od = OrderedDict() # 有序字典同时保存键值和访问顺序
def get(self, key: int) -> int:
if key not in self.od:
return -1
self.od.move_to_end(key) # 访问过的 key 移到末尾,表示最近使用
return self.od[key]
def put(self, key: int, value: int) -> None:
if key in self.od:
self.od.move_to_end(key) # 访问过的 key 移到末尾,表示最近使用
self.od[key] = value
if len(self.od) > self.cap:
self.od.popitem(last=False) # 容量超限时删除最久未使用的队头元素
快速复习
LRU:哈希表 + 双向链表。
33. LC 148 排序链表
难度:中等
标签:链表、归并排序
原题简述
对链表进行升序排序。
知识点介绍
链表适合归并排序,因为可以用快慢指针拆分,用指针合并。
标准思路
快慢指针找到中点断开,递归排序两半,再合并有序链表。
最优标准解
from typing import Optional
class Solution:
def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
if not head or not head.next:
return head
slow, fast = head, head.next # 快慢指针找链表中点
while fast and fast.next: # fast 还能走两步时,继续推进快慢指针
slow = slow.next # slow 每次走一步
fast = fast.next.next # fast 每次走两步
mid = slow.next # 右半部分从 mid 开始
slow.next = None # 断开左右两段,方便分别递归排序
left = self.sortList(head)
right = self.sortList(mid)
dummy = cur = ListNode() # 虚拟头节点,方便统一处理链表头部变化
while left and right:
if left.val <= right.val:
cur.next = left
left = left.next
else:
cur.next = right
right = right.next
cur = cur.next
cur.next = left if left else right
return dummy.next # 返回虚拟头节点后面的真实链表头
复杂度
时间 O(n log n),空间 O(log n) 递归栈。
易错点
拆分时要断开 slow.next。
多解补充
多解 1:转数组排序
取舍说明: 简单但不是链表算法核心,空间 O(n)。
from typing import Optional
class Solution:
def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
vals = []
while head:
vals.append(head.val) # 加入当前路径、节点或中间结果
head = head.next
vals.sort() # 排序后方便利用有序性
dummy = cur = ListNode() # 虚拟头节点,方便统一处理链表头部变化
for x in vals:
cur.next = ListNode(x)
cur = cur.next
return dummy.next # 返回虚拟头节点后面的真实链表头
快速复习
链表排序:归并最自然。
34. LC 152 乘积最大子数组
难度:中等
标签:动态规划
原题简述
返回连续子数组的最大乘积。
知识点介绍
负数会让最大值和最小值互换,所以同时维护当前最大乘积和最小乘积。
标准思路
遍历每个数,若 x 为负先交换 imax/imin,再更新。
最优标准解
from typing import List
class Solution:
def maxProduct(self, nums: List[int]) -> int:
imax = imin = ans = nums[0]
for x in nums[1:]:
if x < 0: # 遇到负数时,最大乘积和最小乘积交换角色
imax, imin = imin, imax
imax = max(x, imax * x)
imin = min(x, imin * x)
ans = max(ans, imax) # 用当前位置结尾的最大和更新全局答案
return ans # 返回最终答案
复杂度
时间 O(n),空间 O(1)。
易错点
负数要交换最大和最小;0 会自动重开。
多解补充
多解 1:暴力枚举
取舍说明: 能做但 O(n²),不推荐。
from typing import List
class Solution:
def maxProduct(self, nums: List[int]) -> int:
ans = nums[0]
for i in range(len(nums)):
prod = 1
for j in range(i, len(nums)):
prod *= nums[j]
ans = max(ans, prod) # 用当前位置结尾的最大和更新全局答案
return ans # 返回最终答案
快速复习
最大乘积:同时维护最大/最小。
35. LC 155 最小栈
难度:中等
标签:栈、设计
原题简述
设计支持 push/pop/top/getMin 都为 O(1) 的栈。
知识点介绍
辅助栈同步记录当前最小值。
标准思路
每次 push 时把 min(x, 当前最小值) 压入 min_stack。
最优标准解
class MinStack:
def __init__(self):
self.stack = [] # 栈保存还没结算的下标或状态
self.min_stack = [] # 栈保存还没结算的下标或状态
def push(self, val: int) -> None:
self.stack.append(val) # 加入当前路径、节点或中间结果
cur_min = val if not self.min_stack else min(val, self.min_stack[-1]) # 栈保存还没结算的下标或状态
self.min_stack.append(cur_min) # 加入当前路径、节点或中间结果
def pop(self) -> None:
self.stack.pop() # 弹出真实栈顶元素
self.min_stack.pop() # 同步弹出这一层对应的最小值
def top(self) -> int:
return self.stack[-1]
def getMin(self) -> int:
return self.min_stack[-1]
复杂度
各操作 O(1),空间 O(n)。
易错点
主栈和辅助栈要同步 push/pop。
多解补充
多解 1:辅助栈只存更小值
取舍说明: 空间可能更省,但 pop 逻辑要小心。
class MinStack:
def __init__(self):
self.stack = [] # 栈保存还没结算的下标或状态
self.min_stack = [] # 栈保存还没结算的下标或状态
def push(self, val: int) -> None:
self.stack.append(val) # 加入当前路径、节点或中间结果
if not self.min_stack or val <= self.min_stack[-1]: # 栈保存还没结算的下标或状态
self.min_stack.append(val) # 加入当前路径、节点或中间结果
def pop(self) -> None:
val = self.stack.pop() # 栈保存还没结算的下标或状态
if val == self.min_stack[-1]: # 栈保存还没结算的下标或状态
self.min_stack.pop() # 同步弹出这一层对应的最小值
def top(self) -> int:
return self.stack[-1]
def getMin(self) -> int:
return self.min_stack[-1]
快速复习
最小栈:辅助栈存当前最小。
36. LC 198 打家劫舍
难度:中等
标签:动态规划
原题简述
不能偷相邻房屋,求最大金额。
知识点介绍
到第 i 间时,要么不偷它,要么偷它并加上 i-2 的最优值。
标准思路
用两个变量滚动表示前一间和前两间的最优值。
最优标准解
from typing import List
class Solution:
def rob(self, nums: List[int]) -> int:
prev2 = prev1 = 0 # prev2 表示偷到前两间房时的最大金额
for money in nums:
cur = max(prev1, prev2 + money) # 在不同选择中取收益最大的方案
prev2, prev1 = prev1, cur # prev2 表示偷到前两间房时的最大金额
return prev1 # prev1 表示偷到前一间房时的最大金额
复杂度
时间 O(n),空间 O(1)。
易错点
状态是最大金额,不是当前是否偷。
多解补充
多解 1:DP 数组
取舍说明: 更直观但空间 O(n)。
from typing import List
class Solution:
def rob(self, nums: List[int]) -> int:
if not nums:
return 0
dp = [0] * (len(nums) + 1) # 创建 DP 表,保存子问题答案
for i, x in enumerate(nums, 1):
dp[i] = max(dp[i-1], dp[i-2] + x) # 创建 DP 表,保存子问题答案
return dp[-1]
快速复习
打家劫舍:偷当前=前前+当前,不偷=前一。
37. LC 200 岛屿数量
难度:中等
标签:DFS、BFS、并查集
原题简述
网格中 1 表示陆地,0 表示水,返回岛屿个数。
知识点介绍
遇到一块未访问陆地,就用 DFS/BFS 把整座岛淹掉。
标准思路
遍历每个格子,遇到 1 就答案加一并把相连 1 改为 0。
最优标准解
from typing import List
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
m, n = len(grid), len(grid[0])
def dfs(i, j):
if i<0 or i>=m or j<0 or j>=n or grid[i][j] != '1':
return # 结束当前递归分支
grid[i][j] = '0' # 把当前陆地标为水,避免重复统计
dfs(i+1,j)
dfs(i-1,j)
dfs(i,j+1)
dfs(i,j-1)
ans = 0
for i in range(m):
for j in range(n):
if grid[i][j] == '1': # 发现一块未访问陆地,说明找到一个新岛屿
ans += 1
dfs(i, j)
return ans # 返回最终答案
复杂度
时间 O(mn),空间 O(mn) 最坏递归栈。
易错点
访问后要标记,防止重复。
多解补充
多解 1:BFS
取舍说明: 避免递归过深。
from typing import List
from collections import deque
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
m,n=len(grid),len(grid[0])
ans=0
for i in range(m):
for j in range(n):
if grid[i][j]=='1':
ans += 1
grid[i][j]='0'
q=deque([(i,j)])
while q:
x,y=q.popleft()
for dx,dy in [(1,0),(-1,0),(0,1),(0,-1)]:
a,b=x+dx,y+dy
if 0<=a<m and 0<=b<n and grid[a][b]=='1':
grid[a][b]='0'
q.append((a,b)) # 加入当前路径、节点或中间结果
return ans # 返回最终答案
快速复习
岛屿数量:发现陆地就淹掉整座岛。
38. LC 207 课程表
难度:中等
标签:图、拓扑排序
原题简述
给课程数和先修关系,判断能否完成所有课程。
知识点介绍
有向图有环则无法完成;拓扑排序可检测环。
标准思路
建立入度和邻接表,先学入度为 0 的课,逐步减少后续课程入度。
最优标准解
from typing import List
from collections import deque
class Solution:
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
graph = [[] for _ in range(numCourses)] # 建图:先修课指向依赖它的后续课程
indeg = [0] * numCourses
for a, b in prerequisites:
graph[b].append(a) # 加入当前路径、节点或中间结果
indeg[a] += 1
q = deque([i for i in range(numCourses) if indeg[i] == 0])
learned = 0
while q:
course = q.popleft()
learned += 1
for nxt in graph[course]:
indeg[nxt] -= 1
if indeg[nxt] == 0:
q.append(nxt) # 加入当前路径、节点或中间结果
return learned == numCourses
复杂度
时间 O(V+E),空间 O(V+E)。
易错点
边方向是 prerequisite -> course。
多解补充
多解 1:DFS 三色标记
取舍说明: 同样优秀,用递归检测有向环。
from typing import List
class Solution:
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
graph=[[] for _ in range(numCourses)] # 建图:先修课指向依赖它的后续课程
for a,b in prerequisites: graph[b].append(a) # 加入当前路径、节点或中间结果
color=[0]*numCourses
def dfs(u):
if color[u]==1:
return False # 当前情况不符合题意
if color[u]==2:
return True # 当前检查通过
color[u]=1
for v in graph[u]:
if not dfs(v):
return False # 当前情况不符合题意
color[u]=2
return True # 当前检查通过
return all(dfs(i) for i in range(numCourses))
快速复习
课程表:拓扑排序判环。
39. LC 208 实现 Trie 前缀树
难度:中等
标签:设计、字典树
原题简述
实现插入单词、查找单词、查找前缀。
知识点介绍
Trie 用树结构按字符存储路径,节点需要 children 和 is_end。
标准思路
插入时沿字符创建节点;搜索时沿字符走,最后检查 is_end 或仅检查路径存在。
最优标准解
class TrieNode:
def __init__(self):
self.children = {} # children 保存当前节点到下一层字符节点的映射
self.is_end = False # 标记是否有单词在当前节点结束
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word: str) -> None:
node = self.root
for ch in word:
if ch not in node.children: # children 保存当前节点到下一层字符节点的映射
node.children[ch] = TrieNode() # children 保存当前节点到下一层字符节点的映射
node = node.children[ch] # children 保存当前节点到下一层字符节点的映射
node.is_end = True # 标记是否有单词在当前节点结束
def search(self, word: str) -> bool:
node = self._find(word)
return bool(node and node.is_end) # 标记是否有单词在当前节点结束
def startsWith(self, prefix: str) -> bool:
return self._find(prefix) is not None
def _find(self, s):
node = self.root
for ch in s:
if ch not in node.children: # children 保存当前节点到下一层字符节点的映射
return None # 没有可返回的节点或结果
node = node.children[ch] # children 保存当前节点到下一层字符节点的映射
return node # 返回当前构造出的节点
复杂度
操作时间 O(L),空间与插入字符总数相关。
易错点
search 要检查 is_end;startsWith 不用。
多解补充
多解 1:嵌套字典
取舍说明: 代码更短,但结构不如节点类清晰。
class Trie:
def __init__(self):
self.root = {}
def insert(self, word: str) -> None:
node = self.root
for ch in word:
node = node.setdefault(ch, {})
node['#'] = True
def search(self, word: str) -> bool:
node = self.root
for ch in word:
if ch not in node:
return False # 当前情况不符合题意
node = node[ch]
return '#' in node
def startsWith(self, prefix: str) -> bool:
node = self.root
for ch in prefix:
if ch not in node:
return False # 当前情况不符合题意
node = node[ch]
return True # 当前检查通过
快速复习
Trie:按字符建树。
40. LC 215 数组中的第 K 个最大元素
难度:中等
标签:堆、快速选择
原题简述
返回数组中第 k 大元素。
知识点介绍
小根堆维护 k 个最大元素,堆顶就是第 k 大;快速选择平均 O(n)。
标准思路
遍历元素压入小根堆,堆大小超过 k 就弹出最小。
最优标准解
from typing import List
import heapq
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
heap = [] # 堆用来维护当前最有价值的 k 个元素
for x in nums:
heapq.heappush(heap, x) # 把当前候选放入堆
if len(heap) > k:
heapq.heappop(heap) # 堆大小超过 k 时,弹出优先级最低的元素
return heap[0]
复杂度
时间 O(n log k),空间 O(k)。
易错点
维护的是 k 个最大,不是全量排序。
多解补充
多解 1:排序
取舍说明: 最简单但 O(n log n)。
from typing import List
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
nums.sort(reverse=True) # 排序后方便利用有序性
return nums[k - 1]
快速复习
第 K 大:小根堆大小 k。
41. LC 221 最大正方形
难度:中等
标签:动态规划、矩阵
原题简述
在 0/1 矩阵中找到只含 1 的最大正方形面积。
知识点介绍
dp[i][j] 表示以该格为右下角的最大正方形边长。
标准思路
若当前为 1,边长由上、左、左上三者最小值 +1 决定。
最优标准解
from typing import List
class Solution:
def maximalSquare(self, matrix: List[List[str]]) -> int:
m, n = len(matrix), len(matrix[0])
dp = [[0]*(n+1) for _ in range(m+1)] # 创建 DP 表,保存子问题答案
best = 0
for i in range(1, m+1):
for j in range(1, n+1):
if matrix[i-1][j-1] == '1':
dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 # 创建 DP 表,保存子问题答案
best = max(best, dp[i][j]) # 创建 DP 表,保存子问题答案
return best * best
复杂度
时间 O(mn),空间 O(mn)。
易错点
返回面积不是边长;边界可用多一行一列简化。
多解补充
多解 1:空间压缩
取舍说明: 同样优秀,把空间降到 O(n)。
from typing import List
class Solution:
def maximalSquare(self, matrix: List[List[str]]) -> int:
m,n=len(matrix),len(matrix[0])
dp=[0]*(n+1) # 创建 DP 表,保存子问题答案
best=prev=0
for i in range(1,m+1):
prev=0
for j in range(1,n+1):
tmp=dp[j] # 创建 DP 表,保存子问题答案
if matrix[i-1][j-1]=='1':
dp[j]=min(dp[j],dp[j-1],prev)+1 # 创建 DP 表,保存子问题答案
best=max(best,dp[j]) # 创建 DP 表,保存子问题答案
else:
dp[j]=0 # 创建 DP 表,保存子问题答案
prev=tmp
return best*best
快速复习
最大正方形:右下角 DP。
42. LC 236 二叉树的最近公共祖先
难度:中等
标签:树、DFS
原题简述
给定二叉树和两个节点,返回最近公共祖先。
知识点介绍
如果一个节点的左右子树分别找到 p 和 q,则该节点就是 LCA。
标准思路
递归返回当前子树中是否找到 p/q;左右都非空返回 root。
最优标准解
from typing import Optional
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
if not root or root is p or root is q:
return root # 返回处理后的根节点
left = self.lowestCommonAncestor(root.left, p, q)
right = self.lowestCommonAncestor(root.right, p, q)
if left and right:
return root # 返回处理后的根节点
return left if left else right
复杂度
时间 O(n),空间 O(h)。
易错点
不是 BST,不能用大小关系;比较节点对象。
多解补充
多解 1:父指针集合
取舍说明: 需要额外空间,思路直观。
class Solution:
def lowestCommonAncestor(self, root, p, q):
parent = {root: None}
stack = [root]
while p not in parent or q not in parent:
node = stack.pop()
if node.left:
parent[node.left]=node
stack.append(node.left) # 加入当前路径、节点或中间结果
if node.right:
parent[node.right]=node
stack.append(node.right) # 加入当前路径、节点或中间结果
seen=set()
while p:
seen.add(p)
p=parent[p]
while q not in seen:
q=parent[q]
return q
快速复习
LCA:左右都找到则当前是祖先。
43. LC 238 除自身以外数组的乘积
难度:中等
标签:数组、前缀积
原题简述
返回每个位置除自身外其余元素乘积,不能用除法。
知识点介绍
某位置答案=左边所有数乘积*右边所有数乘积。
标准思路
先从左到右存左乘积,再从右到左乘上右乘积。
最优标准解
from typing import List
class Solution:
def productExceptSelf(self, nums: List[int]) -> List[int]:
n=len(nums)
ans=[1]*n
left=1
for i in range(n):
ans[i]=left
left*=nums[i]
right=1
for i in range(n-1,-1,-1):
ans[i]*=right
right*=nums[i]
return ans # 返回最终答案
复杂度
时间 O(n),空间 O(1) 不算返回数组。
易错点
不能把返回数组算作额外空间;左右乘积初始为 1。
多解补充
多解 1:左右数组
取舍说明: 更直观但空间 O(n)。
from typing import List
class Solution:
def productExceptSelf(self, nums: List[int]) -> List[int]:
n=len(nums)
left=[1]*n
right=[1]*n
for i in range(1,n): left[i]=left[i-1]*nums[i-1]
for i in range(n-2,-1,-1): right[i]=right[i+1]*nums[i+1]
return [left[i]*right[i] for i in range(n)]
快速复习
除自身乘积:左积 * 右积。
44. LC 240 搜索二维矩阵 II
难度:中等
标签:矩阵、二分、双指针
原题简述
矩阵每行每列升序,判断 target 是否存在。
知识点介绍
从右上角开始:左边更小,下面更大,能单调排除一行或一列。
标准思路
若当前大于 target 左移;小于 target 下移。
最优标准解
from typing import List
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
m,n=len(matrix),len(matrix[0])
i,j=0,n-1
while i<m and j>=0:
if matrix[i][j]==target:
return True # 当前检查通过
if matrix[i][j]>target:
j-=1
else:
i+=1
return False # 当前情况不符合题意
复杂度
时间 O(m+n),空间 O(1)。
易错点
从右上或左下开始才有单调排除效果。
多解补充
多解 1:每行二分
取舍说明: 可做但 O(m log n)。
from typing import List
import bisect
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
for row in matrix:
idx=bisect.bisect_left(row,target)
if idx<len(row) and row[idx]==target:
return True # 当前检查通过
return False # 当前情况不符合题意
快速复习
二维搜索:右上角走楼梯。
45. LC 279 完全平方数
难度:中等
标签:动态规划、BFS
原题简述
给定 n,求和为 n 的完全平方数最少数量。
知识点介绍
完全背包:平方数可重复使用。
标准思路
dp[i] 表示凑成 i 的最少平方数个数。
最优标准解
class Solution:
def numSquares(self, n: int) -> int:
squares=[i*i for i in range(1,int(n**0.5)+1)]
dp=[0]+[float('inf')]*n # 创建 DP 表,保存子问题答案
for i in range(1,n+1):
for s in squares:
if s>i:
break
dp[i]=min(dp[i],dp[i-s]+1) # 创建 DP 表,保存子问题答案
return dp[n]
复杂度
时间 O(n√n),空间 O(n)。
易错点
dp[0]=0;平方数可重复用。
多解补充
多解 1:BFS
取舍说明: 按层表示使用几个平方数,第一次到 n 即最少。
from collections import deque
class Solution:
def numSquares(self, n: int) -> int:
squares=[i*i for i in range(1,int(n**0.5)+1)]
q=deque([(0,0)])
seen={0}
while q:
cur,step=q.popleft()
for s in squares:
nxt=cur+s
if nxt==n:
return step+1
if nxt<n and nxt not in seen:
seen.add(nxt)
q.append((nxt,step+1)) # 加入当前路径、节点或中间结果
快速复习
完全平方数:完全背包 DP。
46. LC 287 寻找重复数
难度:中等
标签:数组、双指针、二分
原题简述
长度 n+1,数字在 1..n,只有一个重复数,不能改数组。
知识点介绍
把 nums[i] 看成指针 i->nums[i],重复数对应环入口。
标准思路
Floyd 判环后找入口。
最优标准解
from typing import List
class Solution:
def findDuplicate(self, nums: List[int]) -> int:
slow=fast=nums[0]
while True:
slow=nums[slow]
fast=nums[nums[fast]]
if slow==fast:
break
p=nums[0]
while p!=slow:
p=nums[p]
slow=nums[slow]
return p
复杂度
时间 O(n),空间 O(1)。
易错点
这是数组上的环,不是链表节点;初始化和移动方式要一致。
多解补充
多解 1:二分计数
取舍说明: 不改数组,O(n log n)。
from typing import List
class Solution:
def findDuplicate(self, nums: List[int]) -> int:
left,right=1,len(nums)-1
while left<right:
mid=(left+right)//2
cnt=sum(x<=mid for x in nums)
if cnt>mid:
right=mid
else:
left=mid+1
return left
快速复习
重复数:数组映射成环入口。
47. LC 300 最长递增子序列
难度:中等
标签:动态规划、二分
原题简述
返回最长严格递增子序列长度。
知识点介绍
tails[len] 保存长度为 len+1 的递增子序列的最小结尾。
标准思路
每个数用二分替换第一个 >= 它的位置。
最优标准解
from typing import List
import bisect
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
tails=[]
for x in nums:
i=bisect.bisect_left(tails,x)
if i==len(tails):
tails.append(x) # 加入当前路径、节点或中间结果
else:
tails[i]=x
return len(tails)
复杂度
时间 O(n log n),空间 O(n)。
易错点
tails 不是实际 LIS,只保存最优结尾。
多解补充
多解 1:O(n²) DP
取舍说明: 更直观,但大数据较慢。
from typing import List
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
dp=[1]*len(nums) # 创建 DP 表,保存子问题答案
for i in range(len(nums)):
for j in range(i):
if nums[j]<nums[i]:
dp[i]=max(dp[i],dp[j]+1) # 创建 DP 表,保存子问题答案
return max(dp) # 在不同选择中取收益最大的方案
快速复习
LIS:二分维护最小结尾。
48. LC 309 最佳买卖股票时机含冷冻期
难度:中等
标签:动态规划
原题简述
卖出股票后第二天不能买入,求最大利润。
知识点介绍
每天状态:持有、空仓且可买、空仓且冷冻。
标准思路
状态转移时卖出进入冷冻,冷冻下一天变为可买。
最优标准解
from typing import List
class Solution:
def maxProfit(self, prices: List[int]) -> int:
hold=-prices[0] # hold 表示当天结束后手里持有股票的最大利润
free=0
cool=0
for p in prices[1:]:
new_hold=max(hold, free-p) # hold 表示当天结束后手里持有股票的最大利润
new_free=max(free, cool)
new_cool=hold+p # hold 表示当天结束后手里持有股票的最大利润
hold,free,cool=new_hold,new_free,new_cool # hold 表示当天结束后手里持有股票的最大利润
return max(free,cool)
复杂度
时间 O(n),空间 O(1)。
易错点
买入只能从 free 来,不能从 cool 来。
多解补充
多解 1:DP 数组
取舍说明: 更直观但空间 O(n)。
from typing import List
class Solution:
def maxProfit(self, prices: List[int]) -> int:
n=len(prices)
hold=[0]*n # hold 表示当天结束后手里持有股票的最大利润
free=[0]*n
cool=[0]*n
hold[0]=-prices[0] # hold 表示当天结束后手里持有股票的最大利润
for i in range(1,n):
hold[i]=max(hold[i-1],free[i-1]-prices[i]) # hold 表示当天结束后手里持有股票的最大利润
free[i]=max(free[i-1],cool[i-1])
cool[i]=hold[i-1]+prices[i] # hold 表示当天结束后手里持有股票的最大利润
return max(free[-1],cool[-1])
快速复习
股票冷冻期:hold/free/cool 三状态。
49. LC 322 零钱兑换
难度:中等
标签:动态规划、完全背包
原题简述
给定硬币面额和总金额,求最少硬币数,不可达返回 -1。
知识点介绍
完全背包最少数量:硬币可重复使用。
标准思路
dp[x] 表示凑成金额 x 的最少硬币数。
最优标准解
from typing import List
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp=[float('inf')]*(amount+1) # 创建 DP 表,保存子问题答案
dp[0]=0 # 创建 DP 表,保存子问题答案
for x in range(1,amount+1):
for c in coins:
if x>=c:
dp[x]=min(dp[x],dp[x-c]+1) # 创建 DP 表,保存子问题答案
return -1 if dp[amount]==float('inf') else dp[amount] # 创建 DP 表,保存子问题答案
复杂度
时间 O(amount·len(coins)),空间 O(amount)。
易错点
不可达要返回 -1;dp[0]=0。
多解补充
多解 1:BFS
取舍说明: 按硬币数层层扩展,首次到达 amount 最少。
from typing import List
from collections import deque
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
q=deque([(0,0)])
seen={0}
while q:
cur,step=q.popleft()
if cur==amount:
return step
for c in coins:
nxt=cur+c
if nxt<=amount and nxt not in seen:
seen.add(nxt)
q.append((nxt,step+1)) # 加入当前路径、节点或中间结果
return -1
快速复习
零钱兑换:完全背包最少数。
50. LC 337 打家劫舍 III
难度:中等
标签:树形 DP
原题简述
二叉树上不能偷直接相连的两个节点,求最大金额。
知识点介绍
每个节点有两个状态:偷当前、不偷当前。
标准思路
递归返回 (not_rob, rob)。偷当前则孩子不能偷;不偷当前则孩子可偷可不偷。
最优标准解
from typing import Optional
class Solution:
def rob(self, root: Optional[TreeNode]) -> int:
def dfs(node):
if not node:
return (0,0)
l0,l1=dfs(node.left)
r0,r1=dfs(node.right)
rob=node.val+l0+r0
not_rob=max(l0,l1)+max(r0,r1) # 在不同选择中取收益最大的方案
return (not_rob, rob)
return max(dfs(root)) # 在不同选择中取收益最大的方案
复杂度
时间 O(n),空间 O(h)。
易错点
不能简单隔层相加;每个节点都要两个状态。
多解补充
多解 1:记忆化递归
取舍说明: 也能做,但状态不如二元返回清晰。
from typing import Optional
class Solution:
def rob(self, root: Optional[TreeNode]) -> int:
memo={}
def f(node):
if not node:
return 0
if node in memo:
return memo[node]
take=node.val
if node.left:
take += f(node.left.left)+f(node.left.right)
if node.right:
take += f(node.right.left)+f(node.right.right)
skip=f(node.left)+f(node.right)
memo[node]=max(take,skip) # 在不同选择中取收益最大的方案
return memo[node]
return f(root)
快速复习
树上打劫:返回偷/不偷。
51. LC 347 前 K 个高频元素
难度:中等
标签:哈希表、堆、桶排序
原题简述
返回出现频率最高的 k 个元素。
知识点介绍
先计数,再用堆或桶按频率取前 k。
标准思路
用小根堆维护 k 个最高频元素。
最优标准解
from typing import List
from collections import Counter
import heapq
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
count=Counter(nums) # 统计每个元素出现次数
heap=[] # 堆用来维护当前最有价值的 k 个元素
for num,freq in count.items():
heapq.heappush(heap,(freq,num)) # 把当前候选放入堆
if len(heap)>k:
heapq.heappop(heap) # 堆大小超过 k 时,弹出优先级最低的元素
return [num for freq,num in heap]
复杂度
时间 O(n log k),空间 O(n)。
易错点
堆里放频率;返回顺序不限。
多解补充
多解 1:桶排序
取舍说明: 同样优秀,频率最大不超过 n。
from typing import List
from collections import Counter
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
count=Counter(nums) # 统计每个元素出现次数
buckets=[[] for _ in range(len(nums)+1)]
for num,freq in count.items(): buckets[freq].append(num) # 加入当前路径、节点或中间结果
ans=[]
for f in range(len(buckets)-1,0,-1):
for num in buckets[f]:
ans.append(num)
if len(ans)==k:
return ans # 返回最终答案
快速复习
Top K 高频:计数 + 小根堆。
52. LC 394 字符串解码
难度:中等
标签:栈、字符串
原题简述
解码形如 k[encoded] 的字符串,支持嵌套。
知识点介绍
遇到 '[' 时保存当前字符串和重复次数,遇到 ']' 时弹出并拼接。
标准思路
num 负责读多位数字,cur 负责当前层字符串。
最优标准解
class Solution:
def decodeString(self, s: str) -> str:
stack=[]
cur=''
num=0
for ch in s:
if ch.isdigit():
num=num*10+int(ch)
elif ch=='[':
stack.append((cur,num)) # 加入当前路径、节点或中间结果
cur=''
num=0
elif ch==']':
prev,k=stack.pop()
cur=prev+cur*k
else:
cur+=ch
return cur
复杂度
时间 O(输出长度),空间 O(嵌套深度+输出长度)。
易错点
数字可能多位;嵌套要用栈。
多解补充
多解 1:递归解析
取舍说明: 同样正确,但索引管理更难。
class Solution:
def decodeString(self, s: str) -> str:
def dfs(i):
res=''
num=0
while i<len(s):
ch=s[i]
if ch.isdigit():
num=num*10+int(ch)
elif ch=='[':
sub,i=dfs(i+1)
res+=sub*num
num=0
elif ch==']':
return res,i
else:
res+=ch
i+=1
return res,i
return dfs(0)[0]
快速复习
解码字符串:栈保存上一层。
53. LC 399 除法求值
难度:中等
标签:图、DFS、并查集
原题简述
给变量除法关系和查询,返回查询结果。
知识点介绍
a/b=value 可看成有权图边 a->b=value,b->a=1/value。
标准思路
对每个查询从起点 DFS 到终点,沿途乘权重。
最优标准解
from typing import List
from collections import defaultdict
class Solution:
def calcEquation(self, equations: List[List[str]], values: List[float], queries: List[List[str]]) -> List[float]:
graph=defaultdict(list)
for (a,b),v in zip(equations,values):
graph[a].append((b,v)) # 加入当前路径、节点或中间结果
graph[b].append((a,1/v)) # 加入当前路径、节点或中间结果
def dfs(a,b,seen):
if a not in graph or b not in graph:
return -1.0
if a==b:
return 1.0
seen.add(a)
for nxt,w in graph[a]:
if nxt not in seen:
res=dfs(nxt,b,seen)
if res!=-1.0:
return w*res
return -1.0
return [dfs(a,b,set()) for a,b in queries]
复杂度
建图 O(E),每次查询最坏 O(V+E)。空间 O(V+E)。
易错点
要加反向边;不存在变量返回 -1。
多解补充
多解 1:Floyd
取舍说明: 查询多时可预处理所有比值,但空间 O(V²)。
from typing import List
from collections import defaultdict
class Solution:
def calcEquation(self, equations: List[List[str]], values: List[float], queries: List[List[str]]) -> List[float]:
dist=defaultdict(dict)
for (a,b),v in zip(equations,values):
dist[a][a]=dist[b][b]=1.0
dist[a][b]=v
dist[b][a]=1/v
for k in list(dist):
for i in list(dist):
for j in list(dist):
if k in dist[i] and j in dist[k]:
dist[i][j]=dist[i][k]*dist[k][j]
return [dist[a].get(b,-1.0) for a,b in queries]
快速复习
除法求值:有权图路径乘积。
54. LC 406 根据身高重建队列
难度:中等
标签:贪心、排序
原题简述
每个人 [h,k] 表示前面身高>=h 的人数为 k,重建队列。
知识点介绍
先放高个子,矮个子不会影响高个子的 k。
标准思路
按身高降序、k 升序排序,然后按 k 下标插入。
最优标准解
from typing import List
class Solution:
def reconstructQueue(self, people: List[List[int]]) -> List[List[int]]:
people.sort(key=lambda x: (-x[0], x[1])) # 排序后方便利用有序性
ans=[]
for p in people:
ans.insert(p[1], p)
return ans # 返回最终答案
复杂度
时间 O(n²),空间 O(n)。
易错点
排序规则必须是身高降序、k 升序。
多解补充
多解 1:反向思路
取舍说明: 低个先放需要空位统计,代码复杂。
from typing import List
class Solution:
def reconstructQueue(self, people: List[List[int]]) -> List[List[int]]:
people.sort(key=lambda x:(x[0],-x[1])) # 排序后方便利用有序性
ans=[None]*len(people)
for h,k in people:
spaces=k+1
for i in range(len(ans)):
if ans[i] is None:
spaces-=1
if spaces==0:
ans[i]=[h,k]
break
return ans # 返回最终答案
快速复习
重建队列:高个先插。
55. LC 416 分割等和子集
难度:中等
标签:动态规划、背包
原题简述
判断数组能否分成两个和相等的子集。
知识点介绍
总和必须为偶数;问题转成能否选若干数凑出 sum/2。
标准思路
0/1 背包,用 dp[j] 表示是否能凑出 j,倒序更新。
最优标准解
from typing import List
class Solution:
def canPartition(self, nums: List[int]) -> bool:
total=sum(nums)
if total%2:
return False # 当前情况不符合题意
target=total//2
dp=[False]*(target+1) # 创建 DP 表,保存子问题答案
dp[0]=True # 创建 DP 表,保存子问题答案
for x in nums:
for j in range(target,x-1,-1):
dp[j]=dp[j] or dp[j-x] # 创建 DP 表,保存子问题答案
return dp[target]
复杂度
时间 O(n·target),空间 O(target)。
易错点
0/1 背包要倒序;总和奇数直接 False。
多解补充
多解 1:集合滚动
取舍说明: 写法直观,但空间也可能较大。
from typing import List
class Solution:
def canPartition(self, nums: List[int]) -> bool:
s=sum(nums)
if s%2:
return False # 当前情况不符合题意
target=s//2
possible={0}
for x in nums:
possible |= {v+x for v in possible if v+x<=target}
return target in possible
快速复习
等和分割:0/1 背包凑半和。
56. LC 437 路径总和 III
难度:中等
标签:树、前缀和、DFS
原题简述
统计二叉树中路径和等于 targetSum 的路径数量,路径向下即可,不必从根开始。
知识点介绍
树上前缀和:当前前缀 curr,若之前有 curr-target,则中间路径和为 target。
标准思路
DFS 时维护前缀和出现次数,回溯离开节点时撤销当前前缀。
最优标准解
from typing import Optional
from collections import defaultdict
class Solution:
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> int:
count=defaultdict(int)
count[0]=1
ans=0
def dfs(node, cur):
nonlocal ans
if not node: # 空节点是递归边界
return # 结束当前递归分支
cur += node.val
ans += count[cur-targetSum]
count[cur] += 1
dfs(node.left, cur)
dfs(node.right, cur)
count[cur] -= 1
dfs(root,0)
return ans # 返回最终答案
复杂度
时间 O(n),空间 O(h) 到 O(n)。
易错点
回溯时必须 count[cur]-=1;路径只向下。
多解补充
多解 1:从每个节点向下 DFS
取舍说明: 直观但最坏 O(n²)。
from typing import Optional
class Solution:
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> int:
def from_node(node, remain):
if not node: # 空节点是递归边界
return 0
return (node.val==remain)+from_node(node.left,remain-node.val)+from_node(node.right,remain-node.val)
if not root:
return 0
return from_node(root,targetSum)+self.pathSum(root.left,targetSum)+self.pathSum(root.right,targetSum)
快速复习
路径总和 III:树上前缀和。
57. LC 438 找到字符串中所有字母异位词
难度:中等
标签:滑动窗口、哈希表
原题简述
返回 s 中所有 p 的异位词起始下标。
知识点介绍
固定长度窗口,比较窗口字符计数和 p 的计数。
标准思路
用 need/window 计数,窗口长度超过 p 时移出左端。
最优标准解
from typing import List
from collections import Counter
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
need=Counter(p)
window=Counter()
ans=[]
m=len(p)
for i,ch in enumerate(s):
window[ch]+=1
if i>=m:
left=s[i-m]
window[left]-=1
if window[left]==0:
del window[left]
if window==need:
ans.append(i-m+1)
return ans # 返回最终答案
复杂度
时间 O(n·字符集比较),空间 O(字符集)。小写字母可视为 O(n)。
易错点
窗口长度固定为 len(p)。
多解补充
多解 1:数组计数
取舍说明: 只含小写字母时更快。
from typing import List
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
if len(p)>len(s):
return []
need=[0]*26
win=[0]*26
for ch in p: need[ord(ch)-97]+=1
ans=[]
m=len(p)
for i,ch in enumerate(s):
win[ord(ch)-97]+=1
if i>=m:
win[ord(s[i-m])-97]-=1
if win==need:
ans.append(i-m+1)
return ans # 返回最终答案
快速复习
异位词索引:固定窗口计数。
58. LC 494 目标和
难度:中等
标签:动态规划、背包
原题简述
给每个数加 + 或 -,求表达式结果为 target 的方案数。
知识点介绍
设正数和为 P,总和 S,则 P-(S-P)=target,转成子集和 P=(S+target)/2。
标准思路
用 0/1 背包计数 dp[j] 表示凑出 j 的方案数。
最优标准解
from typing import List
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
s=sum(nums)
if abs(target)>s or (s+target)%2:
return 0
bag=(s+target)//2
dp=[0]*(bag+1) # 创建 DP 表,保存子问题答案
dp[0]=1 # 创建 DP 表,保存子问题答案
for x in nums:
for j in range(bag,x-1,-1):
dp[j]+=dp[j-x] # 创建 DP 表,保存子问题答案
return dp[bag]
复杂度
时间 O(n·bag),空间 O(bag)。
易错点
奇偶不匹配直接 0;计数背包要累加。
多解补充
多解 1:DFS 记忆化
取舍说明: 直观但状态数可能较多。
from typing import List
from functools import lru_cache
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
@lru_cache(None)
def dfs(i,total):
if i==len(nums):
return 1 if total==target else 0
return dfs(i+1,total+nums[i])+dfs(i+1,total-nums[i])
return dfs(0,0)
快速复习
目标和:转子集和计数。
59. LC 538 把二叉搜索树转换为累加树
难度:中等
标签:树、反向中序
原题简述
将 BST 每个节点值变为原树中大于等于它的节点值之和。
知识点介绍
BST 中序升序,反向中序就是从大到小遍历。
标准思路
维护累计和 total,按右根左顺序更新节点值。
最优标准解
from typing import Optional
class Solution:
def convertBST(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
total=0
def dfs(node):
nonlocal total
if not node:
return # 结束当前递归分支
dfs(node.right)
total += node.val
node.val = total
dfs(node.left)
dfs(root)
return root # 返回处理后的根节点
复杂度
时间 O(n),空间 O(h)。
易错点
遍历顺序是右根左,不是普通中序。
多解补充
多解 1:迭代栈
取舍说明: 同样正确,避免递归。
from typing import Optional
class Solution:
def convertBST(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
total=0
stack=[]
cur=root
while cur or stack:
while cur:
stack.append(cur) # 加入当前路径、节点或中间结果
cur=cur.right
cur=stack.pop()
total+=cur.val
cur.val=total
cur=cur.left
return root # 返回处理后的根节点
快速复习
累加树:反向中序累计。
60. LC 560 和为 K 的子数组
难度:中等
标签:前缀和、哈希表
原题简述
统计连续子数组和等于 k 的数量。
知识点介绍
若当前前缀和为 cur,之前出现过 cur-k,则中间子数组和为 k。
标准思路
遍历时边统计边更新前缀和次数。
最优标准解
from typing import List
from collections import defaultdict
class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
count=defaultdict(int)
count[0]=1
cur=ans=0
for x in nums:
cur += x
ans += count[cur-k]
count[cur] += 1
return ans # 返回最终答案
复杂度
时间 O(n),空间 O(n)。
易错点
不能用滑动窗口,因为有负数;count[0]=1。
多解补充
多解 1:暴力前缀枚举
取舍说明: 能做但 O(n²)。
from typing import List
class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
ans=0
for i in range(len(nums)):
s=0
for j in range(i,len(nums)):
s+=nums[j]
if s==k:
ans+=1
return ans # 返回最终答案
快速复习
子数组和 K:前缀和 + 哈希。
61. LC 581 最短无序连续子数组
难度:中等
标签:数组、排序、双指针
原题简述
找到最短连续子数组,排序它后整个数组有序。
知识点介绍
从左到右找右边界:只要当前数小于历史最大值,说明它应在无序段内。反向找左边界。
标准思路
维护前缀最大和后缀最小。
最优标准解
from typing import List
class Solution:
def findUnsortedSubarray(self, nums: List[int]) -> int:
n=len(nums)
right=-1
max_seen=float('-inf')
for i,x in enumerate(nums):
if x<max_seen:
right=i
else:
max_seen=x
left=n
min_seen=float('inf')
for i in range(n-1,-1,-1):
if nums[i]>min_seen:
left=i
else:
min_seen=nums[i]
return 0 if right==-1 else right-left+1
复杂度
时间 O(n),空间 O(1)。
易错点
已经有序时返回 0。
多解补充
多解 1:排序比较
取舍说明: 简单但 O(n log n)。
from typing import List
class Solution:
def findUnsortedSubarray(self, nums: List[int]) -> int:
sorted_nums=sorted(nums)
l=0
while l<len(nums) and nums[l]==sorted_nums[l]:
l+=1
r=len(nums)-1
while r>=0 and nums[r]==sorted_nums[r]:
r-=1
return 0 if l>=r else r-l+1
快速复习
最短无序段:正扫定右界,反扫定左界。
62. LC 621 任务调度器
难度:中等
标签:贪心、计数
原题简述
相同任务之间至少间隔 n,求完成所有任务最短时间。
知识点介绍
最频繁任务决定框架长度。
标准思路
若最大频率为 maxf,有 cnt 个任务达到 maxf,最短为 max((maxf-1)*(n+1)+cnt, 总任务数)。
最优标准解
from typing import List
from collections import Counter
class Solution:
def leastInterval(self, tasks: List[str], n: int) -> int:
freq=Counter(tasks).values()
maxf=max(freq)
cnt=sum(1 for f in freq if f==maxf)
return max(len(tasks), (maxf-1)*(n+1)+cnt)
复杂度
时间 O(T),空间 O(字符集)。
易错点
最后一组不需要完整冷却;要和任务总数取 max。
多解补充
多解 1:堆模拟
取舍说明: 通用但复杂度更高。
from typing import List
from collections import Counter, deque
import heapq
class Solution:
def leastInterval(self, tasks: List[str], n: int) -> int:
heap=[-c for c in Counter(tasks).values()]
heapq.heapify(heap)
wait=deque()
time=0
while heap or wait:
time+=1
if heap:
c=heapq.heappop(heap)+1
if c:
wait.append((time+n,c)) # 加入当前路径、节点或中间结果
if wait and wait[0][0]==time:
heapq.heappush(heap, wait.popleft()[1])
return time
快速复习
任务调度:最高频任务搭框架。
63. LC 647 回文子串
难度:中等
标签:字符串、中心扩展、DP
原题简述
统计字符串中所有回文子串数量。
知识点介绍
每个回文都有中心,奇数和偶数中心都要统计。
标准思路
对每个中心向两边扩,扩成功一次就计数加一。
最优标准解
class Solution:
def countSubstrings(self, s: str) -> int:
ans=0
def expand(l,r):
nonlocal ans
while l>=0 and r<len(s) and s[l]==s[r]:
ans+=1
l-=1
r+=1
for i in range(len(s)):
expand(i,i)
expand(i,i+1)
return ans # 返回最终答案
复杂度
时间 O(n²),空间 O(1)。
易错点
偶数中心不能漏。
多解补充
多解 1:DP
取舍说明: 可做但空间 O(n²)。
class Solution:
def countSubstrings(self, s: str) -> int:
n=len(s)
dp=[[False]*n for _ in range(n)]
ans=0
for length in range(1,n+1):
for i in range(n-length+1):
j=i+length-1
if s[i]==s[j] and (length<=2 or dp[i+1][j-1]):
dp[i][j]=True
ans+=1
return ans # 返回最终答案
快速复习
回文子串:中心扩展计数。
64. LC 739 每日温度
难度:中等
标签:单调栈
原题简述
对每天温度,返回还要等几天才有更高温度。
知识点介绍
单调栈存还没找到更高温的下标,栈内温度递减。
标准思路
当前温度比栈顶高时,栈顶那天的答案就是当前下标差。
最优标准解
from typing import List
class Solution:
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
ans=[0]*len(temperatures)
stack=[] # 栈保存还没结算的下标或状态
for i,t in enumerate(temperatures):
while stack and t>temperatures[stack[-1]]: # 单调栈保存还没找到更高温度的日期下标
j=stack.pop() # 栈保存还没结算的下标或状态
ans[j]=i-j
stack.append(i) # 加入当前路径、节点或中间结果
return ans # 返回最终答案
复杂度
时间 O(n),空间 O(n)。
易错点
栈里存下标,不是温度;条件是当前温度更高。
多解补充
多解 1:暴力枚举
取舍说明: 能做但 O(n²)。
from typing import List
class Solution:
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
ans=[0]*len(temperatures)
for i in range(len(temperatures)):
for j in range(i+1,len(temperatures)):
if temperatures[j]>temperatures[i]:
ans[i]=j-i
break
return ans # 返回最终答案
快速复习
每日温度:单调递减栈。