454. 四数相加 II
- 思路
- example
- 四个数组相同长度
- 第一步:brute-force:
- hash: dict
- record[num] = freq
第二步:
第三步:(nums1[i]+nums2[j]) + (nums3[k]+nums4[l]),
- 复杂度. 时间:, 空间:
class Solution:
def fourSumCount(self, nums1: List[int], nums2: List[int], nums3: List[int], nums4: List[int]) -> int:
record = collections.defaultdict(int)
for num1 in nums1:
for num2 in nums2:
record[num1+num2] += 1
res = 0
for num3 in nums3:
for num4 in nums4:
if record[-(num3+num4)] > 0:
res += record[-(num3+num4)] # !!!
return res
151. 反转字符串中的单词
- 思路
- example
- 内置函数
- 复杂度. 时间:O(n), 空间: O(n)
class Solution:
def reverseWords(self, s: str) -> str:
return ' '.join(reversed(s.split()))
- 反转两次 (局部,整体)
- step 1: trim spaces
- " abc def " --> “abc def"
- step 2: reverse each word in the string
- "cba fed"
- step 3: reverse the whole string
- “def abc"
- 细节比较多
class Solution:
def reverseWords(self, s: str) -> str:
def trim(s):
n = len(s)
left = 0
while left < n and s[left] == ' ':
left += 1
right = n-1
while left <= right and s[right] == ' ':
right -= 1
s1 = []
while left <= right:
if s[left] != ' ' or s1[-1] != ' ': # !!!
s1.append(s[left])
left += 1
return s1
def reverse_word(s1):
n = len(s1)
left, right = 0, 0
while right < n:
ch = s1[right]
if ch == ' ':
reverse(s1, left, right-1)
left = right + 1
elif right == n-1: # !!!
reverse(s1, left, right)
right += 1
def reverse(s1, left, right):
while left < right:
s1[left], s1[right] = s1[right], s1[left]
left += 1
right -= 1
# step 1, trim spaces
s1 = trim(s) # output is a list
# step 2
reverse_word(s1) # reverse each word
# step 3
reverse(s1, 0, len(s1)-1) # reverse the whole list
return ''.join(s1)
28. 找出字符串中第一个匹配项的下标
class Solution:
def strStr(self, haystack: str, needle: str) -> int:
def compute_next(s):
n = len(s)
nxt = [0 for _ in range(n)]
i = 1
j = 0
while i < n:
if s[i] == s[j]:
nxt[i] = j + 1
i += 1
j += 1
else:
if j > 0:
j = nxt[j-1]
else:
nxt[i] = 0
i += 1
return nxt
m, n = len(haystack), len(needle)
nxt = compute_next(needle)
i, j = 0, 0
while i < m:
if haystack[i] == needle[j]:
i += 1
j += 1
else:
if j > 0:
j = nxt[j-1]
else:
i += 1
j = 0
if j == n:
return i - n
return -1
20. 有效的括号
- 思路
- example
- '(([]{}))'
- '(([]})'
- '(('
- 用list实现stack (FILO), 用dict记录配对。
- 最后return的时候注意判断stack是否非空。
- example
- 复杂度. 时间:O(n), 空间: O(n)
class Solution:
def isValid(self, s: str) -> bool:
pairs = {'(' : ')', '[': ']', '{': '}'}
stack = []
for ch in s:
if ch in pairs:
stack.append(ch)
else:
if stack == []: # !!!
return False
else:
ch0 = stack.pop()
if pairs[ch0] != ch: # !!!
return False
if stack: # !!!
return False
return True
239. 滑动窗口最大值
- 思路
- example
- 手动实现单调队列 class
- que = deque(): 左边出,右边进或出
- que 维护单调下降数列
- 新加元素大于前面元素,可以把前面元素pop(),因为前面元素不可能成为当前或者下面窗口的最大值。
- 新加元素小前面元素,append新元素即可。
- que 维护单调下降数列
- size(que) = k,滑动窗口size = k
- 每次需要 (先出后进)
- 移除最左边元素
- 加进当前位置元素
- 取que[0]得到当前窗口最大值
- que = deque(): 左边出,右边进或出
- 复杂度. 时间:O(n), 空间: O(k)
单调队列:先进先出 + 维护最大(小)值
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
n = len(nums)
que = collections.deque()
res = []
for i in range(n):
num = nums[i]
while que and num >= nums[que[-1]]: # !!!
que.pop()
que.append(i)
if i - que[0] + 1 > k: # !!!
que.popleft()
if i >= k-1:
res.append(nums[que[0]])
return res
4. 寻找两个正序数组的中位数
- 第k小 (k >= 1), 二分查找
m+n 奇数:k = (m+n)//2 + 1
m+n 偶数:k = (m+n)//2, (m+n)//2+1 --> average
nums1[index1,...,index1+k//2-2], nums2[index2,...,index2+k//2-2] 假设没有越界情况
pivot1 = nums1[index1+k//2-1], pivot2 = nums2[index2+k//2-1]
Case 1: pivot1 <= pivot2, pivot1 不可能是第k小 (最多第k-1小),index1 = min(index1 + k//2, m-1) , 更新k继续搜索直到k=1
Case 2: pivot1 > pivot2, pivot2 不可能是第k小 (最多第k-1小),index2 = min(index2 + k//2, n-1), 更新k继续搜索直到k=1
class Solution:
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
def getKth(k):
left1, left2 = 0, 0 # !!!
while k >= 1:
if left1 == m: # !!!
return nums2[left2+k-1]
if left2 == n: # !!!
return nums1[left1+k-1]
if k == 1: # !!!
return min(nums1[left1], nums2[left2])
right1 = min(left1+k//2-1, m-1) # valid index in nums1
right2 = min(left2+k//2-1, n-1) # valid index in nums2
# 有越界的时候,并不是找第k小。比如可能是找第k-3小,但不影响算法,因为每次并不是k//2的递减!!!
pivot1, pivot2 = nums1[right1], nums2[right2]
if pivot1 <= pivot2:
k -= right1 - left1 + 1
left1 = right1 + 1
else:
k -= right2 - left2 + 1
left2 = right2 + 1
m, n = len(nums1), len(nums2)
if (m+n) % 2 == 1:
return getKth((m+n)//2+1)
else:
return (getKth((m+n)//2) + getKth((m+n)//2+1)) / 2
114. Flatten Binary Tree to Linked List
- 思路
- example
-
分解子问题 (后序遍历)
class Solution:
def flatten(self, root: Optional[TreeNode]) -> None:
"""
Do not return anything, modify root in-place instead.
"""
if root == None:
return
self.flatten(root.left)
self.flatten(root.right)
old_left = root.left
old_right = root.right
root.left = None
root.right = old_left
while root.right:
root = root.right
root.right = old_right
98. 验证二叉搜索树
有效 二叉搜索树定义如下:
- 节点的左子树只包含 小于 当前节点的数。
- 节点的右子树只包含 大于 当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
- 思路
- example
- 递归
- 注意搜索树的定义
- 验证左右子树是否搜索树
-
只比较root.val,root.left.val, root.right.val是不够的。下面的递归前序遍历是错误的。还需要验证根节点值小于右子树最小值,大于左子树最大值。
- 上图中,就算右子树改为[6,3,7]也是False.
- 复杂度. 时间:O(n), 空间: O(n)
- 递归,中序遍历。BST利用中序遍历就是自然的递增顺序!
- 中序遍历中,如果知道当前节点的pre节点,并且是递增顺序,那么符合要求。
- 最简单的方法,中序遍历得到一个数组,再判断该数组是不是严格递增顺序。
class Solution:
def isValidBST(self, root: Optional[TreeNode]) -> bool:
def traversal(root):
nonlocal pre
if root == None:
return True
if traversal(root.left) == False:
return False
if pre and pre.val >= root.val:
return False
pre = root
if traversal(root.right) == False:
return False
return True
pre = None
return traversal(root)
501. 二叉搜索树中的众数
- 思路
- example
- 含重复值
- 有序数组累计频率
- 递归,中序,维护pre节点, 双指针
- pre节点
- max_freq
- cur_freq
- res = [] 保存结果
- 复杂度. 时间:O(n), 空间: O(1) (如果递归栈不算在内)
class Solution:
def findMode(self, root: Optional[TreeNode]) -> List[int]:
def traversal(root):
nonlocal pre, cnt, cnt_max, res # res!!!
if root == None:
return
traversal(root.left)
if pre:
if root.val == pre.val:
cnt += 1
else:
cnt = 1
else:
cnt = 1
if cnt > cnt_max:
cnt_max = cnt
res = [root.val]
elif cnt == cnt_max:
res.append(root.val)
pre = root
traversal(root.right)
cnt, cnt_max = -1, -float('inf')
pre = None
res = []
traversal(root)
return res
236. 二叉树的最近公共祖先
- 思路
- example
- 所有 Node.val 互不相同.
- p and q will exist in the tree.
- 递归,后序,自下而上,回溯
返回值:p, q, None, 或者LCA(p,q)
- 遍历整棵树
- 不直观, 注意Base Case的逻辑。
- 前提:树中必有p,q节点,并且树中节点数值各不相同!!!
- def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
-
函数意义:
- 树中含有p,q时,返回最近公共祖先
- 树中只含p或q的一个时,返回相应的节点
- 树中不含p和q时,返回None
- 后序:结合左子树与右子树遍历结果
- 如果左子树含有p,q。那么左子树返回结果即为答案。如果右子树含有p,q. 那到右子树返回结果即为答案。
- 如果左子树只含q,q中的一个,返回左子树结果。如果右子树只含q,q中的一个,返回右子树结果。
-
函数意义:
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
if root == None:
return None
if root == p or root == q:
return root
left = self.lowestCommonAncestor(root.left, p, q)
right = self.lowestCommonAncestor(root.right, p, q)
if left and right:
return root
if left == None:
return right
if right == None:
return left
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
def traversal(root):
if root == None:
return None
if root == p or root == q:
return root
left = traversal(root.left)
right = traversal(root.right)
if left == None:
return right
if right == None:
return left
return root
return traversal(root)
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
if root == None:
return None
left = self.lowestCommonAncestor(root.left, p, q)
right = self.lowestCommonAncestor(root.right, p, q)
if root.val == p.val or root.val == q.val:
return root
if left and right == None:
return left
if left == None and right:
return right
if left == None and right == None:
return None
if left and right:
return root
77. 组合
- 思路
- example
- 回溯,去重
- 宽度:n
-
深度:k
- 复杂度. 时间:, 空间:
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
def backtrack(start):
if len(path) == k:
res.append(path[:])
return
for i in range(start+1, n+1):
path.append(i)
backtrack(i)
path.pop()
res = []
path = []
backtrack(0)
return res
- 优化:剪枝
-
横向遍历的时候去掉不可能成功(个数不足)的情况。
-
class Solution:
def combine(self, n: int, k: int) -> List[List[int]]:
def backtrack(start):
if len(path) == k:
res.append(path[:])
return
for i in range(start, n-(k-len(path))+2):
path.append(i)
backtrack(i+1)
path.pop()
res, path = [], []
backtrack(1)
return res
93. 复原 IP 地址
class Solution:
def restoreIpAddresses(self, s: str) -> List[str]:
def backtrack(start_idx):
if start_idx == len(s):
if len(path) == 4:
res.append('.'.join(path))
return
for i in range(start_idx, min(start_idx+3, len(s))):
if s[start_idx] == '0' and i > start_idx: # !!!
break
if int(s[start_idx:i+1]) <= 255:
if len(path) == 4:
break
path.append(s[start_idx:i+1])
backtrack(i+1)
path.pop()
res, path = [], []
backtrack(0)
return res
698. 划分为k个相等的子集
class Solution:
def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:
cache = dict()
def dfs(state, cur_sum):
if (state, cur_sum) in cache:
return cache[(state, cur_sum)]
if state == (1<<len(nums))-1:
return True
for j in range(len(nums)):
if state & (1<<j) == 0:
if cur_sum + nums[j] > target:
break
if dfs(state+(1<<j), (cur_sum+nums[j]) % target):
cache[(state, cur_sum)] = True
return True
cache[(state, cur_sum)] = False
return False
total = sum(nums)
if total % k != 0:
return False
target = total // k
nums.sort()
if nums[-1] > target:
return False
return dfs(0, 0)
491. Non-decreasing Subsequences
class Solution:
def findSubsequences(self, nums: List[int]) -> List[List[int]]:
def backtrack(start_idx):
if len(path) >= 2:
res.append(path[:])
if start_idx == len(nums):
return
used = set()
for i in range(start_idx, len(nums)):
if nums[i] in used:
continue
if path and nums[i] < path[-1]:
continue
path.append(nums[i])
backtrack(i+1)
path.pop()
used.add(nums[i]) # !!!
res, path = [], []
backtrack(0)
return res
332. 重新安排行程
class Solution:
def findItinerary(self, tickets: List[List[str]]) -> List[str]:
def backtrack(start_pos):
if len(path) == n+1:
return True
for _ in range(len(record[start_pos])): # !!!
end_pos = record[start_pos].pop(0)
path.append(end_pos)
if backtrack(end_pos):
return True
path.pop()
record[start_pos].append(end_pos)
return False
record = collections.defaultdict(list)
n = len(tickets)
for i in range(n):
record[tickets[i][0]].append(tickets[i][1])
for key, val in record.items():
record[key].sort()
path = ['JFK']
backtrack('JFK')
return path
53. 最大子数组和
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
n = len(nums)
sum_ = nums[0]
res = nums[0]
for i in range(1, n):
if sum_ < 0:
sum_ = nums[i]
else:
sum_ += nums[i]
res = max(res, sum_)
return res
1005. K 次取反后最大化的数组和
class Solution:
def largestSumAfterKNegations(self, nums: List[int], k: int) -> int:
nums.sort() # !!!
n = len(nums)
cnt = 0
for i in range(n):
if nums[i] < 0:
cnt += 1
for i in range(min(cnt, k)):
nums[i] = -nums[i]
k -= min(cnt, k)
if k % 2 == 0:
return sum(nums)
else:
return sum(nums) - 2*min(nums)
class Solution:
def largestSumAfterKNegations(self, nums: List[int], k: int) -> int:
sum_ = sum(nums)
freq = collections.defaultdict(int)
for num in nums:
freq[num] += 1
for num in range(-100, 0):
if freq[num] == 0:
continue
times = min(k, freq[num])
sum_ -= 2*num*times
freq[num] -= times
freq[-num] += times
k -= times
if k == 0:
return sum_
for num in range(0, 101):
if freq[num] == 0:
continue
if k % 2 == 0:
return sum_
else:
return sum_ - 2*num
406. 根据身高重建队列
class Solution:
def reconstructQueue(self, people: List[List[int]]) -> List[List[int]]:
people.sort(key=lambda x: (-x[0], x[1]))
res = []
for i in range(len(people)):
res.insert(people[i][1], people[i])
return res
452. 用最少数量的箭引爆气球
class Solution:
def findMinArrowShots(self, points: List[List[int]]) -> int:
n = len(points)
points.sort(key=lambda x: x[0]) # !!!
res = 1
right = points[0][1]
for i in range(1, n):
if points[i][0] > right:
res += 1
right = points[i][1]
else: # !!!
right = min(right, points[i][1])
return res
968. 监控二叉树
# - 0:无覆盖, 无摄像头 (没有**被childern覆盖**)
# - 1:有覆盖, 有摄像头
# - 2:有覆盖,无摄像头 (被其中一个child覆盖)
class Solution:
def minCameraCover(self, root: Optional[TreeNode]) -> int:
def traversal(root):
nonlocal res
if root == None:
return 2
left = traversal(root.left)
right = traversal(root.right)
if left == 0 or right == 0:
res += 1
return 1
if left == 1 or right == 1:
return 2
if left == 2 and right == 2:
return 0
res = 0
if traversal(root) == 0:
res += 1
return res
494. 目标和
- 思路
- example
- 返回不同 表达式 的数目。
- 暴力回溯
- 数字分为两组:positive (+), negative (-)
positive - negative = target
positive + negative = sum_
positive = (sum_ + target) // 2 (if sum+ target is even)
- 选出(+)子集使得和为positive (integer)
- 0 <= nums[i] <= 1000, -1000 <= target <= 1000
- 0-1背包
- 背包容量:j
- 物品:每个数字最多选一次
- DP (1D): dp[j]: 使得(+)数组和为j的组合数。
- dp[0] = 1
- 目标:dp[positive]
- 复杂度. 时间:O(n*sum_), 空间: O(sum_)
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
n = len(nums)
sum_ = sum(nums)
if (target + sum_) % 2 != 0:
return 0
else:
positive = (target + sum_) // 2
if positive < 0:
return 0
dp = [0 for _ in range(positive+1)]
dp[0] = 1
for i in range(n):
for j in range(positive, -1, -1):
if j >= nums[i]:
dp[j] += dp[j-nums[i]] # 选与不选两种情况累加
return dp[positive]
- 2D, 前i个
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
n = len(nums)
total = sum(nums)
if (total + target) % 2 != 0:
return 0
pos = (total + target) // 2
if pos < 0: #!!!
return 0
dp = [[0 for _ in range(pos+1)] for _ in range(n+1)]
dp[0][0] = 1
for i in range(1, n+1):
for j in range(pos+1):
if j < nums[i-1]:
dp[i][j] = dp[i-1][j]
else:
dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]]
return dp[n][pos]
322. Coin Change
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
n = len(coins)
dp = [[amount+1 for _ in range(amount+1)] for _ in range(n+1)] # !!!
dp[0][0] = 0
for i in range(1, n+1):
coin = coins[i-1]
for j in range(amount+1):
if j < coin:
dp[i][j] = dp[i-1][j]
else:
dp[i][j] = min(dp[i-1][j], dp[i][j-coin] + 1) # !!!
return dp[n][amount] if dp[n][amount] != amount+1 else -1
518. 零钱兑换 II
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
n = len(coins)
dp = [[0 for _ in range(amount+1)] for _ in range(n+1)]
dp[0][0] = 1
for i in range(1, n+1):
coin = coins[i-1]
for j in range(amount+1):
if j < coin:
dp[i][j] = dp[i-1][j]
else:
dp[i][j] = dp[i-1][j] + dp[i][j-coin]
return dp[n][amount]
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
dp = [0 for _ in range(amount+1)]
dp[0] = 1 # !!!
for coin in coins:
for j in range(coin, amount+1):
dp[j] += dp[j-coin]
return dp[amount]
377. 组合总和 Ⅳ
- 爬楼梯框架
class Solution:
def combinationSum4(self, nums: List[int], target: int) -> int:
dp = [0 for _ in range(target+1)]
dp[0] = 1 # !!!
for j in range(target+1):
for num in nums:
if j >= num:
dp[j] += dp[j-num]
return dp[target]
139. 单词拆分
- 思路
- example
- 暴力回溯,memo dfs
- 分割字符串问题
-
完全背包 + 排列 (有先后顺序)
- 物品:wordDict
- 遍历顺序:先背包 再物品
- 注意index j的意义及取值范围
- 复杂度. 时间:O(?), 空间: O(?)
- 为了处理空字符串,用前j个逻辑。
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
n = len(s)
m = len(wordDict)
dp = [False for _ in range(n+1)]
dp[0] = True
for j in range(1, n+1):
for i in range(m):
length = len(wordDict[i])
if j >= length:
dp[j] = dp[j] or (dp[j-length] and s[j-length:j] == wordDict[i])
if dp[j]:
break
return dp[n]