目录
一、LC 105. 从前序与中序遍历序列构造二叉树
1.1 题求
1.2 求解
法一:递归
# 44ms - 89.77
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
# preorder - 根节点 - 左子树 - 右子树 - [3,9,20,15,7]
# inorder - 左子树 - 根节点 - 右子树 - [9,3,15,20,7]
def helper(lhs, rhs):
''' 递归 '''
# 越界返回
if lhs > rhs:
return None
# 当前(子)树根节点
root = TreeNode(preorder.pop(0))
# 当前(子)树根节点在中序遍历中的位置索引
idx = hashmap[root.val]
# 当前(子)树根节点的左子树
root.left = helper(lhs, idx-1)
# 当前(子)树根节点的右子树
root.right = helper(idx+1, rhs)
# 返回当前(子)树根节点
return root
# 中序遍历节点值及其索引映射表
hashmap = {val: idx for idx, val in enumerate(inorder)}
return helper(0, len(preorder)-1)
法二:迭代
# 36ms - 97.19% - 费解
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
if not preorder:
return None
root = TreeNode(preorder[0])
stack = [root] # 前序遍历首个节点 - 根节点
inorderIndex = 0 # 中序遍历首个节点
# 依次枚举前序遍历中除了第 1 个节点外的每个节点
for i in range(1, len(preorder)):
# 前序遍历第 i 个节点
preorderVal = preorder[i]
# 栈顶当前节点
node = stack[-1]
# 如果 inorder[inorderIndex] 和栈顶节点不同,于是将当前节点 node 作为栈顶节点的左儿子
if node.val != inorder[inorderIndex]:
node.left = TreeNode(preorderVal)
stack.append(node.left) # 左儿子入栈
# 如果 inorderIndex 恰好指向栈顶节点,那么不断地弹出栈顶节点并栈顶节点 inorderIndex,
# 并将当前节点作为最后一个弹出的节点的右儿子
else:
while stack and stack[-1].val == inorder[inorderIndex]:
node = stack.pop() # 栈顶节点
inorderIndex += 1 # 栈顶节点
node.right = TreeNode(preorderVal)
stack.append(node.right)
return root
参考资料:
二、LC 172. 阶乘后的零
2.1 题求
2.2 求解
法一:计算因子 5
# 88ms - 32.11%
class Solution:
def trailingZeroes(self, n: int) -> int:
zero_count = 0
for i in range(5, n+1, 5):
current = i
while current % 5 == 0:
zero_count += 1
current //= 5
return zero_count
# 88ms - 32.11%
class Solution:
def trailingZeroes(self, n: int) -> int:
zero_count = 0
for i in range(5, n+1, 5):
power_of_5 = 5 # 5 的幂次
while i % power_of_5 == 0:
zero_count += 1
power_of_5 *= 5
return zero_count
法三:高效的计算因子 5
# 36ms - 74.84%
class Solution:
def trailingZeroes(self, n: int) -> int:
zero_count = 0
current_multiple = 5
while n >= current_multiple:
zero_count += n // current_multiple
current_multiple *= 5
return zero_count
# 36ms - 74.84%
class Solution:
def trailingZeroes(self, n: int) -> int:
zero_count = 0
while n > 0:
n //= 5
zero_count += n
return zero_count
参考资料
三、</> 古生物血缘远近判定
3.1 题求
3.2 求解
法一:动态规划 - 递归 (从右往左比较)
# 16ms - 60.00%
### 编辑距离 ###
# 提取输入
word1, word2 = input().split(',') # word1, word2 = map(str, input().split(','))
# 构造问题求解类对象
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
# 哈希表备忘录
memo = {}
def dp(i, j):
''' 返回 word1[0..i] 和 word2[0..j] 的最小编辑距离 '''
if memo.get((i, j)) is not None:
return memo[(i, j)]
# word1 已经到头了, 只需要在 word1 插入剩余 word2 即可
if i == -1:
return j + 1
# word2 已经到头了, 只需要在 word2 插入剩余 word1 即可
elif j == -1:
return i + 1
# 当前值相等, 编辑距离完全取决于前面的部分
if word1[i] == word2[j]:
memo[(i, j)] = dp(i-1, j-1)
# 插入、删除、修改三种方式中, 最少次数/编辑距离能够达到的一者
else:
memo[(i, j)] = min(dp(i-1, j), dp(i, j-1), dp(i-1, j-1)) + 1
return memo[(i, j)]
return dp(len(word1)-1, len(word2)-1)
# 实例化类对象并调用方法
solver = Solution()
print(solver.minDistance(word1, word2))
法二:动态规划 - 迭代 (从左往右比较)
# 16ms - 60.00%
### 编辑距离 ###
# 提取输入
word1, word2 = input().split(',') # word1, word2 = map(str, input().split(','))
import collections
# 构造问题求解类对象
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
m, n = len(word1), len(word2)
if not word1:
return n
elif not word2:
return m
dis = 0 # 最小距离
visited = {(0, 0)} # 已遍历坐标/索引
queue = collections.deque([(0, 0)]) # 辅助 BFS 队列
while queue:
num = len(queue)
for _ in range(num):
# 当前需要试探的坐标 (两字符串索引)
i, j = queue.popleft()
# 从左往右遍历 - 相等直接跳过 不增加操作数
while i < m and j < n and word1[i] == word2[j]:
i += 1
j += 1
# 与 DFS 不同, 当两个字符串都正好遍历完, 则此时操作数即最少操作数
# (相当于其余部分完全相同 或 遍历到尽头 无需任何操作 应返回结果了)
if i == m and j == n:
return dis
# i 还在范围内 且 i+1 尚未遍历过 —— 增
idx_add = (i+1, j)
if i < m and idx_add not in visited:
visited.add(idx_add)
queue.append(idx_add)
# j 还在范围内 且 j+1 尚未遍历过 —— 删
idx_delete = (i, j+1)
if j < n and idx_delete not in visited:
visited.add(idx_delete)
queue.append(idx_delete)
# i, j 还在范围内 且 i+1, j+1 尚未遍历过 —— 改
idx_update = (i+1, j+1)
if i < m and j < n and idx_update not in visited:
visited.add(idx_update)
queue.append(idx_update)
# 每层操作数+1 (增、删、改 均为 1 个操作)
dis += 1
# 实例化类对象并调用方法
solver = Solution()
print(solver.minDistance(word1, word2))
参考资料:
四、LC 84. 柱状图中的最大矩形 ☆
4.1 题求
4.2 求解
法一:暴力法
# 超出时间限制
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
max_area = heights[0]
for i in range(len(heights)):
if heights[i] == 0:
continue
# 往左扫描
cur_len = heights[i]
k = i - 1
while k >= 0 and heights[k] > 0:
cur_len = min(cur_len, heights[k]) # 最小高度
max_area = max(max_area, (i-k+1)*cur_len) # 最大面积
k -= 1
return max_area
官方说明
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int n = heights.size();
int ans = 0;
// 枚举左边界
for (int left = 0; left < n; ++left) {
int minHeight = INT_MAX;
// 枚举右边界
for (int right = left; right < n; ++right) {
// 确定高度
minHeight = min(minHeight, heights[right]);
// 计算面积
ans = max(ans, (right-left+1) * minHeight);
}
}
return ans;
}
};
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int n = heights.size();
int ans = 0;
for (int mid = 0; mid < n; ++mid) {
// 枚举高
int height = heights[mid];
int left = mid, right = mid;
// 确定左右边界
while (left - 1 >= 0 && heights[left - 1] >= height) {
--left;
}
while (right + 1 < n && heights[right + 1] >= height) {
++right;
}
// 计算面积
ans = max(ans, (right - left + 1) * height);
}
return ans;
}
};
# 308ms - 38.62%
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
# 柱子总数
n = len(heights)
# 左、右侧最近最低柱子索引
left, right = [0] * n, [0] * n
# 单调递增栈 - 辅助存放左侧最近最低柱子索引
mono_stack = []
# 从左往右遍历
for i in range(n):
# 栈非空 且 栈顶位置高度 heights[mono_stack[-1]] 高于当前位置高度 heights[i],
# 则不会挡住 heights[i], 直至遇到距离最近且高度低于 heights[i] 的首根柱子
while mono_stack and heights[mono_stack[-1]] >= heights[i]:
mono_stack.pop()
# mono_stack[-1] 即 i 左侧距离最近且高度低于 heights[i] 的首根柱子索引
left[i] = mono_stack[-1] if mono_stack else -1 # -1 是左哨兵
mono_stack.append(i)
# 单调递增栈 - 辅助存放右侧最近最低柱子索引
mono_stack = []
# 从右往左遍历
for i in range(n-1, -1, -1):
while mono_stack and heights[mono_stack[-1]] >= heights[i]:
mono_stack.pop()
# mono_stack[-1] 即 i 右侧最近的小于 heights[i] 的柱子的索引
right[i] = mono_stack[-1] if mono_stack else n # n 是右哨兵
mono_stack.append(i)
# 最大矩形面积
ans = max((right[i]-left[i]-1) * heights[i] for i in range(n)) if n > 0 else 0
return ans
# 208ms - 92.67%
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
# 柱子总数
n = len(heights)
# 左、右侧最近最低柱子索引
left, right = [0] * n, [n] * n # new init
# 单调递增栈 - 辅助存放左侧最近最低柱子索引
mono_stack = []
# 从左往右遍历
for i in range(n):
while mono_stack and heights[mono_stack[-1]] >= heights[i]:
right[mono_stack[-1]] = i # new step - i 即为此时弹出的位置的右边界
mono_stack.pop()
# mono_stack[-1] 即 i 左侧最近的小于 heights[i] 的柱子的索引
left[i] = mono_stack[-1] if mono_stack else -1 # -1 是左哨兵
mono_stack.append(i)
# 最大矩形面积
ans = max((right[i]-left[i]-1) * heights[i] for i in range(n)) if n > 0 else 0
return ans
参考资料:
五、LC 85. 最大矩形
5.1 题求
5.2 求解
# 256ms - 17.77%
class Solution:
def maximalRectangle(self, matrix: List[List[str]]) -> int:
if not matrix:
return 0
m = len(matrix) # 行数
n = len(matrix[0]) # 列数
dp = [[0 for _ in range(n+1)] for _ in range(m)] # dp 数组
max_area = 0
for i in range(m):
for j in range(n):
# 横向计算
if matrix[i][j] == '0': # 0 不考虑
continue
dp[i][j] = dp[i][j-1] + 1 # 最右边的 dp +1 作为 index = -1
# 纵向比较
cur_area = cur_width = dp[i][j] # 当前最大面积、宽度
k = i - 1
while k >= 0 and dp[k][j] > 0:
cur_width = min(dp[k][j], cur_width)
cur_area = max((i-k+1) * cur_width, cur_area)
k -= 1
# 最大矩形面积
max_area = max(max_area, cur_area)
return max_area
# 88ms - 65.11%
class Solution:
def maximalRectangle(self, matrix: List[List[str]]) -> int:
if not matrix:
return 0
m = len(matrix)
n = len(matrix[0])
max_area = 0
left = [[0 for _ in range(n)] for _ in range(m)]
# 横向最大长度
for i in range(m):
for j in range(n):
if matrix[i][j] == '1':
left[i][j] = (0 if j == 0 else left[i][j-1]) + 1
# 对于每一列使用基于柱状图的方法 (左转的柱状图, left 相当于 heights)
for j in range(n):
up = [0] * m # 上边界
down = [m] * m # 下边界
stack = []
# 从上往下遍历
for i in range(m):
while stack and left[stack[-1]][j] >= left[i][j]:
down[stack[-1]] = i # i 即为此时弹出的位置的下边界
stack.pop()
up[i] = stack[-1] if stack else -1 # 栈顶即为当前位置的上边界
stack.append(i)
# 当前列的最大矩形面积
for i in range(m):
max_area = max(max_area, (down[i]-up[i]-1) * left[i][j])
return max_area
参考资料:
六、LC 146. LRU 缓存机制
6.1 题求
6.2 求解
法一:基于数组
# 超出时间限制
class LRUCache:
def __init__(self, capacity: int):
self.size = capacity # LRU 容量固定不变
self.hashmap = {} # 记录 key-value
self.deque = collections.deque() # 记录 key 的最近调用顺序
def get(self, key: int) -> int:
val = self.hashmap.get(key)
if val is None:
return -1
else:
self.update(key, val) # 更新最近调用的 key 顺序
return val
def put(self, key: int, value: int) -> None:
# 待加入值已存在, 则更新最近调用的 key 顺序
if self.get(key) != -1:
self.update(key, value)
# 待加入值不存在, 且长度未达到上限
elif len(self.deque) < self.size:
self.add(key, value)
# 待加入值不存在, 但长度到达上限, 则删一增一
else:
self.delete(key, value)
#print(self.deque)
def update(self, key, value):
self.hashmap[key] = value
num = len(self.deque) # 需要弹出的次数
# 1. 前端
while self.deque[0] != key:
self.deque.append(self.deque.popleft())
num -= 1
# 2. 找到
cur_key = self.deque.popleft() # 最近使用
num -= 1
# 3. 其余
while num > 0:
self.deque.append(self.deque.popleft())
num -= 1
# 4. 最近
self.deque.appendleft(cur_key) # 最近使用
def delete(self, key, value):
del self.hashmap[self.deque.pop()] # 删除最后一个最不常用值
self.hashmap[key] = value # 更新键值对
self.deque.appendleft(key) # 加入新值在首位
def add(self, key, value):
self.hashmap[key] = value # 加入键值对
self.deque.appendleft(key) # 加入新值在首位
官方说明
class LRUCache(collections.OrderedDict):
def __init__(self, capacity: int):
super().__init__()
self.capacity = capacity
def get(self, key: int) -> int:
if key not in self:
return -1
self.move_to_end(key)
return self[key]
def put(self, key: int, value: int) -> None:
if key in self:
self.move_to_end(key)
self[key] = value
if len(self) > self.capacity:
self.popitem(last=False)
# 612ms - 52.21%
class DLinkedNode:
''' 双链表节点 '''
def __init__(self, key=0, value=0):
self.key = key
self.value = value
self.prev = None
self.next = None
class LRUCache:
''' LRU 缓存 '''
def __init__(self, capacity: int):
self.cache = dict() # 哈希表:key -> node
self.head = DLinkedNode() # 伪头部节点节点
self.tail = DLinkedNode() # 伪尾部节点
self.head.next = self.tail # ->
self.tail.prev = self.head # <-
self.capacity = capacity # 最大容量
self.size = 0 # 当前容量
def get(self, key: int) -> int:
if key not in self.cache:
return -1
# 如果 key 存在,先通过哈希表定位 node,再移到 head
node = self.cache[key]
self.moveToHead(node)
return node.value
def put(self, key: int, value: int) -> None:
if key not in self.cache:
# 如果 key 不存在,创建一个新的节点
node = DLinkedNode(key, value)
# 添加进哈希表
self.cache[key] = node
# 添加至双向链表的头部
self.addToHead(node)
self.size += 1
if self.size > self.capacity:
# 如果超出容量,删除双向链表的尾部节点
removed = self.removeTail()
# 删除哈希表中对应的项
self.cache.pop(removed.key)
self.size -= 1
else:
# 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
node = self.cache[key]
node.value = value
self.moveToHead(node)
def addToHead(self, node):
node.prev = self.head # <-
node.next = self.head.next # ->
self.head.next.prev = node # <-
self.head.next = node # ->
def removeNode(self, node):
node.prev.next = node.next # ->
node.next.prev = node.prev # <-
def moveToHead(self, node):
self.removeNode(node)
self.addToHead(node)
def removeTail(self):
node = self.tail.prev # <-
self.removeNode(node)
return node
参考资料
七、LC 907. 子数组的最小值之和
7.1 题求
7.2 求解
法一:暴力法
# 超出时间限制
class Solution:
def sumSubarrayMins(self, arr: List[int]) -> int:
n = len(arr)
summ = 0
for i in range(n): # 起点索引
dp = arr[i]
for j in range(i, n): # 终点索引
dp = min(arr[j], dp)
summ += dp
return summ % (10**9 + 7)
官方说明
法一:单调栈
# 192ms - 38.80%
class Solution:
def sumSubarrayMins(self, arr: List[int]) -> int:
MOD = 10 ** 9 + 7
N = len(arr)
# prev has i* - 1 in increasing order of A[i* - 1]
# where i* is the answer to query j
stack = [] # 单调栈
prev_ = [None] * N # 左边界数组
for i in range(N):
while stack and arr[stack[-1]] >= arr[i]: # 遇小弹出
stack.pop()
# 左侧首个小于 arr[i] 的数的索引
prev_[i] = stack[-1] if stack else -1 # -1 为左哨兵
stack.append(i) ### 作为后续数组中潜在的左边界索引
# next has k* + 1 in increasing order of A[k* + 1]
# where k* is the answer to query j
stack = [] # 单调栈
next_ = [None] * N # 右边界数组
for k in range(N-1, -1, -1):
while stack and arr[stack[-1]] > arr[k]: # 遇小弹出
stack.pop()
# 右侧首个小于 arr[k] 的数的索引
next_[k] = stack[-1] if stack else N # N 为右哨兵
stack.append(k) ### 作为后续数组中潜在的右边界索引
# print(arr) # [3, 1, 2, 4]
# print(prev_) # [-1, -1, 1, 2]
# print(next_) # [1, 4, 4, 4]
# Use prev/next array to count answer
# 分别计算 arr[i] 作为最小值时的子数组个数 * arr[i]
return sum((i-prev_[i]) * (next_[i]-i) * arr[i]
for i in range(N)) % MOD
法二:最小值栈
# 136ms - 84.80%
class Solution:
def sumSubarrayMins(self, arr: List[int]) -> int:
MOD = 10**9 + 7
stack = [] # 重要点的编码记录栈 —— 以 val 为最小值的子数组个数 count
ans = dot = 0 # 和 & 子数组和
# index, val
for j, y_val in enumerate(arr):
# Add all answers for subarrays [i, j], i <= j
y_cnt = 1
# x_val >= y_val 则 x_val 不再作为最小值, 而以 y_val 取而代之
while stack and stack[-1][0] >= y_val:
x_val, x_cnt = stack.pop()
y_cnt += x_cnt # y_val 可作为最小值的个数
dot -= x_val * x_cnt # x_val 不再作为最小值, 删除其和
stack.append((y_val, y_cnt)) # 重要点的编码 (val, count) 入栈
dot += y_val * y_cnt # 最小值 * 个数 (同时包含之前和当前的最小值部分)
ans += dot # 和
# [3,1,2,4]
#print(f"y_val: {y_val}, y_cnt: {y_cnt}, stack: {stack}, dot: {dot}, ans: {ans}")
# y_val: 3, y_cnt: 1, stack: [(3, 1)], dot: 3, ans: 3
# y_val: 1, y_cnt: 2, stack: [(1, 2)], dot: 2, ans: 5
# y_val: 2, y_cnt: 1, stack: [(1, 2), (2, 1)], dot: 4, ans: 9
# y_val: 4, y_cnt: 1, stack: [(1, 2), (2, 1), (4, 1)], dot: 8, ans: 17
return ans % MOD
参考资料:
八、LC 403. 青蛙过河
8.1 题求
8.2 求解
法一:动态规划
# 164ms - 81.37%
class Solution:
def canCross(self, stones: List[int]) -> bool:
# dp[i] 表示到达石头 i 的步长 k 的集合
dp = defaultdict(set) # 缺省字典 - 默认类型 set
dp[0] = {0}
stones_set = set(stones) # 石头集合
for i in stones:
for k in dp[i]:
for j in [-1, 0, 1]:
step = k + j
dist = i + step
if (step > 0) and dist in stones_set:
dp[dist].add(step) # 以 setp 步到达 dist
return dp[stones[-1]] != set()
官方说明
# 64ms - 96.18%
class Solution:
def canCross(self, stones: List[int]) -> bool:
# import functools
# @functools.lru_cache(maxsize=128, typed=False)
# maxsize 是缓存的最大结果数目, 当 maxsize=None 时会变成简单的 cache, 就不具备 LRU特性了
# typed 表示是否根据传入参数类型的不同, 缓存不同的结果
@lru_cache(None)
def dfs(pos, step):
''' dfs(pos, step): 若青蛙经 step 步跳跃至 pos 位置的石头, 它能否跳到终点 '''
# 可达终点
if pos == stones[-1]:
return True
# 三种步长
for d in {-1, 0, 1}:
s = step + d # 新步长
p = pos + s # 新步长跳达的位置
if (s > 0) and (p in stones_set):
if dfs(p, s):
return True
# 不可达终点
return False
stones_set = set(stones) # 石头距离集合
pos, step = 0, 0 # 起点, 步长
return dfs(pos, step)
参考资料: