91. 解码方法
动态规划。和第70题爬楼梯差不多,dp[i] 只与前两项有关,都是每次可以走一步或两步,只是这题走一步时需要判断是否为0,走两步需要判断数字组是不是在10到26中。
(1) 若 s[i] == '0',dp[i] = dp[i-2]。因为只能由第 i-2 项走两步得到。
(2) 若 s[i-1] == '1' 或 (s[i-1] == '2' 且 0 <= s[i] <= 6) ,dp[i] = dp[i-1] + dp[i-2]。可以第 i-1 项走一步得到,也第 i-2 项可以走两步得到。
(3) 其他则 dp[i] = dp[i-1],只能走一步得到。
同样可以优化内存,只存最后的三个位置的编码数就够了,每次覆盖前面的。
92. 反转链表 II
先找到第 m 个节点,翻转第 m 到第 n 个节点的顺序。可以三个指针迭代翻转要翻转的部分,翻转到了第 m 个后把翻转部分和前后连起来;也可以把当前元素不断插到第 m-1个元素之后(也就是不断放到第 m 个元素的位置)。
93. 复原IP地址
① 直接。三层循环尝试放三个点,每层长度小于等于 3。如果等于 0 但长度大于 1,或大于 255,就break。
② 回溯。backtrack(pre = -1, dots = 3),参数表示上个放置点和待放置点。分别检查 pre 与 pre 的后三个位置之间的字符串是否有效,有效就继续放下个点。
判断两点之间的数字是否有效,如果不为 0 则需要小于 255,如果为 0 则需要长度为 1:int(segment) <= 255 if segment[0] != '0' else len(segment) == 1
94. 二叉树的中序遍历
递归是经典方法写起来也容易就不说了。
栈的方法,每个节点入栈,到左子树再入栈,循环直到左子树为空。出栈一个元素 p 并访问,然后 p = p.right,对 p 进行前面的循环入栈。
相当于把左边依次入栈,到了最左以后,出栈顺序就是左子树优先的顺序了。
stack = []
p= root
while(p or stack):
while(p):
stack.append(p)
p = p.left
p = stack.pop()
访问 p
p = p.right
95. 不同的二叉搜索树 II
递归。每次以一个数为根节点,左边和右边的数分别会构成它的左子树和右子树中。generate_trees(start, end) 参数为当前需要生成的子树的数字范围。对于范围内的每个数 i,递归得到每个 i 的左右子树列表,把这些左右子树分别与 i 组合起来 (对左、右子树二层循环两两合在一起),保存。最后返回所有数字的子树列表 。
96. 不同的二叉搜索树
① 动态规划。根 i 的不同二叉搜索树数量,是左右子树个数的笛卡尔积。同时左右子树的数量只与序列长度有关,与序列内容无关。
初始化长度为 n+1 的数组 dp,dp[0] = 1,dp[1] = 1。外层循环 for i in range(2, n+1) 遍历每个 dp[i],内层 for j in range(1, i+1) 遍历每个可能根节点。循环执行 dp[i] += dp[j-1] * dp[i-j]。
② 数学方法。Catalan数:C0 = 1,Cn+1 = Cn*2(2n+1) / (n+2)
97. 交错字符串
① 回溯。helper(p1, p2, p3) 参数分别为三个字符串中下标。先判断如果三个指针都到终点则 return True,只有 s3 到终点则 return False。如果 p1<len(s1) 且 s1[p1] == s3[p3],则 helper(p1+1,p2,p3+1)。对 p2 也是。else 都没有等于 s3[p3] 的就 return False。加上记忆优化。
② 二维动态规划。考虑用 s1 和 s2 的某个前缀是否能形成 s3 的一个前缀。dp[i][j] 表示到 s1 的前 i 个字符以及 s2 前 j 个字符,是否交错构成 s3 的前缀。
初始化 dp[0][0] = 1,以下情况:
(1) s1[i-1] == s3[i+j-1] 且 dp[i-1][j],则 dp[i - 1][j] == 1。也就是 s1 的第 i 个字符即 s[i-1] 可以匹配 s3 字符。
(2) s2同上。
(2) s1[i-1] 和 s2[j-1] 都不能匹配 s3[i+j+1],则 dp[i][j] = False。
最后 return dp[len(s1)][len(s2)]。
98. 验证二叉搜索树
① 递归。helper(node, lower = float('-inf'), upper = float('inf')),结点的值与左边界 lower 和右边界 upper 比较。然后,对左子树和右子树递归进行该过程。
(1) if node.val <= lower or node.val >= upper: return False
(2) 递归检查左子树,左边界为lower,右边界为当前节点值。if not helper(node.left, lower, node.val): return False
(3) 递归检查右子树。if not helper(node.right, node.val, upper): return False
② 迭代。使用栈将递归转为迭代。
初始化 stack = [(root, float('-inf'), float('inf'))] 。while stack 栈不为空循环 :出栈。检查。左右子树入栈。
(1) node, lower, upper = stack.pop()
(2) if node.val <= lower or node.val >= upper: return False
(3) stack.append((root.left, lower, val));stack.append((root.right, val, upper));
③ 中序遍历,检查每个元素是否比下一个小。
99. 恢复二叉搜索树
中序遍历,根据前后大小判断是否交换了,记录被交换的两个数的节点,把要换的两个数重新赋值。
遍历时对比连续的两个数找到交换节点:
if pred and curr.val < pred.val:
y = curr
if x is None:
x = pred
else:
return
pred = curr
100. 相同的树
递归。先判断当前两结点值是否为空以及是否相等,然后检查左右子树 return self.isSameTree(p.right, q.right) and self.isSameTree(p.left, q.left)
或者迭代。出栈一对,入栈它们的左右子树。
101. 对称二叉树
和第 100 题是一样的,把两个树换成左右子树,return check(p.left, q.right) and check(p.right, q.left)。
102. 二叉树的层次遍历
① 迭代(BFS)。广度优先最常见的就是用队列,每次出队一个,处理,然后把它的左右子树入队。这题需要把每一层的放到一个列表中,这里对每层做单独循环,循环次数为循环前的 queue 的长度,每层循环结束把本次循环得到的列表 append 到结果中。
② 递归(DFS)。这里递归还是经典深度优先的先序遍历,helper(node, level)加了个参数 level 表示层数 ,每次 ans[level].append(tree.val),就可以把每个点的值放到结果 ans 的对应层中去。每一层开始时要在 ans 中初始化当前层的列表,通过对比 ans 已有的层数和当前所在层数实现,if len(ans) == level: ans.append([])。
103. 二叉树的锯齿形层次遍历
把第 102 题每次递归中 append 当前数的操作加个判断,如果是偶数行就 append 到这行最一个,奇数行就插在这行最前面。
if level % 2 == 0: ans[level].append(tree.val)
else: ans[level].insert(0,tree.val)
insert 时间复杂度 O(n),可以使用双端队列 deque,ans[level].appendleft(node.val) 代替 insert 操作
python 队列库 deque 使用需要导入 from collections import deque。deque([]) 新建队列,可使用 append、appendleft、pop、popleft 等操作。
104. 二叉树的最大深度
递归。每层最大深度等于左右子树中最大深度加一。若 node == None 则 return 0,否则 return max(self.maxDepth(node.left), self.maxDepth(node.right)) + 1。
105. 从前序与中序遍历序列构造二叉树
前序找根结点,中序去分左右。前序遍历的第一个元素为根节点,而在中序遍历,该根节点所在位置的左侧为左子树,右侧为右子树。同时,一个子树的下标都是连续的,下标区间长度都是固定的。
可以认为构建二叉树的方法为:找到各个子树的根节点 root、构建该根节点的左子树、构建该根节点的右子树。
如果 inorder 为 0 返回 None
preorder[0] 为当前子树根结点:root = TreeNode(preorder[0])
查找 root 在 inorder 中的下标(题目说没有重复元素):loc = inorder.index(preorder[0])
构建左子树:root.left = self.buildTree(preorder[1: loc +1], inorder[: loc ])
右子树:root.right = self.buildTree(preorder[loc +1: ], inorder[loc +1: ])
return root
106. 从中序与后序遍历序列构造二叉树
和第 105 题没什么大区别。
root.left = self.buildTree(inorder[: loc], postorder[: loc])
root.right = self.buildTree(inorder[loc+1: ], postorder[loc: -1])
107. 二叉树的层次遍历 II
参考第 101 题,把结果翻转或者结果用栈保存什么的。
108. 将有序数组转换为二叉搜索树
平衡二叉树说明要使左右子树高度尽可能相等,所以使中位数为根结点,列表左边的数都比它小,构成左子树,右边的数同理构成右子树。列表中点 mid 作为根结点,mid 左右分别为左右子树,递归。
root.left = self.sortedArrayToBST(nums[0:mid])
root.right = self.sortedArrayToBST(nums[mid+1:])
109. 有序链表转换二叉搜索树
思路和第 108 题一样,还是找中间值当做根结点,区别在于有序链表找中间值。
① 快慢指针。每次快指针移动两步,满指针移动一步,快指针到末尾时满指针指向中点。
② 转成数组。直接遍历一遍链表,把链表值放到列表里,然后和第 108 题一样。
③ 中序遍历模拟。升序链表顺序就是中序遍历时的顺序。假装已经建好一棵树了,然后使用中序遍历来遍历这个树,对每个结点的操作为开辟结点空间并赋值。
最开始遍历链表计算它的长度 size。可以认为这里的 size、 l 和 r 等计算只是为了判断递归出口。最外层函数 return helper(0 ,size) 即可。
def helper(l, r):
nonlocal head
if l >= r:
return None
mid = (l+r)//2
left = helper(l, mid)
root = TreeNode(head.val)
root.left = left
head = head.next
root.right = helper(mid+1, r)
return root
110. 平衡二叉树
自底向上计算。过程也就是后序遍历,先看左右子树再看中间。
def isBalanced(self, root: TreeNode) -> bool:
def helper(node):
if not node:
return 0, True
l_height, flag = helper(node.left)
if not flag:
return 0, False
r_height, flag = helper(node.right)
if not flag:
return 0, False
return 1 + max(l_height, r_height), (abs(l_height - r_height)<2)
return helper(root)[1]
111. 二叉树的最小深度
① 深度优先。如果左右子树都是空,返回 0;有一个空,返回不空的那个子树的最小深度,都不为空就返回左右子树两个最小高度中的最小值。
def helper(node):
if node == None:
return 0
l_height = helper(node.left)
r_height = helper(node.right)
if l_height!=0 and r_height!= 0:
return min(l_height, r_height) + 1
return max(l_height, r_height)+1
② 广度优先。DFS 一定会遍历完所有节点,而 BFS 会在第一个叶节点返回。
队列实现,把节点和深度的元组放到队列,每次出队一个节点 (node, depth),进队它的左右子树 (depth + 1, node.left) 和 (depth + 1, node.right) 。遇到第一个左右子树为空的结点就 return depth。
112. 路径总和
① 递归。sum -= root.val,如果当前 root 左右子树为空,return sum == 0。否则递归 return self.hasPathSum(root.left, sum) or self.hasPathSum(root.right, sum)。
② 迭代。初始化 stack = [(root, sum-root.val), ],while stack 循环,出栈一个, 检查是否左右为空且剩余和等于 0,不是就将右左子树入栈 stack.append((node.right, curr_sum - node.right.val))。相当于前序遍历。
113. 路径总和 II
和第 112 题一样,递归中加个参数 path,迭代中元组改为 (root, sum-root.val, path),每次 path = path + [node.val] 即可。若等于且左右子树为空,则 ans.append(path)。
114. 二叉树展开为链表
可以看到展开后从上往下看是原树先序遍历的顺序,所以就直接先序遍历迭代,把后一个节点 p 放到前个节点 pre 的 right 指针上就好了。每次指针修改完使 pre = p,pre.left = None。
115. 不同的子序列
二维动态规划。dp[i][j] 表示 s 的前 j 个字符序列包含多少 t 前 i 个字符序列。初始化 dp[0][1] = 1,其他为 0。两重循环 i, j 都从 1 开始:
(1) 当 t[i] == s[j] , dp[i][j] = dp[i][j-1] + dp[i-1][j-1],对应于两种情况,不选择当前字母和选择当前字母。
(2) 当 t[i] != s[j] , dp[i][j] = dp[i][j-1],不选择当前字符。
116. 填充每个节点的下一个右侧节点指针
① 层次遍历。层次遍历使用队列,按层次遍历顺序把前后结点用 next 相连。
② 利用上一层 next 指针。当一层已经建立好 next 指针,可以通过 next 指针访问同一层的所有节点。因此可以使用第 N 层的 next 指针,为第 N+1 层节点建立 next 指针。
建立一个 start 结点,表示当前层最左边的结点,每个层次循环结束,start = start.left 即可移动到下一个层次。
对于每个层次,使用 node(初始为 start)遍历,while node 不为空,它的左子树的 next 为它的右子树 node.left.next = node.right,它的右子树的 next 为它右边结点的左子树 if node.next: node.right.next = node.next.left,然后 node= node.next。
117. 填充每个节点的下一个右侧节点指针 II
和第 116 题差不多,层次遍历方法没变化,利用上一层 next 的方法需要记录下一层的最左结点。
118. 杨辉三角
。。。
119. 杨辉三角 II
比第 118 题可以省点空间,用一行存储即可,每行在末尾添加 1,从后往前覆盖自己 r[j] += r[j-1](只算首尾中间的那些元素)。
120. 三角形最小路径和
自顶向下。每格上左和上方格子的最小值加上本格的值覆盖本格 triangle[i][j] += min(triangle[i-1][j],triangle[i-1][j-1])。最左右的格分别等于自己加正上和自己加左上。返回最后一行的最小值。
自底向上。每格等于自己加正下和上右下的最小值 triangle[i][j] += min(triangle[i+1][j], triangle[i+1][j+1]),最后 return triangle[0][0]。优点是不用特殊处理最左右。
121. 买卖股票的最佳时机
一遍循环,low 记录见过的最低价,每次用当前价减去最低价,与见过的最大利润比较。profit = max(prices[i]-low, profit),low = min(low,prices[i])。
122. 买卖股票的最佳时机 II
① 峰谷法。一遍遍历,i 遇到升序 prices[i] < prices[i+1] 时买入,降序 prices[i] >= prices[i+1] 时卖出。
② 改进的峰谷法。遍历时,如果 prices[i] < prices[i+1],就 profit += prices[i+1] - prices[i]。
123. 买卖股票的最佳时机 III
动态规划。
初始化三维数组dp[len(prices)+1][k+1][2],dp[i][j][0] 维度表示第 i 天 第 k 次交易,最后一维表示是否持股,dp 值表示当前 profit。
初始化 dp[0][j][0] 和 dp[0][j][1] = 0(第0天利润为 0);dp[i][0][0] = 0(没交易过利润为0)和 dp[i][0][1] = float("-inf")(没交易不可能持股)。
则状态转移方程为:dp[i][j][0] = max(dp[i-1][j][0], dp[i-1][j][1] + prices[i]) 即前一天没持股与前一天持股今天卖掉中的最大值;p[i][j][1] = max(dp[i-1][j][1], dp[i-1][j-1][0] - prices[i]) 即前一天持股和前一天没持股今天买了的最大值。
然后循环对每个 i = [1, len(prices)],j = [1, 2],是否持股等于 0 和 1,更新 dp。最后返回 dp[len(prices)][2][0]。
124. 二叉树中的最大路径和
一定有一个结点 node,最大路径和 = node.val+ left 最大贡献 + right 最大贡献,也就是两个叶结点分别在它的左右子树中。
对每个结点 node 递归计算它的左右子树最大路径和 left_max = helper(node.left) 和 right_max = helper(node.right)。
(1) 把node当做最大路径的中心根结点(两个叶结点分别在它的左右子树中)。计算以当前节点为中心根结点的最大路径和 cur_sum = node.val + left_gain + right_gain,并检查是否更新目前的全局最大路径和。
(2) 把 node 当做最大路径的一个分支。返回当前结点与它的一个子树的最大路径和:return node.val + max(left_max, righ_max)。
125. 验证回文串
双指针或翻转对比,跳过非字母且非数字。
126. 单词接龙 II
BFS 用第 127 题的方法找到最短转换长度。DFS 查找所有最短距离的 beginWord 转化到 endWord 的序列。
BFS 遍历时记录每层搜索过的 word 的集合,在 DFS 时只从每层对应的这个集合中选,相当于剪枝。
127. 单词接龙
① BFS。建立 visited 集合,把走过的单词放进去,每次先判断下,减少重复。找邻接单词,可以对当前单词 curr 遍历每个字母 i,内层循环将 i 替换成其他字母,检查替换后是否在 wordSet 中,在就放到 nextWord 列表中,最开始把候选列表转为集合 wordSet = set(wordList) 用哈希加快 in 判断速度。
② 双向 BFS。一边从 beginWord 开始,另一边从 endWord 开始。每次从两边各扩展一个节点,当发现某一时刻两边都访问了某一顶点时就停止搜索。
128. 最长连续序列
字典 key 为数组中整数,value 为对应连续区间大小。
遍历时,对每个数查看是否在字典里,在就跳过 if x in dic: continue。
不在就取字典里它左右的数 left = dic.get(x-1, 0),right = dic.get(x+1, 0),计算连续区间大小 interval = left + right + 1。
检查更新最大连续序列 maxLen = max(interval, maxLen)。
更新字典中当前整数的值(因为可能是孤立点)dic[x] = interval ;更新连续区间最左右端点(每次只会用到区间端点的值,中间不用管)dic[x-left] = interval,dic[x+right] = interval。
129. 求根到叶子节点数字之和
DFS,helper(node, sums),每层 sums = sums * 10 + node.val,如果 node 左右子树为空就把 sums 加在结果中。
130. 被围绕的区域
图的 DFS 和 BFS,把和边界不连通的 O 换成 X。
一种思路是遍历时把所有与边界的 O 和它们连通的 O 都换成另一个字符比如 #,最后把所有 O 换成 X,再把 # 换成 O 即可。
以DFS 递归为例查找连通 O:超出边界、遇到 X、遇到 # 则 return,否则将本格修改为 #, 然后 dfs 上下左右的格子。