算法训练营day16 513.找树左下角的值、112. 路径总和、106.从中序与后序遍历序列构造二叉树
二叉树章节的第四篇博客!今天的题目相对都有些困难,加油!我更多的说明都放在了注释里,希望能给同样刷题的大家一点帮助
513.找树左下角的值
层序遍历
层序遍历对于这道题而言是很简单的(可以独立完成),整体思路是:每一层放在一个数组中(or判断深度是否大于最大深度,大于则覆盖),最后输出最后一层的第一个数即可,因为层序遍历不涉及回溯,所以理解和实现起来相对简单
# 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:if not root:return Nonequeue = collections.deque([root])result = []while queue:path = []size = len(queue)for i in range(size):node = queue.popleft()path.append(node.val)if node.left:queue.append(node.left)if node.right:queue.append(node.right)result.append(path)out1= result[-1]out2 = out1[0]return out2'''
# 可以对比一下和答案的写法, 更简洁更节省空间
# 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
from collections import deque
class Solution:def findBottomLeftValue(self, root):if root is None:return 0queue = deque()queue.append(root)result = 0while queue:size = len(queue)for i in range(size):node = queue.popleft()if i == 0:result = node.val # 这个地方很关键if node.left:queue.append(node.left)if node.right:queue.append(node.right)return result
'''
递归
这个题目要注意:深度最大的叶子节点一定是最后一行。
递归不太好写,所以还是要多写写,主要还是覆盖的逻辑
# 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:self.max_depth = float('-inf')self.result = Noneself.traversal(root, 0)return self.resultdef traversal(self, node, depth):# 确认需要递归的参数if not node.left and not node.right: # 确认返回逻辑, 判断深度, 递归节点值# 因为同深度不会覆盖, 所以值一直是最左侧节点值if depth > self.max_depth:self.max_depth = depthself.result = node.val # 更新# 这些变量属于类实例,在整个类的生命周期内保持状态# 递归函数 self.traversal 可以直接访问和修改这些属性# 若写为局部变量 max_depth 和 result, 递归函数无法直接访问if node.left:depth += 1self.traversal(node.left, depth)depth -= 1if node.right:depth += 1self.traversal(node.right, depth)depth -= 1
112. 路径总和
递归法
这个题目需要增加对于函数返回参数的掌握
1.确定递归函数的参数和返回类型
参数:需要二叉树的根节点,还需要一个计数器,这个计数器用来计算二叉树的一条路径之和是否正好是目标和(int)
再来看返回值,递归函数什么时候需要返回值?什么时候不需要返回值?这里总结如下三点:
-
如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。
-
如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。
-
如果只需搜索其中一条符合条件的路径,那么递归就需要返回值,因为遇到符合条件的路径了就要及时返回——这种情况下(例如本题)并不要遍历整棵树,可以用bool类型表示。
2.确定终止条件
不要去累加然后判断是否等于目标和,那么代码比较麻烦,可以用递减,让计数器count初始为目标和,然后每次减去遍历路径节点上的数值——如果最后count == 0,同时到了叶子节点的话,说明找到了目标和。如果遍历到了叶子节点,count不为0,就是没找到。
递归终止条件代码如下:
# 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 hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:if root is None:return Falsereturn self.traversal(root, targetSum - root.val)def traversal(self, cur: TreeNode, count: int) -> bool:if not cur.left and not cur.right and count == 0: # 判断成功return True if not cur.left and not cur.right: # 叶子节点不正确直接退出return Falseif cur.left: # 这里需要bool判断count -= cur.left.valif self.traversal(cur.left, count):# 作为判断结果return Truecount += cur.left.valif cur.right:count -= cur.right.valif self.traversal(cur.right, count):return Truecount += cur.right.valreturn False
迭代法
这个迭代法如果之前跟着做过题目的话,还是比较简单可以举一反三出来的,但是需要优化一点具体逻辑——具体在哪里大家可以看下面两种代码的比较(根据实际情况使用列表),同时这个代码的关键点在于——回溯的位置,这个一定要搞清楚
# 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 hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:# 更好的迭代法, 因为需要记录每一个节点的sum, 所以建议列表中元素有所改变# pair<节点指针, 路径总和># 使用遍历方法, 遍历到直接退出if not root:return Falsest = [(root, root.val)] # 注意这个位置while st:node, path_sum = st.pop()if not node.left and not node.right and path_sum == targetSum:return Trueif node.right:st.append((node.right, path_sum + node.right.val))# 隐藏的回溯if node.left:st.append((node.left, path_sum + node.left.val))# 注意这个数据结构是栈, 前序遍历return False# 使用记录路径的方式, 但是这种方式较麻烦'''result = []path = []# 需要增加空节点判断if not root:return Falseself.traversal(root, path, result)if targetSum in result:return True else:return Falsedef traversal(self, cur, path, result): # 节点值、路径存储、和存储path.append(cur.val)sum = 0if not cur.left and not cur.right:size = len(path)for i in range(size):sum += path[i] # 计算次数较多result.append(sum)return # 没有做判断, 所以会遍历整个树# path.pop() 重大错误!回溯位置究竟在哪里if cur.left:self.traversal(cur.left, path, result)path.pop()if cur.right:self.traversal(cur.right, path, result)path.pop()'''
补充练习
113. 路径总和 II - 力扣(LeetCode)
递归法
113.路径总和ii要遍历整个树,找到所有路径,所以递归函数不要返回值!直接return 空就好了,参考上面对于112的返回值分析
再次说道python中的引用传递和赋值传递,虽然和这个题关系不是很大,但是对于理解递归,需要心里有数
- 不可变对象(如 int、str、tuple)赋值操作会生成新对象:
- 可变对象(如 list、dict、set)赋值操作传递的是引用:
操作 | 含义 | 结果影响 |
---|---|---|
self.result.append(self.path) | 直接添加列表引用:将 self.path 列表的内存地址添加到 self.result 中。 | 后续对 self.path 的任何修改(如 pop() )都会反映到 self.result 中。 |
self.result.append(self.path[:]) | 添加列表副本:创建 self.path 的一个浅拷贝,并添加到 self.result 中。 | self.result 中保存的是独立的列表,不受 self.path 后续修改的影响。 |
# 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 pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:self.result = []self.path = []if not root:return self.resultself.path.append(root.val)# 提前加入self.traversal(root, targetSum - root.val) # 使用减法return self.resultdef traversal(self, cur, count):if not cur.left and not cur.right and count == 0:self.result.append(self.path[:]) # 注意这里是[:] 很重要!return # 注意这里不需要返回值 很重要!if not cur.left and not cur.right:return # 直接退出, 和上面一样if cur.left: # 直接判断子节点self.path.append(cur.left.val)count -= cur.left.val # 递归进入下一层self.traversal(cur.left, count)count += cur.left.valself.path.pop() # 回溯, 使用栈的数据结构if cur.right:self.path.append(cur.right.val)count -= cur.right.valself.traversal(cur.right, count)count += cur.right.valself.path.pop()return # 单次递归结束
迭代法
其实差别不是很大
# 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 pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:if not root:return []stack = [(root, [root.val])] # 调整栈中数据结构res = []while stack:node, path = stack.pop()if not node.left and not node.right and sum(path) == targetSum: # 使用sum函数res.append(path)if node.right:stack.append((node.right, path + [node.right.val]))# 连接列表, 存在回溯if node.left:stack.append((node.left, path + [node.left.val]))return res
106.从中序与后序遍历序列构造二叉树(重点题目待续)
给定两个遍历的数组,然后根据数组返回二叉树,第一遍做看起来感觉完全没有思路,直接给出解题方法:后序数组的最后一个元素为切割点,先切中序数组,根据中序数组,反过来再切后序数组。一层一层切下去,每次后序数组最后一个元素就是节点元素。配合下面的插图更好理解:
注释:引用自《代码随想录》
细分步骤为:
-
第一步:如果数组大小为零的话,说明是空节点了。
-
第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。
-
第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点
-
第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
-
第五步:切割后序数组,切成后序左数组和后序右数组
-
第六步:递归处理左区间和右区间
跟着写一遍代码:
# 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 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数组# Python的列表切片操作中, 区间是左闭右开的# 也就是包含起始索引, 但不包含结束索引。inorder_left = inorder[:separator_idx]inorder_right = inorder[separator_idx + 1:]# 切割postorder数组, 得到数组的左右两边postorder_left = postorder[:len(inorder_left)]postorder_right = postorder[len(inorder_left): len(postorder) - 1]# 不包含最后数值 - 1 ,索引从0开始 - 1# 递归root.left = self.buildTree(inorder_left, postorder_left)root.right = self.buildTree(inorder_left, postorder_right)return root
补充练习
105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)
具体思路和106一样
# 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 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_left = inorder[:separator_idx]inorder_right = inorder[separator_idx + 1 :]# 继续切割pre数组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
思考补充
前序和中序可以唯一确定一棵二叉树。
后序和中序可以唯一确定一棵二叉树。
那么前序和后序可不可以唯一确定一棵二叉树呢?
前序和后序不能唯一确定一棵二叉树!因为没有中序遍历无法确定左右部分,也就是无法分割。
注释:引用自《代码随想录》
tree1 的前序遍历是[1 2 3], 后序遍历是[3 2 1]。
tree2 的前序遍历是[1 2 3], 后序遍历是[3 2 1]。
那么tree1 和 tree2 的前序和后序完全相同,这是一棵树么,很明显是两棵树!
所以前序和后序不能唯一确定一棵二叉树!