513. 找树左下角的值
难度:☆2
本题要求是最后一行,然后是最左边的值。思维误区:(1)一棵二叉树左下角的值不一定是左孩子,也可能是右孩子。(2)一直向左遍历到的最后一个节点,未必是在最后一行。
a. 迭代法:层序遍历
最直观的想法是层序遍历,因为很容易得到最下面一层的节点,并返回最左边的值。用一个变量保存每一层最左边的值,这样变量最终的值就是左下角的值。
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def findBottomLeftValue(self, root: Optional[TreeNode]) -> int:
queue = deque([root])
while queue:
size = len(queue)
for i in range(size):
cur = queue.popleft()
if i == 0:
bottomLeft = cur.val
if cur.left:
queue.append(cur.left)
if cur.right:
queue.append(cur.right)
return bottomLeft
另一种巧妙的实现是:在遍历一个节点时,先把它的非空右孩子放入队列,再把它的非空左孩子放入队列,这样保证从右到左遍历每一层的节点。广度优先搜索遍历的最后一个节点的值,就是最底层最左边节点的值。
class Solution:
def findBottomLeftValue(self, root: Optional[TreeNode]) -> int:
queue = deque([root])
while queue:
cur = queue.popleft()
bottomLeft = cur.val # 最后一个节点的值即左下角的值
if cur.right: # 先右
queue.append(cur.right)
if cur.left: # 后左
queue.append(cur.left)
return bottomLeft
b. 递归法+回溯
(1)如何判断是最后一行呢?深度最大的叶子节点一定在最后一行。(2)如何找最左边的呢?可以使用前序遍历(前中后序都可以,本题没有中间节点的处理逻辑),保证优先搜索左边,然后记录深度最大的叶子节点,就是树的最后一行最左边的值。
维护一个全局变量记录二叉树的最大深度,初始值为 0。维护一个全局变量记录左下角的值,初始值为根节点的值。注意递归中的回溯如何处理。
class Solution:
def findBottomLeftValue(self, root: Optional[TreeNode]) -> int:
max_depth = 0
bottom_left = root.val
def traverse(node, cur_depth):
nonlocal max_depth, bottom_left
if not node.left and not node.right: # 叶子节点
if cur_depth > max_depth:
max_depth = cur_depth
bottom_left = node.val
if node.left:
cur_depth += 1
traverse(node.left, cur_depth)
cur_depth -= 1 # 回溯
if node.right:
cur_depth += 1
traverse(node.right, cur_depth)
cur_depth -= 1 # 回溯
traverse(root, 0)
return bottom_left
隐藏回溯如下,因为没有改变 cur_depth
的值:
if node.left:
traverse(node.left, cur_depth + 1) # 隐藏回溯
if node.right:
traverse(node.right, cur_depth + 1) # 隐藏回溯
112. 路径总和
难度:☆4
a. 递归法+回溯
本题如果熟练掌握递归法,应当不难,不过还是需要参考题解。
(1)递归函数的参数和返回类型:注意 target
是 targetSum
减去遍历节点值剩余的值,和 0 比较。本题递归函数有返回值(对比 113 题),返回类型是布尔类型,遇到符合条件的路径及时返回,以免遍历整棵树。(2)终止条件:遍历到了叶子节点,判断 target
的值。(3)单层递归的逻辑:向左向右递归,如果递归函数返回 True
,说明找到了合适的路径,应该立刻返回 True
。以下代码把回溯的过程体现出来了。
class Solution:
def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
if not root:
return False
return self.findPath(root, targetSum-root.val)
def findPath(self, node: Optional[TreeNode], target: int) -> bool:
if not node.left and not node.right and target == 0:
return True # 叶子节点,存在路径
if not node.left and not node.right and target != 0:
return False # 叶子节点,不存在路径
if node.left: # 向左递归
target -= node.left.val
if self.findPath(node.left, target):
return True # 存在路径,向上返回
target += node.left.val # 回溯
if node.right: # 向右递归
target -= node.right.val
if self.findPath(node.right, target):
return True # 存在路径,向上返回
target += node.right.val # 回溯
return False
隐藏回溯如下:
if node.left: # 向左递归
if self.findPath(node.left, target-node.left.val): # 递归隐藏回溯
return True # 存在路径,向上返回
if node.right: # 向右递归
if self.findPath(node.right, target-node.right.val): # 递归隐藏回溯
return True # 存在路径,向上返回
极简版隐藏回溯:
class Solution:
def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
if not root:
return False
if not root.left and not root.right and targetSum == root.val:
return True # 叶子节点,存在路径
return self.hasPathSum(root.left, targetSum-root.val) or self.hasPathSum(root.right, targetSum-root.val)
b. 迭代法:前序遍历
使用栈模拟递归实现前序遍历(中右左),此时栈里一个元素不仅要记录该节点,还要记录从头节点到该节点的路径数值总和。用 pair 结构来存放栈里的元素:(当前节点,路径数值)。
class Solution:
def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
if not root:
return False
stack = [] # [(当前节点,路径数值), ...]
stack.append((root, root.val))
while stack:
cur_node, path_sum = stack.pop()
if not cur_node.left and not cur_node.right and path_sum == targetSum:
return True # 中
if cur_node.right: # 右
stack.append((cur_node.right, path_sum + cur_node.right.val))
if cur_node.left: # 左
stack.append((cur_node.left, path_sum + cur_node.left.val))
return False
113. 路径总和 II
难度:☆4
a. 递归法+回溯
与 112 题的区别在于:要用列表记录所有符合要求的路径。本题递归函数不需要返回值(对比 112 题)。
在调用递归函数中隐藏 target
的回溯,保留 path
的回溯:
class Solution:
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
def findPath(node: Optional[TreeNode], target: int) -> None:
if not node.left and not node.right: # 叶子节点
if target == 0: # 存在路径
result.append(path[:]) # 硬拷贝复制path列表
return
if node.left: # 向左递归
path.append(node.left.val)
findPath(node.left, target-node.left.val) # 递归隐藏回溯
path.pop() # 回溯
if node.right: # 向右递归
path.append(node.right.val)
findPath(node.right, target-node.right.val) # 递归隐藏回溯
path.pop() # 回溯
return
result = []
if not root:
return result
path = [root.val]
findPath(root, targetSum-root.val)
return result
保留所有回溯:
if node.left: # 向左递归
path.append(node.left.val)
target -= node.left.val
findPath(node.left, target) # 递归
target += node.left.val # 回溯
path.pop() # 回溯
if node.right: # 向右递归
path.append(node.right.val)
target -= node.right.val
findPath(node.right, target) # 递归
target += node.right.val # 回溯
path.pop() # 回溯
还有一种官方解法,没有判断当前节点是否有左右孩子,隐藏了部分回溯:
class Solution:
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
result = []
path = []
def findPath(node: Optional[TreeNode], target: int) -> None:
if not node:
return
path.append(node.val)
target -= node.val # target隐藏回溯
if not node.left and not node.right and target == 0: # 叶子节点且找到路径
result.append(path[:])
findPath(node.left, target)
findPath(node.right, target)
path.pop() # path回溯
findPath(root, targetSum)
return result
106. 从中序与后序遍历序列构造二叉树
难度:☆4
本题开始构造二叉树了。
题意说明 inorder 和 postorder 都由不同的值组成,否则无法找到唯一的中间节点来划分。
a. 递归法
思路:从后序数组中获知中间节点,再用中间节点去中序数组当中划分左右子树两部分,再去后序数组中划分对应的左右两部分,并找左右子树的中间节点,依次类推。
划分左右数组的时候,注意确定切割的标准,是左闭右开,还是左闭右闭,这个就是不变量,要在递归中保持不变量。
class Solution:
def buildTree(self, inorder: List[int], postorder: List[int]) -> Optional[TreeNode]:
# 第一步:特殊情况讨论——树为空 (递归终止条件)
if not postorder:
return None
# 第二步:后序遍历的最后一个就是当前的中间节点
root_val = postorder[-1]
root = TreeNode(root_val)
# 第三步:找切割点
separator_idx = inorder.index(root_val)
# 第四步:切割inorder数组,得到inorder数组的左、右半边
inorder_left = inorder[:separator_idx]
inorder_right = inorder[separator_idx + 1:]
# 第五步:切割postorder数组,得到postorder数组的左、右半边
# 重点:中序数组大小一定跟后序数组大小是相同的
postorder_left = postorder[:len(inorder_left)]
postorder_right = postorder[len(inorder_left):-1]
# 第六步:递归
root.left = self.buildTree(inorder_left, postorder_left)
root.right = self.buildTree(inorder_right, postorder_right)
return root
在第六步递归之前,通过打印日志,方便调试:
# print log
print('------------')
print('inorder_left:')
print(inorder_left)
print('inorder_right:')
print(inorder_right)
print('postorder_left:')
print(postorder_left)
print('postorder_right:')
print(postorder_right)
105. 从前序与中序遍历序列构造二叉树
难度:☆4
a. 递归法
思路同 106 题。
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
# 第一步:特殊情况讨论——树为空 (递归终止条件)
if not preorder:
return None
# 第二步:前序遍历的第一个就是当前的中间节点
root_val = preorder[0]
root = TreeNode(root_val)
# 第三步:找切割点
separator_idx = inorder.index(root_val)
# 第四步:切割inorder数组,得到inorder数组的左、右半边
inorder_left = inorder[:separator_idx]
inorder_right = inorder[separator_idx + 1:]
# 第五步:切割preorder数组,得到preorder数组的左、右半边
# 重点:中序数组大小一定跟前序数组大小是相同的
preorder_left = preorder[1:1 + len(inorder_left)]
preorder_right = preorder[1 + len(inorder_left):]
# 第六步:递归
root.left = self.buildTree(preorder_left, inorder_left)
root.right = self.buildTree(preorder_right, inorder_right)
return root
拓展:从前序与后序遍历序列不能唯一确定一棵二叉树,因为没有中序遍历,无法确定左右部分,也就无法分割。