leetcode思路简述(91-130)

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 上下左右的格子。

 

posted @ 2020-04-01 01:18  肃木易  阅读(209)  评论(0编辑  收藏  举报