HOT100 复习笔记:题干 + 思路 + 代码
HOT100 从 23 年开始零零散散做过很多遍。
但其中 trick 与脑筋急转弯比较多,还是很容易忘。
因此特意花了一周的时间重新全部做了一边,用了同一的思路和代码风格方便复习。
此外,还有些面试常见题也会写在后面。
和 ‘算法题 - 分类索引’ 有一定的重复。
HOT 100 的分类其实有点问题,暂时先按顺序写了,比如很多题的知识点和类别就关系不大。
此文章全部用 python,我也建议面试全部使用 python,原因如下:
- 现在很多面试会搓 torch,直接用 python 写代码题避免切换麻烦
- python 功能更强大且写的快,比如切片和 @cache 等
- 面试一般不要求大样本,python 的速度劣势体现不出来
- python 语法不严格,避免了面试看到编译错误紧张
哈希 #
题干:给一个数组,找的任意两数之和等于目标值
思路:一轮遍历,用哈希记录当前遍历过哪些数,然后当前元素查哈希表即可
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
m = defaultdict(lambda : -1) # 哈希表缺省值
n = len(nums)
for i in range(n):
if m[target - nums[i]] != -1:
return [i, m[target - nums[i]]]
m[nums[i]] = i
return []
题干:有字符串数组,对其中每个字符串按其中出现的元素分类
输入: strs = [“eat”, “tea”, “tan”, “ate”, “nat”, “bat”]
输出: [[“bat”],[“nat”,“tan”],[“ate”,“eat”,“tea”]]
思路:对每个字符串排序后,哈希 key 为排序后的值
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
n = len(strs)
m = defaultdict(list)
for i in range(n):
tmp_s = ''.join(sorted(strs[i])) # 注意下 str 的 join 用法
m[tmp_s].append(strs[i])
ans = []
for i, v in m.items():
ans.append(v)
return ans
题干:给一个数组,找的其中所有元素排序后最长的连续序列
输入:nums = [100,4,200,1,3,2] 输出:4 解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。
思路:不能使用排序。用桶重构数组,遍历每个连续序列的首值,记录每个序列的长度。最终使数组中每个元素最多遍历一边。
# 桶 + 技巧
class Solution:
def longestConsecutive(self, nums: List[int]) -> int:
m = set(nums)
ans = 0
for x in m: # !注意此处是在 set 中遍历,在 nums 中遍历最差是 On2
if x - 1 in m:
continue
xx = x
length = 0
while xx in m:
length += 1
xx += 1
ans = max(ans, length)
return ans
双指针 #
题干:不改变数组顺序去情况下,将零全部移动到末尾,要求操作次数最少
输入: nums = [0,1,0,3,12] 输出: [1,3,12,0,0]
思路:将一个指针视作栈顶,即目前有多少非零的数,栈顶一定指向零,当遍历到非零元素交换。
注意:和 荷兰国旗问题比较接近,但此题要求有序,因此解法不同。
class Solution:
def moveZeroes(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
n = len(nums)
stack_size = 0
for i in range(n):
if nums[i] != 0:
nums[i], nums[stack_size] = nums[stack_size], nums[i]
stack_size += 1
题干:找数组两个“隔板”之间能容纳最多的水,注意不同与接雨水,不用考虑“隔板”本身的体积。
输入:[1,8,6,2,5,4,8,3,7] 输出:49
思路:双指针从两边向中间走,哪边数小就缩哪边
class Solution:
def maxArea(self, height: List[int]) -> int:
n = len(height)
l = 0
r = n - 1
ans = 0
while l < r:
ans = max(ans, (r - l) * min(height[l], height[r]))
if height[l] < height[r]:
l += 1
else:
r -= 1
return ans
题干:记录数组中所有 x + y + z == 0 的值,需要去重。
思路:排序后三指针,遍历 x 作为基数,由于 y 和 z 一定有序,还是从两边缩小到中间的思路。此外可以遍历过程中去重,也可以用 tuple 做 hash 的 key,后者思维量小。
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
n = len(nums)
nums.sort()
ans = set()
for i, x in enumerate(nums):
l = i + 1
r = n - 1
while l < r:
if nums[l] + nums[r] == -x:
ans.add((nums[i], nums[l], nums[r]))
# break
# 注意下此处不能直接 break,因为同一个 i,可以对应不同的 l r 做答案
l += 1
r -= 1
elif nums[l] + nums[r] < -x:
l += 1
else:
r -= 1
res = []
for x in ans:
res.append([x[0], x[1], x[2]])
return res
下面是循环中去重的代码,注意是在 i 循环 和 当满足条件的时候去跳过重复的,这样更好写
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
nums.sort()
ans = []
n = len(nums)
for i in range(n):
if i != 0 and nums[i] == nums[i - 1]:
continue
j = i + 1
k = n - 1
x = nums[i]
while j < k:
# print(i, j, k)
if x + nums[j] + nums[k] == 0:
ans.append([x, nums[j], nums[k]])
j += 1
k -= 1
while 0 < j < k and nums[j] == nums[j - 1]:
j += 1
while j < k < n - 1 and nums[k] == nums[k + 1]:
k -= 1
if x + nums[j] + nums[k] < 0:
j += 1
if x + nums[j] + nums[k] > 0:
k -= 1
return ans
题干:过于经典,不赘述了
思路:
- 前缀后缀法:通法,思维量较小。统计每个数的最大前后缀
- 双指针实际上就说前缀后缀的优化版,核心就是哪侧前或后缀更小,就缩那测
- 单调递减栈,找上一个更大的,然后填坑
# 前缀后缀
class Solution:
def trap(self, height: List[int]) -> int:
n = len(height)
pre = [0] * n
pre[0] = height[0]
suf = [0] * n
suf[-1] = height[-1]
for i in range(1, n):
pre[i] = max(pre[i - 1], height[i])
for i in range(n - 2, -1, -1):
suf[i] = max(suf[i + 1], height[i])
ans = 0
for i in range(n):
ans += (min(pre[i], suf[i]) - height[i])
return ans
# 双指针,谁小谁移动
class Solution:
def trap(self, height: List[int]) -> int:
n = len(height)
l = 0
r = n - 1
pre_max = height[l]
suf_max = height[r]
ans = 0
while l < r:
if pre_max < suf_max:
ans += (pre_max - height[l])
l += 1
else:
ans += (suf_max - height[r])
r -= 1
pre_max = max(pre_max, height[l])
suf_max = max(suf_max, height[r])
return ans
# 单调栈做法
class Solution:
def trap(self, height: List[int]) -> int:
stack = []
n = len(height)
ans = 0
for i, x in enumerate(height):
while stack and x >= height[stack[-1]]:
bottom = height[stack[-1]]
stack.pop()
if not stack:
break
l = stack[-1]
h = min(height[l], x) - bottom
ans += h * (i - l - 1)
stack.append(i)
return ans
滑动窗口 #
题干:找到数组中无重复字符的最长子串
思路:滑动窗口 + 哈希的模板题
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
n = len(s)
m = defaultdict(int)
l = r = ans = 0
while l <= r and r < n:
m[s[r]] += 1
# print(m)
while l <= r and m[s[r]] != 1:
# print(l, r, m)
m[s[l]] -= 1
l += 1
ans = max(ans, r - l + 1)
r += 1
return ans
题干:找到数组中所有 “目标字符 cnt ”等于”字串字符 cnt“ 的字串下标
思路:稍微变形的滑窗板子,注意当缩减左窗口后,字串长度与目标相同时,一定两者 cnt 相同。
# 滑动窗口板子
class Solution:
def findAnagrams(self, s: str, p: str) -> List[int]:
n = len(s)
l = r = 0
t = defaultdict(int)
for i in list(p):
t[i] += 1
m = defaultdict(int)
ans = []
while l <= r and r < n:
m[s[r]] += 1
while l <= r and m[s[r]] > t[s[r]]: # 注意下缩左窗口的条件
m[s[l]] -= 1
l += 1
# print(r, l, m)
if r - l + 1 == len(p):
ans.append(l)
r += 1
return ans
字串 #
题干:找子串和为 K 的数量
思路:O1 求区间和 -> 需要前缀和 -> 哈希记录出现的指定前缀数量 -> 和两数之和思路一样
# 求区间和 -> 前缀和 -> 哈希记录出现的指定前缀数量
class Solution:
def subarraySum(self, nums: List[int], k: int) -> int:
n = len(nums)
prefix = [0] * (n + 1)
m = defaultdict(int)
m[0] = 1
ans = 0
# prefix[x] - prefix[y] == k
# 即找 m[prefix[i] - k] 的数量
for i in range(1, n + 1):
prefix[i] = prefix[i - 1] + nums[i - 1]
ans += m[prefix[i] - k]
m[prefix[i]] += 1
return ans
题干:找所有定长字串的最大值,要求 On
思路:经典单调队列,仅字串中保持递减的一个队列,右侧插入与单调栈一致。当左侧和首元素下标相同时,队列进行 O1 移除。从而使每个队列的首元素一定是当前字串的最大值。
单调队列相比单调栈出现的更少,注意其基本都是在栈中存下标,然后注意栈中的严格单调性。
此外,此题还有个 ST 表 做法,但复杂度不比单调队列好。
# 单调队列
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
arr = deque()
n = len(nums)
r = 0
ans = []
while r < n:
# print(r, arr)
if r >= k:
ans.append(nums[arr[0]])
if arr[0] == r - k: # 维护左下标
arr.popleft()
while arr and nums[r] >= nums[arr[-1]]: # 入队列操作和单调栈一样
arr.pop()
arr.append(r)
r += 1
ans.append(nums[arr[0]])
return ans
题干:找到所有字串中,“目标串元素 cnt “ 能覆盖的”长度最小字串元素 cnt “是什么?
输入:s = “ADOBECODEBANC”, t = “ABC” 输出:“BANC”
思路:如果只要 O(52*n) 即滑窗的过程每次遍历单词表,那就是滑窗板子,和438. 找到字符串中所有字母异位词 完全一样。
但如果想 O(n) 还是有些难度,新提出一个 less 字符标志,判断单词表中有多少词仍未被覆盖,从而代替遍历单词表。
但要注意 less 的维护方法。
class Solution:
def minWindow(self, s: str, t: str) -> str:
n = len(s)
l = r = 0
target = defaultdict(int)
for i in list(t):
target[i] += 1
m = defaultdict(int)
less = len(target) # 开始时 target 中每个词都没被满足
ans_l, ans_r = 0, inf
while l <= r and r < n:
m[s[r]] += 1
if m[s[r]] == target[s[r]]: # 注意此处一定是等于,只想让 less 更新一次
less -= 1
while l <= r and less == 0:
# print(l, r, less, ans_l, ans_r)
if (ans_r - ans_l) > (r - l): # 注意此时内层循环才满足答案吗要求
ans_l = l
ans_r = r
m[s[l]] -= 1
if m[s[l]] == target[s[l]] - 1:
less += 1 # 同样的,保证 less 只更新一次
l += 1
r += 1
return s[ans_l:ans_r + 1] if ans_r != inf else ""
普通数组 #
题干:找字串中最大的 sum 是多少?
思路:前缀和 + 空间优化(也可以简单 DP)。记录前缀和的最小值即可,简单优化下就能空间 O1。
# 找个前缀就可以
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
n = len(nums)
prefix = 0
pre_min = inf
ans = -inf
for i in range(1, n + 1):
pre_min = min(prefix, pre_min)
prefix = prefix + nums[i - 1]
ans = max(ans, prefix - pre_min)
return ans
题干:合并所有有重叠的区间
思路:对原始区间排序后,比较当前区间的右侧和下一个区间的左侧。大部分语言之间排就可以,小部分要写 lambda。
class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
intervals.sort()
ans = []
l = intervals[0][0]
r = intervals[0][1]
for x, y in intervals:
if x <= r:
r = max(y, r)
else:
ans.append([l, r])
l = x
r = y
ans.append([l, r])
return ans
题干:将数组右移动 k 位
思路:三次反转,先转最大区间,再转两个小区间
提示:如果是左移,其实就等于右移 -k 位
class Solution:
def rotate(self, nums: List[int], k: int) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
# 自带的 reverse 会产生多余空间,自己用双指针搓更合适
def reverse(l, r):
while l < r:
nums[l], nums[r] = nums[r], nums[l]
l += 1
r -= 1
n = len(nums)
k %= n
reverse(0, n - 1)
reverse(0, k - 1)
reverse(k, n - 1)
题干:获得每个数对应的除了他自己以外全部元素的乘积。
思路:明显的前后缀,题目要求只使用一个额外数组,先后缀后前缀直接得到答案就可以
# 前缀后缀积,最好用一个数组解决
class Solution:
def productExceptSelf(self, nums: List[int]) -> List[int]:
n = len(nums)
suf = [1] * n
for i in range(n - 1, 0, -1):
suf[i - 1] = suf[i] * nums[i]
# print(suf)
pre = 1
for i in range(n):
suf[i] *= pre
pre *= nums[i]
return suf
题干:找数组中没出现的最小正数,要求 O(n) 时间,O1 空间。
思路: 经典技巧题,想让每个符合条件的位置满足 nums[i] == nums[nums[i]]。
这样就相当于让位置 i 的 x 一定回到 x 的位置上,此外交换过程中要考虑 swap 依赖问题。
class Solution:
def firstMissingPositive(self, nums: List[int]) -> int:
n = len(nums)
for i in range(n): # 对齐下标
nums[i] -= 1
for i in range(n):
while 0 <= nums[i] < n and nums[i] != nums[nums[i]]:
j = nums[i] # 要让 swap 不被依赖
nums[i], nums[j] = nums[j], nums[i]
# print(nums)
for i in range(n):
if nums[i] != i:
return i + 1
return n + 1
矩阵 #
题干:如果出现零,则将该 行/列 全部置零,要求 O1 空间复杂度。
思路:用原本的第一行和第一列的来记录本行或列是否要归零,用 bool 记录第一行或列自己是否要归零。
class Solution:
def setZeroes(self, matrix: List[List[int]]) -> None:
"""
Do not return anything, modify matrix in-place instead.
"""
n = len(matrix)
m = len(matrix[0])
col_0 = False
raw_0 = False
# 用 bool 记录第一行或列自己是否要归零
for i in range(n):
if matrix[i][0] == 0:
raw_0 = True
break
for i in range(m):
if matrix[0][i] == 0:
col_0 = True
break
# 用第一行和第一列的来记录本行或列是否要归零
for i in range(n):
for j in range(m):
if matrix[i][j] == 0:
matrix[0][j] = 0
matrix[i][0] = 0
# 从第二行列开始进行实际归零
for i in range(1, n):
for j in range(1, m):
if matrix[i][0] == 0:
matrix[i][j] = 0
if matrix[0][j] == 0:
matrix[i][j] = 0
# 修改第一行列
for i in range(n):
if raw_0:
matrix[i][0] = 0
for i in range(m):
if col_0:
matrix[0][i] = 0
题干:顺时针向内旋转的遍历矩阵,输出遍历顺序。
思路:模拟题,记录每行列的起始和方向,特别是循环后要把位置挪到下一个的起始点。
# 走格子模拟,要注意些细节。注意要记录每行列的起始和方向。
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
direction = [[0, 1], [1, 0], [0, -1], [-1, 0]]
index = 0
ans = []
m = len(matrix)
n = len(matrix[0])
px = py = 0
left_m, right_m = 0, m
left_n, right_n = 0, n
while len(ans) != n * m:
# print(ans, px, py, left_m, right_m, left_n, right_n)
dx = direction[index][0]
dy = direction[index][1]
while left_m <= px < right_m and left_n <= py < right_n:
# print(ans, px, py, left_m, right_m, left_n, right_n)
ans.append(matrix[px][py])
px += dx
py += dy
px -= dx # 循环后要把位置挪到下一个的起始点
py -= dy
ne = (index + 1) % 4
px += direction[ne][0]
py += direction[ne][1]
if index == 0: # 维护边界
left_m += 1
if index == 1:
right_n -= 1
if index == 3:
left_n += 1
if index == 2:
right_m -= 1
index = (index + 1) % 4
return ans
题干:矩阵原地顺时针旋转 90°
思路:
顺时针旋转90°,等于 先沿主对角线反转 + 后左右反转。
逆时针选择90°,等于 先沿主对角线反转 + 后上下反转。
旋转 180°,等于 左右反转 + 上下反转
class Solution:
def rotate(self, matrix: List[List[int]]) -> None:
"""
Do not return anything, modify matrix in-place instead.
"""
m, n = len(matrix), len(matrix[0])
for i in range(m):
for j in range(i + 1, n):
matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
# print(matrix)
mid = n // 2
for i in range(m):
for j in range(mid):
matrix[i][j], matrix[i][n-j-1] = matrix[i][n-j-1], matrix[i][j]
题干:矩阵每行从左到右升序,每列从上到下升序,找目标元素
思路:和二分查找树一样,注意右上角是根
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
m, n = len(matrix), len(matrix[0])
x, y = 0, n - 1
while x < m and y >= 0:
if matrix[x][y] == target:
return True
elif matrix[x][y] > target:
y -= 1
elif matrix[x][y] < target:
x += 1
return False
链表 #
题干:找两条链表相交的第一个节点(相交后一直相交),注意其不一定相交。要求时间 O(m+n) 空间 O(1)。
思路:同时遍历两条链表,如果遍历到空,则从另一条头开始遍历,直到两者相遇。(和经典的 O1 空间判断环入口的思路其实一样)
原理:设两条链表的重复部分为 Z,则 链表a 为 X+Z;链表b 为 Y+Z。让两者都走 X+Y+Z,相撞一定在 Z 的入口。不相交就是 Z == 0 的情况。
class Solution:
def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> Optional[ListNode]:
p = headA
q = headB
while p != q:
p = p.next if p else headB
q = q.next if q else headA
return p
思路:分成递归和迭代,建议直接记住吧
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
pre = None
cur = head
while cur:
tmp = cur.next
cur.next = pre
pre = cur
cur = tmp
return pre
class Solution:
def reverseList(self, head: Optional[ListNode]) -> Optional[ListNode]:
if head == None or head.next == None:
return head
nxt = self.reverseList(head.next)
head.next.next = head
head.next = None
return nxt
题干:空间 O1 的判断一个链表是否是回文链表
思路:快慢指针判断中点,对后半段反转,之后双指针判断是否前后一致
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def isPalindrome(self, head: Optional[ListNode]) -> bool:
# 快慢指针找中点
fast = slow = head
while fast and fast.next:
slow = slow.next
fast = fast.next.next
def reverse(head):
cur = head
pre = None
while cur:
tmp = cur.next
cur.next = pre
pre = cur
cur = tmp
return pre
# 反转后半段
right = reverse(slow)
left = head
# 判断前后是否相同
while right:
if left.val != right.val:
return False
left = left.next
right = right.next
return True
题干:链表判断有环,典中典
class Solution:
def hasCycle(self, head: Optional[ListNode]) -> bool:
if not head or not head.next:
return False
slow = fast = head
fast = slow.next
while slow != fast and slow and fast and fast.next:
slow = slow.next
fast = fast.next.next
return True if slow == fast else False
更加优雅的版本
class Solution:
def hasCycle(self, head: Optional[ListNode]) -> bool:
slow = fast = head # 乌龟和兔子同时从起点出发
while fast and fast.next:
slow = slow.next # 乌龟走一步
fast = fast.next.next # 兔子走两步
if fast is slow: # 兔子追上乌龟(套圈),说明有环
return True
return False # 访问到了链表末尾,无环
题干:空间 O1 找环入口
思路:先找快慢指针相撞的位置,之后将快指针挪到头,之后和慢指针一步步走,再次相撞就是环入口
原理:假设进环前的路程为 a,环长为 b。设慢指针走了 x 步时,快慢指针相遇,此时快指针走了 2x 步。显然 2x-x=kb(快指针比慢指针多走了 k 圈),即 x=kb。也就是说慢指针总共走过的路程是 kb,但这 kb 当中,实际上包含了进环前的一个小 a,因此慢指针在环中只走了 kb-a 步,再往前走 a 步,就得到完整的 k 圈,即环入口。
class Solution:
def detectCycle(self, head: Optional[ListNode]) -> Optional[ListNode]:
fast = slow = head
while fast and fast.next:
fast = fast.next.next
slow = slow.next
if fast == slow:
break
if not fast or not fast.next:
return
fast = head
while fast != slow:
fast = fast.next
slow = slow.next
return fast
题干:两个有序链表合成一个有序链表
思路:用一个 pre 节点来连整个链表,两条链表的节点谁小谁移动。注意最后剩下的也要连到后面。
class Solution:
def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]:
prehead = ListNode(-1)
pre = prehead # 用一个 pre 来链接链表
cur1 = list1
cur2 = list2
while cur1 and cur2:
if cur1.val <= cur2.val: # 谁小谁移动
pre.next = cur1
cur1 = cur1.next
pre = pre.next
else:
pre.next = cur2
cur2 = cur2.next
pre = pre.next
if cur1:
pre.next = cur1
else:
pre.next = cur2
return prehead.next
题干:将两个链表诸位相加,大于10则进一位,返回相加完的最终链表。
思路:以其中一条链表为基准,另一条逐位加到上面,当出现长度不同和进位问题时,延长该链表。
class Solution:
def addTwoNumbers(self, l1: Optional[ListNode], l2: Optional[ListNode]) -> Optional[ListNode]:
cur1 = l1
cur2 = l2
# 将 cur1 的值加到 cur2 中,注意如果 cur1 更长或者有进位,需要加节点
while cur1 and cur2:
if not cur2.next and cur1.next:
cur2.next = ListNode(0)
cur2.val += cur1.val
if cur2.val >= 10:
cur2.val %= 10
if cur2.next:
cur2.next.val += 1
else:
node = ListNode(1)
cur2.next = node
cur2 = cur2.next
cur1 = cur1.next
# 如果 cur2 最后还是大于 10,则反复加节点
while cur2 and cur2.val >= 10:
cur2.val %= 10
if cur2.next:
cur2.next.val += 1
else:
node = ListNode(1)
cur2.next = node
cur2 = cur2.next
return l2
题干:一次遍历删除倒数第 N 个结点。
思路:快指针先走 N 步,然后慢指针启动,当快指针到头,慢指针就是倒数第 N 个。
class Solution:
def removeNthFromEnd(self, head: Optional[ListNode], n: int) -> Optional[ListNode]:
fast = head
for _ in range(n):
fast = fast.next
slow = head
prehead = ListNode(-1)
pre = prehead
pre.next = head
while fast:
fast = fast.next
pre = slow
slow = slow.next
pre.next = slow.next
del slow
return prehead.next
题干:将每相邻的两个节点反转,后续不足两个的不变。
思路:用三个指针模拟 pre, cur, nx ,其中反转 cur 和 nx 即可。
class Solution:
def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]:
prehead = ListNode(-1)
pre = prehead
pre.next = head
cur = head
while cur:
nx = cur.next
if not nx:
return prehead.next
pre.next = nx
tmp = nx.next
nx.next = cur
cur.next = tmp
pre = cur
cur = cur.next
return prehead.next
题干:将连续的 k 个链表反转,后续不足 k 的不变。
思路:
- 实现 start 到 tail 的链表反转,实现如上述
206. 反转链表 - 用 pre 指针和 nx 指针维护反转后的链表连接,参考
24. 两两交换链表中的节点
class Solution:
def reverseKGroup(self, head: Optional[ListNode], k: int) -> Optional[ListNode]:
guard = ListNode(-1)
guard.next = head
def reverse(head: Optional[ListNode], k: int):
cur = head
pre = None
kk = k
while cur and k:
k -= 1
tmp = cur.next
cur.next = pre
pre = cur
cur = tmp
# print(pre, head)
return pre, head
cur = head
pre = guard
while cur:
tail = cur
for i in range(k - 1):
tail = tail.next
if not tail:
return guard.next
tail = tail.next
l, r = reverse(cur, k)
pre.next = l
r.next = tail
# print(tail)
pre = r
cur = tail
return guard.next
题干:有一条链表在 next 之外,每个节点多存一个指针 random,其可能指向该链表的任意位置或空。要求深拷贝这条链表。
思路:
- 由于新链表在创建过程中,random 可能指向还没创建的链表,因此需要想办法存新链表的每个节点,然后至少两轮遍历。
- 如果用哈希表,空间复杂度 O(N),即一轮创建新节点,第二轮连接新节点。哈希表的 key 是 node 地址,value 也是 node 地址。
- 也可以用另一种方法,在每个 cur 后面存对应的新 cur,相当于用 链表 模拟出了一种哈希,如果返回值不记录复杂度,则可以视作 空间复杂度 O1。
class Solution:
def copyRandomList(self, head: 'Optional[Node]') -> 'Optional[Node]':
m = defaultdict(Node)
cur = head
pre = Node(-1)
pre.next = cur
while cur:
new_node = Node(cur.val, None, None)
m[cur] = new_node
cur = cur.next
cur = head
print(m[head])
while cur:
if cur.next:
m[cur].next = m[cur.next]
if cur.random:
m[cur].random = m[cur.random]
cur = cur.next
return m[head] if head else None
题干:根据值对链表排序
思路:归并排序用链表实现,分成递归和迭代,递归空间复杂度 O(logN),迭代法是 O1
# 递归法更好写
class Solution:
def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
# 合并链表
def mergeList(head1, head2):
prehead = ListNode(-1)
pre = prehead
cur1 = head1
cur2 = head2
while cur1 and cur2:
if cur1.val <= cur2.val:
pre.next = cur1
cur1 = cur1.next
else:
pre.next = cur2
cur2 = cur2.next
pre = pre.next
pre.next = cur1 if cur1 else cur2
return prehead.next
def merge_sort(head, tail):
if not head:
return head
if head.next == tail:
head.next = None # 注意遍历到 tail 时要断开链表
return head
fast = head
slow = head
while fast != tail and fast.next != tail:
slow = slow.next
fast = fast.next.next
left = merge_sort(head, slow)
right = merge_sort(slow, tail)
return mergeList(left, right)
return merge_sort(head, None)
# 细节还是很多的
class Solution:
def sortList(self, head: Optional[ListNode]) -> Optional[ListNode]:
# 合并链表,要返回合并后的 head 和 tail
def mergeList(head1, head2):
prehead = ListNode(-1)
pre = prehead
cur1 = head1
cur2 = head2
while cur1 and cur2:
if cur1.val <= cur2.val:
pre.next = cur1
cur1 = cur1.next
else:
pre.next = cur2
cur2 = cur2.next
pre = pre.next
pre.next = cur1 if cur1 else cur2
while pre.next:
pre = pre.next
return prehead.next, pre
def getlenght(head) -> int:
cur = head
ans = 0
while cur:
cur = cur.next
ans += 1
return ans
# 分割 head 链表的第 k 个节点,返回分割后的头
def split(head, k) -> ListNode:
# 这里不能复用 getlength,否则会导致 logN 退化为 N2
# length = getlenght(head)
cur = head
for _ in range(k - 1):
if cur == None:
return None
cur = cur.next
if not cur or not cur.next:
return None
tmp = cur.next
cur.next = None
return tmp
# 倍增法实现迭代归并排序
prehead = ListNode(next=head)
step = 1
length = getlenght(head)
while step < length:
pre = prehead # 记录需要合并链表的 pre
cur = prehead.next
while cur:
head1 = cur # 两个 head 是切下来的两个 list
head2 = split(head1, step)
cur = split(head2, step) # cur 是合并后的 next
newhead, newtail = mergeList(head1, head2)
# print(newhead, newtail, step, cur)
pre.next = newhead
newtail.next = cur
pre = newtail # pre 编程 merge 后的 tail
step *= 2 # 倍增
return prehead.next
题干:之前是合并 两个链表,现在合并 k 个链表。共有 L 个元素,则时间复杂度最好 O(L log k)。
思路:
- 堆法,用 val 对做排序,把每个链表的头堆中,如果最小则弹出该节点,然后节点向后移动一次,由于原始列表有序,则包装栈中一定是最小的,空间复杂度 O(K)。
- 递归分治,和上面的归并排序差不多,不过这次的是对 k 进行分治。空间复杂度 O(logK)
- 倍增法分治,同样的用 step 对 k 分治,空间复杂度 O(1)
class Solution:
def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
def mergeList(head1, head2):
prehead = ListNode(-1)
pre = prehead
cur1 = head1
cur2 = head2
while cur1 and cur2:
if cur1.val <= cur2.val:
pre.next = cur1
cur1 = cur1.next
else:
pre.next = cur2
cur2 = cur2.next
pre = pre.next
pre.next = cur1 if cur1 else cur2
while pre.next:
pre = pre.next
return prehead.next
# 倍增法
n = len(lists)
step = 1
while step < n:
for i in range(0, n - step, step * 2):
lists[i] = mergeList(lists[i], lists[i + step])
step *= 2
return lists[0] if lists else None
题干:实现一个 LRU 缓存,需要有考虑 cache 大小,有 get 和 put 操作。其中如果 cache 满了,则删掉最久没用到的节点,如果 put 一个已有的 node 则相当于 update。此外,每个节点有 key 。
思路:
-
双向链表 + 哈希表。链表头代表最近使用,用 map 来 O1 找到节点的位置,并记录链表长度。
-
用哨兵来维护头和尾,哨兵的 next 是 head, 哨兵的 pre 是 tail。
-
get 需要两步,先找到节点,再把节点挪到头。
-
put 先判断是不是 update,如果不是,根据 cache 容量来创建新 node,并可能删除 tail
class Node:
def __init__(self, key=0, value=0, pre=None, next=None):
self.key = key
self.value = value
self.pre = pre
self.next = next
class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
# 双向链表哑节点一定在固定的 head
# 则 dummy.pre 就是 tail 节点
self.dummy = Node()
self.dummy.next = self.dummy
self.dummy.pre = self.dummy
self.map = defaultdict(lambda : -1)
# 将 node 挪到链表头
# 注意不管是挪节点还是 remove,都只对指针操作,创建节点在 put 中
def node_front(self, node):
if node == None:
return
node.pre = self.dummy
node.next = self.dummy.next
node.next.pre = node
node.pre.next = node
# 删去 node
def remove(self, node):
node.pre.next = node.next
node.next.pre = node.pre
# 用 map 查找节点是否在链表中,同时要注意维护 map
# 如何有,则将其挪到最前
def get_node(self, key):
if self.map[key] == -1:
del self.map[key]
return None
node = self.map[key]
self.remove(node)
self.node_front(node)
return node
# 用 get_node 返回 value
def get(self, key: int) -> int:
node = self.get_node(key)
return node.value if node else -1
# 如果当前节点已经在链表中,相当于查找更新并
# 否则相当于添加节点,则需要维护 map
def put(self, key: int, value: int) -> None:
node = self.get_node(key)
if node:
node.value = value
else:
new_node = Node()
new_node.value = value
new_node.key = key
self.map[key] = new_node
self.node_front(new_node)
if len(self.map) > self.capacity:
last_node = self.dummy.pre
del self.map[last_node.key]
self.remove(last_node)
二叉树 #
# 别忘了怎么构建 TreeNode 就行
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
ans = []
def dfs(node):
if node == None:
return node
dfs(node.left)
ans.append(node.val)
dfs(node.right)
dfs(root)
return ans
思路:层级遍历
class Solution:
def maxDepth(self, root: Optional[TreeNode]) -> int:
if root == None:
return 0
queue = deque()
queue.append(root)
ans = 0
while queue:
ans += 1
k = len(queue)
for i in range(k):
tmp = queue[0]
queue.popleft()
if tmp.left:
queue.append(tmp.left)
if tmp.right:
queue.append(tmp.right)
return ans
题干:将二叉树沿中轴线 左右反转。
思路:dfs 时候交换左右指针即可。
class Solution:
def invertTree(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
if not root:
return None
def dfs(node):
if not node:
return
node.left, node.right = node.right, node.left
dfs(node.left)
dfs(node.right)
dfs(root)
return root
题干:判断二叉树是否左右对称
思路:递归稍微有一点难度,检查左右节点的关系
class Solution:
def isSymmetric(self, root: Optional[TreeNode]) -> bool:
if not root:
return True
def check(l, r):
if not l and not r: # 到叶子了
return True
if not l or not r or l.val != r.val:
return False
return check(l.left, r.right) and check(l.right, r.left)
return check(root.left, root.right)
题干:直径指的是 树中任意两个节点之间最长路径的长度,注意其未必经过 root
思路:遍历每个点深度,左右子树深度差的最大值就是直径
# 把下面两个写在一个函数就是一轮遍历
class Solution:
def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int:
m = defaultdict(int)
def depth(node):
if not node:
m[node] = 0
return 0
de = max(depth(node.left), depth(node.right)) + 1
m[node] = de
return de
depth(root)
# print(m)
ans = 0
def dfs(node):
nonlocal ans
if not node:
return
ans = max(ans, m[node.left] + m[node.right])
dfs(node.left)
dfs(node.right)
dfs(root)
return ans
class Solution:
def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
if root == None:
return []
queue = deque()
queue.append(root)
ans = []
while queue:
k = len(queue)
res = []
for i in range(k):
tmp = queue[0]
res.append(tmp.val)
queue.popleft()
if tmp.left:
queue.append(tmp.left)
if tmp.right:
queue.append(tmp.right)
ans.append(res)
return ans
题干:用有序数组创建一颗 二叉查找树,要求平衡(左右深度不超过1)
思路:二分分治,每次用 mid 做根,左右二分做左右子树
class Solution:
def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]:
n = len(nums)
if n == 0:
return None
def dfs(l, r):
nonlocal nums
if l > r:
return None
mid = l + r >> 1
node = TreeNode(nums[mid])
node.left = dfs(l, mid - 1)
node.right = dfs(mid + 1, r)
return node
return dfs(0, n - 1)
思路:注意只查看左右子树是否为满足二叉搜索性质是错的,因为二叉搜索树要求左边全部元素小于跟。
具体思路很多:
- 由于中跟遍历是有序数组,因此只需要维护中跟的前一个数,然后判断当前是否比前面大即可。
- 前跟或后跟 遍历同时记录最大最小值,判断是否满足。
# 中跟
class Solution:
def isValidBST(self, root: Optional[TreeNode]) -> bool:
if not root:
return True
pre = -inf # pre 是中跟遍历的上一个元素
def dfs(node) -> bool:
nonlocal pre
if not node:
return True
l_ans = dfs(node.left) # 中跟遍历
if pre >= node.val: # 如果当前小于上个元素就错了
return False
pre = node.val
return l_ans and dfs(node.right)
return dfs(root)
思路:中跟遍历到的第 K 个就是答案
思考题:如果要频繁查找第 K 个,可以预处理每个节点下面的子节点数量,从而在一棵平衡树中做到查找 O(logN)。
class Solution:
def kthSmallest(self, root: Optional[TreeNode], k: int) -> int:
if not root:
return -1
ans = -1
def dfs(node):
nonlocal ans, k
if not node:
return
dfs(node.left)
k -= 1
if k == 0:
ans = node.val
return # 提前返回
dfs(node.right)
dfs(root)
return ans
题干:从右向左看一颗二叉树,返回看到的 list
思路:层级遍历记录最后一个值。
class Solution:
def rightSideView(self, root: Optional[TreeNode]) -> List[int]:
if root == None:
return []
queue = deque()
queue.append(root)
ans = []
while queue:
res = -1
k = len(queue)
for i in range(k):
tmp = queue[0]
res = tmp.val
queue.popleft()
if tmp.left:
queue.append(tmp.left)
if tmp.right:
queue.append(tmp.right)
ans.append(res)
return ans
题干:将链表用前跟遍历的顺序连接成链表,用树中左指针置空,右指针作为链表 next 来实现,要求原地修改。
思路:
- 按 右左跟 的后跟遍历是正常前跟遍历的 reverse,因此可以从后向前构建链表。
- 分治法,维护左右子链表的最后一个元素,用最后一个元素连接。前跟遍历即可。
# 逆序头插法
class Solution:
def flatten(self, root: Optional[TreeNode]) -> None:
"""
Do not return anything, modify root in-place instead.
"""
head = None
def dfs(node):
nonlocal head
if node == None:
return
dfs(node.right)
dfs(node.left)
node.left = None
node.right = head
head = node
dfs(root)
return head
# 分治法
class Solution:
def flatten(self, root: Optional[TreeNode]) -> None:
if not root:
return None
def dfs(node) -> TreeNode:
if not node:
return None
l_tail = dfs(node.left)
r_tail = dfs(node.right)
if l_tail: # 连接左右两个子链表
l_tail.right = node.right
node.right = node.left
node.left = None
if r_tail:
return r_tail
elif l_tail:
return l_tail
else:
return node
return dfs(root)
思路:递归构造二叉树,输入是前序和中序的左右边界(共四个边界),每轮前序的第一个元素是下一轮的根,用 hash O1 查到 inorder 中的位置,然后就能推出四个边界。
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
m = defaultdict(int)
n = len(preorder)
for i in range(len(inorder)):
m[inorder[i]] = i
if len(preorder) == 0:
return None
# root = TreeNode(preorder[0])
def dfs(lp, rp, li, ri):
nonlocal m, preorder, inorder
if lp > rp or li > ri:
return None
node = TreeNode(preorder[lp])
x = m[node.val]
length_l = x - li
length_r = ri - x
node.left = dfs(lp + 1, lp + length_l, li, x - 1)
node.right = dfs(lp + length_l + 1, rp, x + 1, ri)
return node
return dfs(0, n - 1, 0, n - 1)
题干:二叉树里节点值之和等于 targetSum 的 路径 的数目。路径指任意两节点相连的过程,此题中只考虑从上往下的路径。
思路:和 560. 和为 K 的子数组 一样用前缀和,但此处由于树的递归操作,因此要在左右递归后恢复现场,比较综合的题。
class Solution:
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> int:
prefix = defaultdict(int)
prefix[0] = 1
num = 0
ans = 0
def dfs(node):
nonlocal prefix, num, targetSum, ans
if not node:
return None
# print(node.val, prefix, num, ans)
num += node.val
ans += prefix[num - targetSum]
prefix[num] += 1
dfs(node.left) # 注意此题是要等左右都遍历完才恢复现场
dfs(node.right)
prefix[num] -= 1
num -= node.val
dfs(root)
return ans
题干:找二叉树两点的最近祖先
思路:用后跟遍历 find 判断最近公共祖先。
由于后跟遍历到 node 时,已知其下面的情况,如果找不到两节点中的任意一个,则代表其不是祖先,返回找到的那个。
如果该节点左右都能找到节点,说明他就是答案,返回自己,而其父节点一定只能找到一边,最终返回该答案。
此题思路相当优雅,值得多看。
class Solution:
def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
def find(node, p, q):
if not node:
return None
if node == p or node == q:
return node
left = find(node.left, p, q)
right = find(node.right, p, q)
if left and right:
return node
return left if left else right
return find(root, p, q)
题干:找路径中最大的,注意会出现负数。此题路径距离在 node 上的 val。
思路:depth 记录包含该点的最大值,第二轮遍历中,此点最大路径就是 左右 depth + 该点值。也可以用一轮遍历。
注意:存在大量负数,需要每次保证 max(depth, 0)
class Solution:
def maxPathSum(self, root: Optional[TreeNode]) -> int:
m = defaultdict(lambda : 0)
def depth(node):
nonlocal m
if not node:
return 0
m[node] = max(max(depth(node.left), 0), max(depth(node.right), 0)) + node.val
return m[node]
depth(root)
# print(m[root])
ans = -inf
def dfs(node):
nonlocal m, ans
if not node:
return
ans = max(ans, max(m[node.left], 0) + max(m[node.right], 0) + node.val)
dfs(node.left)
dfs(node.right)
dfs(root)
return ans
图论 #
题干:
给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
样例:
输入:grid = [
['1','1','0','0','0'],
['1','1','0','0','0'],
['0','0','1','0','0'],
['0','0','0','1','1']
]
输出:3
思路:DFS,BFS 都可以,最简单的图
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
direction = [[0, 1], [0, -1], [1, 0], [-1, 0]]
m = len(grid)
n = len(grid[0])
def dfs(i: int, j: int) -> None:
if i < 0 or i >= m or j < 0 or j >= n or grid[i][j] != '1':
return
grid[i][j] = '0' # 淹了陆地
dfs(i, j - 1)
dfs(i, j + 1)
dfs(i - 1, j)
dfs(i + 1, j)
ans = 0
for i in range(m):
for j in range(n):
if grid[i][j] == '1':
dfs(i, j)
ans += 1
return ans
题干:
在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:
- 值
0代表空单元格; - 值
1代表新鲜橘子; - 值
2代表腐烂的橘子。
每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。
返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1 。
# BFS 遍历
class Solution:
def orangesRotting(self, grid: List[List[int]]) -> int:
directionX = [1, 0, -1, 0]
directionY = [0, 1, 0, -1]
q = deque()
m = len(grid)
n = len(grid[0])
for i in range(0, m):
for j in range(0, n):
if grid[i][j] == 2:
q.append([i, j])
# 此题初始状态不算第一步
ans = -1 if q else 0
while q:
k = len(q)
ans += 1
for _ in range(k):
temp = q[0]
q.popleft()
for i in range(4):
dx = temp[0] + directionX[i]
dy = temp[1] + directionY[i]
if 0 <= dx < m and 0 <= dy < n and grid[dx][dy] == 1:
grid[dx][dy] = 2
q.append([dx, dy])
# 如果还有走不到的橘子
for i in range(0, m):
for j in range(0, n):
if grid[i][j] == 1:
return -1
return ans
题干:找图中是否由环
# 拓扑排序
class Solution:
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
grid = [[] for _ in range(numCourses)]
indegree = [0] * numCourses
for x, y in prerequisites:
grid[y].append(x)
indegree[x] += 1
# print(grid)
queue = deque()
for i in range(numCourses):
if indegree[i] == 0:
queue.append(i)
while queue:
node = queue[0]
queue.popleft()
for to in grid[node]:
indegree[to] -= 1
if indegree[to] == 0:
queue.append(to)
for i in range(numCourses):
if indegree[i] != 0:
return False
return True
思路:多维护一个 isEnd 即可,其他都是工程问题
N = 26
class Trie:
def __init__(self):
# isEnd 判断每个单词的结尾
# next 数组表示该字符可能的下一个字符
self.isEnd = False
self.next = [None] * N
def insert(self, word: str) -> None:
node = self
for c in word:
index = ord(c) - ord('a')
if not node.next[index]:
node.next[index] = Trie()
node = node.next[index]
# 标记每个单词的结尾
node.isEnd = True
def search(self, word: str) -> bool:
node = self
for c in word:
# 先前进再判空
index = ord(c) - ord('a')
node = node.next[index]
if not node:
return False
# 遍历到底 但不是结尾
return node.isEnd
def startsWith(self, prefix: str) -> bool:
node = self
for c in prefix:
index = ord(c) - ord('a')
node = node.next[index]
if not node:
return False
# 判读前缀不用管 isEnd
return True
回溯 #
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
# 用 path 维护选了哪些,每轮遍历 nums
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
ans = []
n = len(nums)
def dfs(i: int, path: list):
nonlocal ans, n
if i == n:
ans.append(path.copy())
for idx in range(n):
if nums[idx] not in path:
path.append(nums[idx])
dfs(i + 1, path)
path.pop()
dfs(0, [])
return ans
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
# 选或不选维护 arr 即可,注意深拷贝
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
ans = []
n = len(nums)
def dfs(index, arr):
if index == n:
ans.append(arr)
return
# 下面是深拷贝
new_arr = copy.deepcopy(arr)
new_arr.append(nums[index])
dfs(index + 1, arr)
dfs(index + 1, new_arr)
dfs(0, [])
return ans
题干:分块遍历回溯,只能选而不能不选
有:
m = {'2':['a','b','c'],
'3':['d','e','f'],
'4':['g','h','i'],
'5':['j','k','l'],
'6':['m','n','o'],
'7':['p','q','r','s'],
'8':['t','u','v'],
'9':['w','x','y','z']}
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
class Solution:
def letterCombinations(self, digits: str) -> List[str]:
n = len(digits)
if n == 0:
return []
# 分块
m = {'2':['a','b','c'],
'3':['d','e','f'],
'4':['g','h','i'],
'5':['j','k','l'],
'6':['m','n','o'],
'7':['p','q','r','s'],
'8':['t','u','v'],
'9':['w','x','y','z']}
ans = []
def dfs(index, myStr: str):
if index == n:
ans.append(myStr)
return
for letter in m[digits[index]]:
dfs(index + 1, myStr + letter)
dfs(0, "")
return ans
题干:在 30 个数中选任意个数(可重复),使其综合等于 target,返回全部可能的序列(去重)
# 数据量小 能直接递归回溯
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
ans = set() # 去重用,注意其中不能装可变元素
n = len(candidates)
def dfs(array: List[int], sum: int):
nonlocal ans
if sum > target:
return
if sum == target:
array.sort()
ans.add(tuple(array)) # 使 array 转成元组并哈希
return
for i in range(n):
newArray = array + [candidates[i]]
dfs(newArray, sum + candidates[i])
dfs([], 0)
output = []
for i in ans:
output.append(i)
return output
题干:向生成 n 组合法的括号(n < 9),可能有哪些组合方法
思路:暴力 + 剪枝,用省的左右括号数量做递归输入,如果剩下的右括号少于左括号,则不合法。
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
ans = []
path = []
def dfs(l, r, path):
nonlocal ans
# print(l, r, path)
if l < 0 or r < 0:
return
if l == 0 and r == 0:
ans.append(deepcopy(path))
return
if l > r:
return
path.append('(')
dfs(l - 1, r, path)
path.pop()
path.append(')')
dfs(l, r - 1, path)
path.pop()
dfs(n, n, path)
output = []
for i in ans:
s = ""
for j in i:
s += j
output.append(s)
return output
题干:想在单词矩阵中找“贪吃蛇”形状的某单词,不能走回头路
输入:
board =
[['A','B','C','E'],
['S','F','C','S'],
['A','D','E','E']],
word = "ABCCED"
输出:true
提示:
['*','*','*','E'],
['S','F','*','S'],
['A','*','*','E']
思路:剪枝 + 回溯 DFS
# 走格子回溯,注意优化和剪枝
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
DIRS = [[0, 1], [1, 0], [-1, 0], [0, -1]]
n = len(board)
m = len(board[0])
t = len(word)
ans = False
# 下面是两个小优化
cnt = Counter(c for row in board for c in row)
if not cnt >= Counter(word): # 如果 word 中的词频大于 board 直接返回
return False
if cnt[word[-1]] < cnt[word[0]]: # 将词频更大的元素作为头
word = word[::-1]
def dfs(i: int, x: int, y: int):
nonlocal ans
if ans: # 剪枝
return
if i == t:
ans = True
return
tmp = board[x][y] # 不走重复路,因此要回溯
board[x][y] = "."
for dx, dy in DIRS:
nx = x + dx
ny = y + dy
if 0 <= nx < n and 0 <= ny < m:
if board[nx][ny] == word[i]:
dfs(i + 1, nx, ny)
board[x][y] = tmp
# 找字母的头
for i in range(n):
for j in range(m):
if board[i][j] == word[0]:
dfs(1, i, j)
return ans
题干:将一个字符串,切割为 N 个子字符串,保证每个子字符串是回文串,有多少种切割方法。
输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]
# 枚举字串结束的位置
class Solution:
def partition(self, s: str) -> List[List[str]]:
ans = []
path = []
n = len(s)
def dfs(index):
nonlocal s, ans
# print(index, path)
if index == n:
ans.append(deepcopy(path))
for j in range(index + 1, n + 1):
t = s[index:j] # 注意要先切片后再 reverse,否则切片值不一样
if t[::-1] == t:
path.append(t)
dfs(j)
path.pop()
dfs(0)
return ans
题干:经典中的经典,N 最大值是 9,返回棋盘
思路:
- 状态简化为 path,每个皇后情况其实就是长度为 N 的一维数组,其中 i,x 表示第 i 行的皇后在第 j 列。
- 记录 visited 的皇后位置,保证下一行皇后不与列重复。
- 用 check 检查对角线是否满足,其中 abs(new_x - old_x) == abs(new_y - old_y) 表示对角线有重叠。
- 递归输入当前到第 i 行,回溯并找到满足条件 23 的 path 即可,最后转化为棋盘。
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
ans = []
path = []
visit = set()
def check(arr: List[int]):
# print(arr)
if len(arr) == 0:
return True
last = arr[-1]
last_i = len(arr) - 1
for i, x in enumerate(arr):
if i == last_i:
continue
if abs(last_i - i) == abs(last - x):
return False
return True
def dfs(i: int):
if i == n:
ans.append(path.copy())
for idx in range(n):
if idx not in visit:
# print(idx)
path.append(idx)
visit.add(idx)
if check(path):
# print(path, i)
dfs(i + 1)
path.pop()
visit.remove(idx)
dfs(0)
output = []
for line in ans:
line_output = []
for idx in line:
s = ""
# print(line)
for i in range(n):
if i != idx:
s += "."
else:
s += 'Q'
line_output.append(s)
output.append(line_output)
return output
二分查找 #
题干:找第一个大于等于 target 的下标
class Solution:
def searchInsert(self, nums: List[int], target: int) -> int:
l = 0
r = len(nums)
while l < r:
mid = l + r >> 1
if nums[mid] < target: # 如果找第一个大于 target,就此处改为 <= 。
l = mid + 1
else:
r = mid
return l
# 等价库函数
# return bisect.bisect_left(nums, target)
题干:
- 每行中的整数从左到右按非严格递增顺序排列。
- 每行的第一个整数大于前一行的最后一个整数。
找是否有一个数为 target
思路:
先根据每行最后一个数做二分查找,得到该数应该在那行,然后对该行二分查找。
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
m = len(matrix)
n = len(matrix[0])
l = 0
r = m
while l < r:
mid = l + r >> 1
if matrix[mid][-1] < target:
l = mid + 1
else:
r = mid
line = l
if line >= m:
return False
l = 0
r = n
while l < r:
mid = l + r >> 1
if matrix[line][mid] < target:
l = mid + 1
else:
r = mid
if matrix[line][l] == target:
return True
return False
题干:找 bisect_left 和 bisect_right 的区间
# 此题用于区分四种二分板子 (yxc 模板)
class Solution:
def searchRange(self, nums: List[int], target: int) -> List[int]:
n = len(nums)
if n == 0:
return[-1, -1]
# yxc 板子
# 这种写法是第一个大于等于 target
# 若写出 nums[mid] <= target 则为第一个大于 target
# 下面这种是 lower_bound 写出 <= 是 upper_bound
l = 0
r = n - 1
while l < r :
mid = l + r >> 1
if nums[mid] < target:
l = mid + 1
else:
r = mid
first = l
if nums[first] != target:
return [-1 , -1]
# 这种写法是最后一个小于等于 target
# 若写出 nums[mid] >= target 则为第一个小于 target
l = 0
r = n - 1
while l < r :
mid = l + r + 1 >> 1
if nums[mid] > target:
r = mid - 1
else:
l = mid
return [first, l]
题干:一个有序数组,随机旋转了一次,O(logN) 找目标值
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
思路:还是二分查找,但 check 的条件比较复杂,先判断左右哪边有序(一定有一边有序),然后在判断 target 应该在哪边。
# 需要考虑很多边界值的二分查找
class Solution:
def search(self, nums: List[int], target: int) -> bool:
l = 0
r = len(nums) - 1
while l < r:
mid = l + r >> 1
if target == nums[mid]:
return mid
# 若右半段有序
if nums[mid] < nums[r]:
if nums[mid] < target <= nums[r]:
l = mid + 1
else:
r = mid
else:
if nums[l] <= target < nums[mid]:
r = mid
else:
l = mid + 1
return l if nums[l] == target else -1
题干:有序数组旋转后,O(logN) 找最小值
思路:上面那题的简化版
class Solution:
def findMin(self, nums: List[int]) -> int:
n = len(nums)
l = 0
r = n - 1
while l < r:
mid = l + r >> 1
if nums[mid] > nums[r]:
l = mid + 1
else:
r = mid
return nums[l]
题干:找两个递增数组的中位数,要求O(log (m+n))
思路:
① 若 a=b,则a或b为所求中位数,算法结束。
② 若a<b,则舍弃序列A中较小的一半,同时舍弃序列 B中较大的一半。
③ 若a>b,则舍弃序列A中较大的一半,同时舍弃序列 B中较小的一半。
注意:下面代码实际上不是严格 O(log (m+n))。
# ① 若 a=b,则a或b为所求中位数,算法结束。
# ② 若a<b,则舍弃序列A中较小的一半,同时舍弃序列 B中较大的一半。
# ③ 若a>b,则舍弃序列A中较大的一半,同时舍弃序列 B中较小的一半。
class Solution:
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
def get_mid(arr, l, r):
if (r - l + 1) % 2 == 0:
return (arr[(l + r) // 2] + arr[(l + r) // 2 + 1]) / 2
else:
return arr[(l + r) // 2]
n1 = len(nums1)
n2 = len(nums2)
l1, l2 = 0, 0
r1, r2 = n1 - 1, n2 - 1
# 只要有一个数组长度降到了 2 或更少 就停止
while (r1 - l1 > 1) and (r2 - l2 > 1):
m1 = get_mid(nums1, l1, r1)
m2 = get_mid(nums2, l2, r2)
mid1_index = (l1 + r1) // 2
mid2_index = (l2 + r2) // 2
# 步长必须由较短的那一半决定,防止长数组切多了,或者短数组切空了
shift = min(mid1_index - l1, mid2_index - l2)
# 步长至少为 1
if shift == 0: shift = 1
if m1 == m2:
return m1
if m1 < m2:
# 舍弃 nums1 左边,nums2 右边
l1 = l1 + shift
r2 = r2 - shift
else:
# 舍弃 nums1 右边,nums2 左边
r1 = r1 - shift
l2 = l2 + shift
# 此时至少有一个数组长度 <= 2。
# 由于剩余元素很少(对于 LeetCode 数据规模),直接合并排序是安全的。
last = nums1[l1:r1+1] + nums2[l2:r2+1]
last.sort()
n = len(last)
if n == 0: return 0
if n % 2 == 1:
return last[n // 2]
else:
return (last[n // 2 - 1] + last[n // 2]) / 2
return -1
栈 #
题干:判断括号串有“({[”是否合法,其中不能出现交错
class Solution:
def isValid(self, s: str) -> bool:
stack = []
for c in s:
if c == '(':
stack.append(0)
if c == '[':
stack.append(1)
if c == "{":
stack.append(2)
if c == ')':
if not stack or stack[-1] != 0:
return False
stack.pop()
if c == ']':
if not stack or stack[-1] != 1:
return False
stack.pop()
if c == '}':
if not stack or stack[-1] != 2:
return False
stack.pop()
if not stack:
return True
return False
题干:构造一个可以 O1 找到最小元素的栈
思路:用原始栈 + 单调栈模拟,此题单调栈是非严格单调递减。如果插入小于等于单调栈尾,插入两个栈。如果普通栈尾和单调栈尾相同,弹出两者。
# 用栈 + 单调栈模拟
# 此题单调栈是非严格单调递减,记录当前最小值
class MinStack:
def __init__(self):
self.stack = []
self.min_stack = []
# 如果插入小于等于单调栈尾,插入单调栈
def push(self, val: int) -> None:
if not self.min_stack:
self.stack.append(val)
self.min_stack.append(val)
return
if val <= self.min_stack[-1]:
self.stack.append(val)
self.min_stack.append(val)
else:
self.stack.append(val)
# 如果普通栈尾和单调栈尾相同,弹出两者
def pop(self) -> None:
if self.stack[-1] == self.min_stack[-1]:
self.min_stack.pop()
self.stack.pop()
else:
self.stack.pop()
def top(self) -> int:
return self.stack[-1]
def getMin(self) -> int:
self.min_stack.append(self.stack[-1])
return self.min_stack[-1]
题干:
给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。
输入:s = "3[a]2[bc]"
输出:"aaabcbc"
输入:s = "3[a2[c]]"
输出:"accaccacc"
输入:s = "2[abc]3[cd]ef"
输出:"abcabccdcdcdef"
思路:维护当前字符串和 栈,栈里面维护之前的字符串和需要重复的数量,优雅写法如下
class Solution:
def decodeString(self, s: str) -> str:
stack = []
current_num = 0
current_str = ""
for c in s:
if '0' <= c <= '9':
current_num = current_num * 10 + int(c)
elif c == '[':
# 数字和当前字符串入栈
stack.append((current_str, current_num))
current_str = ""
current_num = 0
elif c == ']':
# 出栈并构造新的字符串
last_str, repeat_num = stack.pop()
current_str = last_str + current_str * repeat_num
else:
# 普通字符追加
current_str += c
return current_str
题干:找数组中 “下一个更大” 的位置,返回每个点和下一个更大的距离。
思路:标准单调栈,记录一个单调递减栈,当有大于顶的元素时,弹出栈并维护答案。
class Solution:
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
n = len(temperatures)
stack = []
ans = [0] * n
for i in range(n):
# print(i, stack)
while stack and temperatures[stack[-1]] < temperatures[i]:
ans[stack[-1]] = i - stack[-1]
stack.pop()
stack.append(i)
return ans
题干:获取一个柱状图中的最大矩阵
输入:heights = [2,1,5,6,2,3] 输出:10 解释:最大的矩形为图中*区域,面积为 10 高度 6 | █ 5 | * * 4 | * * 3 | * * █ 2 | █ * * █ █ 1 | █ █ * * █ █ █ 0 1 2 3 4 5
思路:
和接雨水思路一样用前缀后缀,但由于需要记录第一个小于 X 的位置,不能和接雨水一样直接双指针。
记录每个点前第一个和后第一个比他小的位置,对每个元素,由于左右区间内一定大于等于 height[i],因此则 heights[i] * (right[i] - left[i] - 1) 就是以改点为基准的最大值。遍历所有的最大值的就是答案。
想找到每个点前第一个和后第一个比他小的位置,就用到单调递增栈。
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
stack = []
n = len(heights)
left = [-1] * n # 用单调栈找左边第一个比自身小的
# ans = 0
for i in range(n):
while stack and heights[stack[-1]] >= heights[i]:
stack.pop()
if stack:
left[i] = stack[-1]
stack.append(i)
right = [n] * n # 用单调栈找右边第一个比自身小的
stack.clear()
for i in range(n - 1, -1, -1):
while stack and heights[stack[-1]] >= heights[i]:
stack.pop()
if stack:
right[i] = stack[-1]
stack.append(i)
# 对每个元素,由于左右区间内一定大于等于 height[i]
# 则 heights[i] * (right[i] - left[i] - 1) 就是包含区间的答案
ans = 0
for i in range(n):
ans = max(ans, heights[i] * (right[i] - left[i] - 1))
return ans
堆 #
topK 问题,长度 N 的数组中找到第 K 个元素。
- 堆:时间 O(N*logK),空间 O(K)
- 快速选择:时间平均 ON,最坏 ON2,空间 O1
- BFPRT:用五分中位数的中位数做 pivot,最坏 ON
# 第 K 大用 小跟堆
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
pq = []
for i, x in enumerate(nums):
if len(pq) >= k:
heapq.heappush(pq, x)
heapq.heappop(pq)
else:
heapq.heappush(pq, x)
return pq[0]
题干:找数组前 K 个频率的元素。
思路:Counter 后找 TopK 即可,注意 heapq 中用 cnt 做 key。
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
m = Counter(nums)
pq = []
for i, j in m.items():
if len(pq) >= k:
heapq.heappush(pq, (j, i))
heapq.heappop(pq)
else:
heapq.heappush(pq, (j, i))
ans = []
while pq:
ans.append(pq[0][1])
heapq.heappop(pq)
return ans
题干:想让 O logN 插入,能 O1 找到当前数据流的中位数
思路:一半大根堆,一半小根堆。想让两者长度相同,在遇到一个新元素时候,现在对面的堆里面洗一下,然后弹出到另一边。
class MedianFinder:
def __init__(self):
# 小顶堆(存较大的一半)
self.smallPQ = []
# 大顶堆(存较小的一半,存入负数以模拟大顶堆)
self.bigPQ = []
def addNum(self, num: int) -> None:
if len(self.smallPQ) != len(self.bigPQ):
# 先将 num 放入小顶堆,然后将堆顶移到大顶堆
heapq.heappush(self.smallPQ, num)
heapq.heappush(self.bigPQ, -heapq.heappop(self.smallPQ))
else:
# 先将 num 放入大顶堆,然后将堆顶移到小顶堆
heapq.heappush(self.bigPQ, -num)
heapq.heappush(self.smallPQ, -heapq.heappop(self.bigPQ))
def findMedian(self) -> float:
if len(self.smallPQ) != len(self.bigPQ):
return self.smallPQ[0]
else:
return (self.smallPQ[0] - self.bigPQ[0]) / 2.0
贪心 #
题干:
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。
输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
思路:直接找前缀最小即可,或者如果递增直接卖。这道题应该和股票买卖234一起看。
class Solution:
def maxProfit(self, prices: List[int]) -> int:
pre_min = inf
n = len(prices)
ans = 0
for i in range(n):
pre_min = min(pre_min, prices[i])
ans = max(ans, prices[i] - pre_min)
return ans
题干:数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够从第一个下标到达最后一个下标。
思路:遍历并维护当前能跳到的最远下标,如果当前下标大于之前能跳到的最远下标,说明不能完成任务。注意不要遍历到最后一个点。
class Solution:
def canJump(self, nums: List[int]) -> bool:
fast = 0
n = len(nums)
# 遍历到 n - 2
for i in range(0, n - 1):
fast = max(fast, i + nums[i])
# 如果到最后一个之前就卡住了
if fast <= i:
return False
return True
题干:包装上面情景下一定能走到最后下标,想得到最短的跳跃次数。
输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
思路:同样维护当前能走到的最远位置 fast 和上一次跳跃到的最远位置 slow,如果 i 走到上一次跳跃的最短位置 slow,则说明需要再跳一次,就跳到当前可以跳到的最远位置 fast。
class Solution:
def jump(self, nums: List[int]) -> int:
fast = 0
slow = 0
ans = 0
n = len(nums)
# 遍历到 n - 2
for i in range(0, n - 1):
fast = max(fast, i + nums[i])
if i == slow:
ans += 1
slow = fast
return ans
题干:想让尽可能多的切割一个字符串,让同一字母最多出现在一个子字符串中。
思路:记录每个字母的第一次出现和最后一次出现,就是一个字母的最小区间,之后思路和合并区间完全一样。
class Solution:
def partitionLabels(self, s: str) -> List[int]:
m = defaultdict(lambda : [-1, -1])
for i, c in enumerate(list(s)):
if m[c][0] == -1:
m[c][0] = i
m[c][1] = i
else:
m[c][1] = i
intervals = list(m.values())
# print(intervals)
def merge(intervals: List[List[int]]) -> List[List[int]]:
intervals.sort()
ans = []
l = intervals[0][0]
r = intervals[0][1]
for x, y in intervals:
if x == -1 or y == -1:
continue
if x <= r:
r = max(y, r)
else:
ans.append([l, r])
l = x
r = y
ans.append([l, r])
return ans
ans = []
res = merge(intervals)
for i, j in res:
ans.append(j - i + 1)
return ans
动态规划 #
题干:求斐波那契数列
思路:最简单的 DP,如果用通式或者矩阵快速幂可以做到 OlogN
class Solution:
def climbStairs(self, n: int) -> int:
dp = [0] * (n + 1)
dp[0] = 1
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
题干:模拟一个杨辉三角
class Solution:
def generate(self, numRows: int) -> List[List[int]]:
ans = [[] for _ in range(numRows)]
ans[0].append(1)
for i in range(1, numRows):
ans[i].append(1)
for y in range(1, len(ans[i - 1])):
ans[i].append(ans[i - 1][y - 1] + ans[i - 1][y])
ans[i].append(1)
return ans
题干:典中典的 打家劫舍,即不能偷两个相邻的房子,问最大能偷多少。
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
思路:DP 维护 “到该点最多偷多少”
状态转移:dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])
class Solution:
def rob(self, nums: List[int]) -> int:
ans = 0
n = len(nums)
if n == 1:
return nums[0]
if n == 2:
return max(nums[0], nums[1])
dp = [0] * n
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, n):
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i])
ans = max(ans, dp[i])
print(dp)
return ans
思考:如果是环形,则直接讨论第一个位置有没有被打,如果被打了就对后面 [2 , n - 2] 做打家劫舍,如果没有,就对后面 [2, n - 1] 做打家劫舍。
题干:给你一个整数 n ,返回 和为 n 的完全平方数的最少数量。
思路:明显的完全背包,其中 1 - int(sqrt(n)) 是可以选的物品,n 是背包容量。因此用 DP 表示背包容量为 i 时,正好填满的物品最小数量。
状态转移方程 dp[i + j * j] = min(dp[i + j * j], dp[i] + 1)
此外,此题也可以数学 O1 做出来。
class Solution:
def numSquares(self, n: int) -> int:
high = int(sqrt(n))
dp = [inf] * (n + 1)
dp[0] = 0
for i in range(n + 1):
for j in range(1, high + 1):
if i + j * j <= n:
dp[i + j * j] = min(dp[i + j * j], dp[i] + 1)
return dp[n]
题干:给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额,计算凑到总金额的最小硬币个数。
思路:还是完全背包问题,多了一个可能背包凑不满的情况
状态转移方程:dp[i + j] = min(dp[i + j], dp[i] + 1)
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
n = amount
dp = [inf] * (n + 1)
dp[0] = 0
for i in range(n + 1):
for j in coins:
if i + j <= n:
dp[i + j] = min(dp[i + j], dp[i] + 1)
return dp[n] if dp[n] != inf else -1
思考:零钱兑换Ⅱ是返回凑到amount的硬币种类的数量,那么此题就要先遍历coins 再遍历 i,然后用状态转移方程 dp[i + j] += dp[i]
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
n = amount
dp = [0] * (n + 1)
dp[0] = 1
for j in coins:
for i in range(n + 1):
if i + j <= n:
dp[i + j] += dp[i]
# print(dp)
print(dp)
return dp[n]
题干:给你一个字符串 s 和一个字符串列表 wordDict 。如果可以利用wordDict中出现的一个或多个单词拼接出 s 则返回 true。可以重复使用。
思路:同样还是类似完全背包的动态规划,其中 dp 表示以 i 为截至的字符串能不能被凑出来。如果想节省复杂度,可以用 wordDict 记录每个的长度,然后切片得到子字符串,这样就和完全背包几乎一样。
状态转移方程 :dp[j] = dp[i] or dp[j] if s[i:j] in m
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
m = set(wordDict)
n = len(s)
dp = [False] * (n + 1)
dp[0] = True
for i in range(n + 1):
for j in range(i + 1, n + 1):
if s[i:j] in m:
dp[j] = dp[i] or dp[j]
print(dp)
return dp[n]
题干:找到最长递增子序列的长度(子序列是指对每个元素选或不选而不改变顺序的集合)。
思路:
DP + 二分查找,遍历并用 tails 记录当前 i 下所有可能的子序列长度对应的最小值。
比如 [1, 3, 4, 2, 5] 遍历完后,tails 应该是 [1, 2, 4, 5],所以 tails 一定是单调递增的。
遍历过程中可以对其二分查找,找 nums[i] 在 tails 对应的位置,如果最后 l 与 tails 长度相同,则说明 tails 可以扩充,否则修改 tails[l] 的最小值
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
n = len(nums)
tails = []
tails.append(nums[0])
for i in range(1, n):
l = 0
r = len(tails)
while l < r:
mid = l + r >> 1
if tails[mid] < nums[i]:
l = mid + 1
else:
r = mid
# print(tails, l, nums[i])
if l == len(tails):
tails.append(nums[i])
else:
tails[l] = min(nums[i], tails[l])
return len(tails)
题干:找出数组中乘积最大的非空连续 子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
思路:用二维 DP 记录包含 i 位置的最大和最小元素,如果是 num 就向前对应,如果是负数就 相互取反。
class Solution:
def maxProduct(self, nums: List[int]) -> int:
# dp 表示包含 i 位置的最大和最小元素
n = len(nums)
dp = [[0, 0] for _ in range(n + 1)]
dp[0] = [1, 1]
ans = nums[0] # 初始化为第一个元素
for i in range(1, n + 1):
cur = nums[i - 1]
if cur < 0:
dp[i][0] = max(cur, dp[i - 1][1] * cur)
dp[i][1] = min(cur, dp[i - 1][0] * cur)
elif cur > 0:
dp[i][0] = max(cur, dp[i - 1][0] * cur)
dp[i][1] = min(cur, dp[i - 1][1] * cur)
else:
dp[i][0] = 0
dp[i][1] = 0
ans = max(ans, dp[i][0])
print(dp)
return ans
题干:给你一个 只包含正整数 的 数组 nums 。判断是否可以将这个数组分割成两个子集使其 sum 相等。
思路:目标是在数组中选到和为 sum(nums) // 2 的数,本质上还是一个物品只能选一次的背包问题,如果一开始 sum 是偶数直接返回。
dp[i][j] 表示为,前 j 个数能否表达出 sum 为 i 。
因此状态转移方程为:dp[i][j] = dp[i][j - 1] or dp[i - x][j - 1] or dp[i][j] 即不选 j 和选 j 的情况。
# 目标是在数组中选到和为 sum(nums) // 2 的数
# 本质上还是一个物品只能选一次的背包问题
class Solution:
def canPartition(self, nums: List[int]) -> bool:
all = sum(nums)
n = len(nums)
if all % 2 == 1: # all 必须为偶数
return False
target = all // 2
# 前 j 个数能否表达出 sum 为 i
dp = [[False] * n for _ in range(target + 1)]
for i in range(n):
if nums[i] > target:
return False
dp[0][i] = True
for i in range(target + 1):
for j in range(1, n):
x = nums[j]
dp[i][j] = dp[i][j - 1]
if i - x >= 0:
dp[i][j] = dp[i - x][j - 1] or dp[i][j]
return dp[-1][-1]
题干:找括号串中,有效部分的最长大小
输入:s = ")()())"
输出:4
解释:最长有效括号子串是 "()()"
思路:用 stack 模拟找到出错的位置,之后就变成找未错位置中最长的串就可以。这题和动态规划关系不大。
class Solution:
def longestValidParentheses(self, s: str) -> int:
n = len(s)
unmatch = [0] * n
stack = []
for i in range(n):
c = s[i]
if not stack and c == ')':
unmatch[i] = 1 # 出错
elif c == ')':
stack.pop()
if c == '(':
stack.append(i)
for i in stack: # 剩下的栈也是错的
unmatch[i] = 1
# 找最长的连续零
l = -1
r = -1
ans = 0
for i in range(n):
if unmatch[i]:
r = i
ans = max(ans, r - l - 1)
l = i
return max(ans, n - r - 1)
多维动态规划 #
题干:一个机器人位于一个 m x n 网格的左上角,可以向左或向右走一步,问一共有多少条不同的路径。
思路:组合数即可,机器人共走 m + n - 2 步,总步数中选 m - 1 步就是答案(理解为找出机器人向右走的占总步数中的哪些部分,就是一共多少路径)
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
return comb(m + n - 2, m - 1)
题干:给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左t上角到右下角的路径,使得路径上的数字总和为最小。
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
思路:记忆化搜索走迷宫,相当于每次只有向右和向下两种选择。
class Solution:
def minPathSum(self, grid: List[List[int]]) -> int:
m = len(grid)
n = len(grid[0])
@cache
def dfs(i: int, j: int) -> int:
if i == 0 and j == 0:
return grid[i][j]
elif i == 0:
return dfs(i, j - 1) + grid[i][j]
elif j == 0:
return dfs(i - 1, j) + grid[i][j]
return min(dfs(i - 1, j), dfs(i, j - 1)) + grid[i][j]
return dfs(m - 1, n - 1)
题干:找数组中的最长回文子串(子串一定连续)
思路:
- 中心扩散动态规划,即区间 DP 的一种,
dp[i][j]表示从 i - j 的回文字串长度,先遍历 j ,再从 j 向前遍历 i,从而实现从中间遍历到两边。时间复杂度 ON2 - 马拉车算法实现 ON
class Solution:
def longestPalindrome(self, s: str) -> str:
n = len(s)
dp = [[-inf] * n for _ in range(n)]
mx = 0
index = 0 # 用 index 维护最大值的位置
for j, y in enumerate(s):
for i in range(j, -1, -1):
x = s[i]
if j < i:
continue
if i == j:
dp[i][j] = 1
if mx < 1:
index = i
mx = 1
if i == j - 1 and x == y:
dp[i][j] = 2
if mx < 2:
index = i
mx = 2
if x == y and j - i >= 2:
if mx < dp[i + 1][j - 1] + 2:
index = i
mx = dp[i + 1][j - 1] + 2
dp[i][j] = max(dp[i + 1][j - 1] + 2, dp[i][j])
# print(dp)
return s[index:index + mx]
题干:给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3 。
思路:用 dp[i][j] 表示遍历到 i j 时最长公共序列长度,当 t1[i] == t2[j] 的时候 +1, 然后分别跳过 i 和 j。
下面用记忆化搜索,写起来简单点
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
@cache
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, j - 1), dfs(i - 1, j))
return dfs(len(text1) - 1, len(text2) - 1)
给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
思路:
状态机 DP,dp[i][j]设置为前 i 个 word1 转变为 前 j 个 word2 的编辑距离。
有初始化:当 i == 0 时 dp[i][j] = j 同样,j == 0 时 dp[i][j] = i,这可以理解为全部删除 word1 串,和将空串添加为 word2 串。
如果 word1[i ] == word2[j] 则不需要增加编辑距离,其他情况下,有状态转移方程 min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1 可以视作分别做 ”删、增、改“
# 记忆化搜索
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
n = len(word1)
m = len(word2)
@cache
def dfs(i, j):
if i < 0:
return j + 1
if j < 0:
return i + 1
if word1[i] == word2[j]:
return dfs(i - 1, j - 1)
return min(dfs(i - 1, j), dfs(i, j - 1), dfs(i - 1, j - 1)) + 1
return dfs(n - 1, m - 1)
# 递推
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
n = len(word1)
m = len(word2)
dp = [[0] * (m + 1) for _ in range(n + 1)]
for i in range(n + 1):
dp[i][0] = i # 将 word1 的前 i 个字符变为空字符串,需要 i 次删除
for j in range(m + 1):
dp[0][j] = j # 将空字符串变为 word2 的前 j 个字符,需要 j 次插入
for i in range(1, n + 1):
for j in range(1, m + 1):
if word1[i - 1] == word2[j - 1]:
dp[i][j] = dp[i - 1][j - 1]
else:
dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + 1)
return dp[n][m]
技巧 #
题干:数组除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
思路:异或和即可
class Solution:
def singleNumber(self, nums: List[int]) -> int:
ans = 0
for num in nums:
ans ^= num
return ans
题干:找数组中大于等于一半数量的元素
思路:摩尔计数法,记录当前最多元素 X。当出现 X,则 cnt + 1, 出现其他元素则 cnt - 1。当 cnt <= 0 时候,改 X 为当前遍历到的元素。
class Solution:
def majorityElement(self, nums: List[int]) -> int:
count = 0
candidate = nums[0]
for num in nums:
if num == candidate:
count += 1
else:
if count > 0:
count -= 1
else:
count = 1
candidate = num
return candidate
题干:荷兰国旗问题,数组中只有 0, 1, 2 如何快速排序
思路:用 p1,p2 记录当前遍历到的 0 和 2 的数量,分别指向前 p1 位置和后 p2 位置,swap 直到满足 nums[i] == 1。
# 荷兰国旗问题
class Solution:
def sortColors(self, nums: List[int]) -> None:
n = len(nums)
p0 = 0
p2 = n - 1
for i in range(n):
while i < p2 and nums[i] == 2:
nums[i], nums[p2] = nums[p2], nums[i]
p2 -= 1
while nums[i] == 0 and i > p0:
nums[i], nums[p0] = nums[p0], nums[i]
p0 += 1
return nums
题干:找数组中下一个排序,希望原地修改。
输入:nums = [1,2,3]
输出:[1,3,2]
找规律:
[1, 2, 3, 4]
[1, 2, 4, 3]
[1, 3, 2, 4]
[1, 3, 4, 2]
[1, 4, 2, 3]
[1, 4, 3, 2]
[2, 1, 3, 4]
[2, 1, 4, 3]
思路:
由上面的规律,可以发现找下一个排序的规律:
- 如果当前数组为全部降序,则说明此时已经最大,下一个是全部取反。
- 从后往前找第一个升序(arr[i] > arr[i + 1])的位置 i,x。
- 从后往前找第一个比 x 大的位置:j,y。
- 交换 i, j 的位置,此时 i 之后的数一定是降序,将之后的的位置反转为升序。
思考:如果改成 “上一个排序”,则同样是找规律,把降序升序换一下就可以。
class Solution:
def nextPermutation(self, nums: List[int]) -> None:
"""
Do not return anything, modify nums in-place instead.
"""
n = len(nums)
# 从后向前找最后一个非升序的下标
idx1 = -1
for i in range(n - 2, -1, -1):
if nums[i + 1] > nums[i]:
idx1 = i
break
if idx1 == -1: # 如果全部降序,相当于是最大序列。下一个是最小序列,需要特判。
return nums.reverse()
# 从后往前找最后一个大于 nums[idx1] 的
idx2 = -1
for i in range(n - 1, -1, -1):
if nums[i] > nums[idx1]:
idx2 = i
break
# 交互下标
nums[idx1], nums[idx2] = nums[idx2], nums[idx1]
# 将 idx1 后面的降序子数组改成升序
nums[idx1+1:] = reversed(nums[idx1+1:]) # 实际上会产生临时内存,如果想空间 O1 可以双指针反转
题干:数组中只有一个重复的数,想不修改数组的情况下 O1 空间找到该数。数组中全部数字都在 [1, n] 中。
思路:将数字映射视为链表,然后和双指针找环入口一样。
# 将数组映射视作链表,之后就与快慢指针找环入口一致
class Solution:
def findDuplicate(self, nums: List[int]) -> int:
n = len(nums)
slow = fast = 0
while True:
fast = nums[nums[fast]]
slow = nums[slow]
if fast == slow:
break
fast = 0
while True:
fast = nums[fast]
slow = nums[slow]
if fast == slow:
return fast
return 0
备忘录 #
完整输入输出 #
尤其涉及到 class 的代码,类和完整输入输出可能会忘了怎么写。
下面以反转链表的题目为例,完整版本如下。
import os
class node():
def __init__(self, val = -1, next = None):
self.val = val
self.next = next
def reversed_list(head):
pre = None
cur = head
while cur:
tmp = cur.next
cur.next = pre
pre = cur
cur = tmp
return pre
def build(arr):
dummy = node()
cur = dummy
for i in arr:
tmp = node(i)
cur.next = tmp
cur = cur.next
return dummy
def print_node(head):
cur = head
while cur:
print(cur.val, end=" ")
cur = cur.next
if __name__ == '__main__':
arr = list(map(int, input().split()))
head = build(arr).next
head_re = reversed_list(head)
print_node(head_re)
import 包 #
import sys
import math
from collections import *
from heapq import *
from bisect import *
from functools import *
# 递归防止爆栈,cache 必备
sys.setrecursionlimit(10**6)