思路:
本题要找出树的最后一行的最左边的值。此时大家应该想起用层序遍历是非常简单的了,反而用递归的话会比较难一点。
1.递归法
如果使用递归法,如何判断是最后一行呢,其实就是深度最大的叶子节点一定是最后一行。
那么如何找最左边的呢?可以使用前序遍历(当然中序,后序都可以,因为本题没有 中间节点的处理逻辑,只要左优先就行),保证优先左边搜索,然后记录深度最大的叶子节点,此时就是树的最后一行最左边的值。
递归三部曲:
1.确定递归函数的参数和返回值:参数必须有要遍历的树的根节点,还有就是一个int型的变量用来记录最长深度。 这里就不需要返回值了,所以递归函数的返回类型为void。本题还需要类里的两个全局变量,maxLen用来记录最大深度,result记录最大深度最左节点的数值。
2.确定终止条件:当遇到叶子节点的时候,就需要统计一下最大的深度了,所以需要遇到叶子节点来更新最大深度。
3.确定单层递归的逻辑:在找最大深度的时候,递归的过程中依然要使用回溯
2. 迭代法
本题使用层序遍历再合适不过了,比递归要好理解得多!
只需要记录最后一行第一个节点的数值就可以了。
代码:
递归法
class Solution:
def findBottomLeftValue(self, root: Optional[TreeNode]) -> int:
self.max_depth = float('-inf') # 初始化一个类变量max_depth,用来记录目前找到的最深深度,初始化为负无穷大
self.result = None # 初始化一个类变量result,用来记录最深深度下最左边的节点的值,初始化为None
self.traversal(root, 0) # 调用traversal方法,开始从根节点遍历二叉树,并将深度初始化为0
return self.result # 返回result,即最底层最左边的节点的值
def traversal(self, node, depth): # 定义一个递归辅助方法traversal,输入是当前节点node和当前深度depth
if not node.left and not node.right: # 如果当前节点是叶子节点(即没有左子节点和右子节点)
if depth > self.max_depth: # 如果当前深度大于已知的最大深度
self.max_depth = depth # 更新最大深度
self.result = node.val # 更新最深深度下最左边的节点的值
return # 叶子节点处理完毕,返回
if node.left: # 如果当前节点有左子节点
depth += 1 # 增加深度
self.traversal(node.left, depth) # 递归调用traversal方法处理左子节点
depth -= 1 # 返回上一级节点时,将深度减一(回溯)
if node.right: # 如果当前节点有右子节点
depth += 1 # 增加深度
self.traversal(node.right, depth) # 递归调用traversal方法处理右子节点
depth -= 1 # 返回上一级节点时,将深度减一
return # 处理完当前节点的左右子节点,返回
时间复杂度:O(n),其中 n是二叉树的节点数目。需要遍历 n个节点。
空间复杂度:O(n)。递归栈需要占用 O(n)的空间。
补充:在这段代码中,traversal
方法对二叉树进行了类似前序遍历的遍历(即先处理当前节点,然后遍历左子树,最后遍历右子树),但有一个关键的区别:在遍历过程中,它记录了遍历到的最大深度,并据此更新最底层最左边节点的值。
迭代法
from collections import deque # 从collections模块导入deque,这是一个双端队列,可以高效地在队列的两端进行添加和删除操作。
class Solution:
def findBottomLeftValue(self, root: Optional[TreeNode]) -> int:
if root is None: # 判断根节点是否为空,如果为空,则返回0
return 0
queue = deque() # 初始化一个空的双端队列
queue.append(root) # 将根节点添加到队列中
result = 0 # 初始化result变量为0,用于存储最底层最左边节点的值
while queue: # 当队列不为空时,继续循环
size = len(queue) # 获取当前队列的长度,即当前层的节点数
for i in range(size): # 遍历当前层的所有节点
node = queue.popleft() # 从队列的左侧(前端)弹出一个节点
if i == 0: # 判断当前节点是否是当前层的第一个节点
result = node.val # 如果是当前层的第一个节点,更新result为当前节点的值
if node.left: # 判断当前节点是否有左子节点
queue.append(node.left) # 如果有左子节点,将左子节点添加到队列中
if node.right: # 判断当前节点是否有右子节点
queue.append(node.right) # 如果有右子节点,将右子节点添加到队列中
return result # 返回最底层最左边节点的值
时间复杂度:O(n),其中 n是二叉树的节点数目。
空间复杂度:O(n)。如果二叉树是满完全二叉树,那么队列最多保存n/2个节点。
思路:
可以使用深度优先遍历的方式(本题前中后序都可以,无所谓,因为中节点也没有处理逻辑)来遍历二叉树
1.确定递归函数的参数和返回类型:参数:需要二叉树的根节点,还需要一个计数器,这个计数器用来计算二叉树的一条边之和是否正好是目标和,计数器为int型。递归函数需要返回值,可以用bool类型表示
2.确定终止条件:不要去累加然后判断是否等于目标和,那么代码比较麻烦,可以用递减,让计数器count初始为目标和,然后每次减去遍历路径节点上的数值。如果最后count == 0,同时到了叶子节点的话,说明找到了目标和。如果遍历到了叶子节点,count不为0,就是没找到。
3.确定单层递归的逻辑:因为终止条件是判断叶子节点,所以递归的过程中就不要让空节点进入递归了。递归函数是有返回值的,如果递归函数返回true,说明找到了合适的路径,应该立刻返回。
思路:
113.路径总和ii要遍历整个树,找到所有路径,所以递归函数不要返回值!
代码:
路径总和
class Solution:
def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
if root is None: # 如果根节点为空,说明二叉树为空,返回False
return False
return self.traversal(root, targetSum - root.val) # 调用traversal方法,开始从根节点遍历二叉树,传入targetSum - root.val作为count的初始值,即减去根节点的值
def traversal(self, cur: TreeNode, count: int) -> bool: # 定义traversal方法,它接收一个当前节点cur和一个计数器count作为参数,返回一个布尔值
if not cur.left and not cur.right and count == 0: # 如果当前节点是叶子节点(即没有左子节点和右子节点),并且count为0,说明从根节点到该叶子节点的路径上节点值的和等于目标和targetSum
return True # 返回True,表示找到了符合条件的路径
if not cur.left and not cur.right: # 如果当前节点是叶子节点,但是count不为0,说明该路径不符合条件,返回False
return False
if cur.left: # 如果当前节点有左子节点
count -= cur.left.val # 更新count,减去左子节点的值
if self.traversal(cur.left, count): # 递归调用traversal方法,继续遍历左子树
return True # 如果左子树中存在符合条件的路径,则返回True
count += cur.left.val # 如果左子树中不存在符合条件的路径,回溯到当前节点,恢复count的值
if cur.right: # 如果当前节点有右子节点
count -= cur.right.val # 更新count,减去右子节点的值
if self.traversal(cur.right, count): # 递归调用traversal方法,继续遍历右子树
return True # 如果右子树中存在符合条件的路径,则返回True
count += cur.right.val # 如果右子树中不存在符合条件的路径,回溯到当前节点,恢复count的值
return False # 如果当前节点的左子树和右子树都不存在符合条件的路径,则返回False
时间复杂度 O(n)
空间复杂度 O(n)
补充:
这段代码中的遍历方式类似于前序遍历,但带有额外的逻辑来处理路径和。
前序遍历的标准步骤是“根-左-右”,即先访问根节点,然后遍历左子树,最后遍历右子树。在这段代码中,当遍历到一个节点时,首先会检查当前节点是否是叶子节点并且路径和是否等于目标 sum
。如果不是叶子节点,则会递归地先遍历左子树,再遍历右子树。
路径总和II
class Solution:
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
path=[] # 初始化一个空列表用于存储路径上的节点值
result=[] # 初始化一个空列表用于存储所有满足条件的路径
if not root: # 如果根节点为空,则没有路径可走,直接返回空的结果列表
return result
path.append(root.val) # 将根节点的值加入当前路径
self.traversal(root, targetSum - root.val, path, result) # 调用traversal方法递归遍历树,并将目标和减去根节点的值作为新的目标和传入
return result # 返回所有满足条件的路径列表
def traversal(self, cur, count, path, result): # 输入参数为当前节点cur、目标和count、当前路径path和结果列表result
if not cur.left and not cur.right and count == 0: # 如果当前节点是叶子节点,且路径上的节点值之和等于目标和
result.append(path[:]) # 将当前路径的副本添加到结果列表中
return # 当前路径已经处理完毕,直接返回
if not cur.left and not cur.right: # 如果当前节点是叶子节点,但路径上的节点值之和不等于目标和
return # 则说明这条路径不满足条件,直接返回
if cur.left: # 如果当前节点有左子节点
path.append(cur.left.val) # 将左子节点的值加入当前路径
count -= cur.left.val # 更新目标和,减去左子节点的值
self.traversal(cur.left, count, path, result) # 递归遍历左子树
count += cur.left.val # 回溯,更新目标和,加上左子节点的值,恢复之前的状态
path.pop() # 回溯,将左子节点的值从路径中移除
if cur.right: # 如果当前节点有右子节点
path.append(cur.right.val) # 将右子节点的值加入当前路径
count -= cur.right.val # 更新目标和,减去右子节点的值
self.traversal(cur.right, count, path, result) # 递归遍历右子树
count += cur.right.val # 回溯,更新目标和,加上右子节点的值,恢复之前的状态
path.pop() # 回溯,将右子节点的值从路径中移除
时间复杂度:O(N^2)
空间复杂度:O(N)
补充:
上述代码中的result.append(path[:])为什么不能写成result.append(path)?
在Python中,path[:]
是一个切片操作,它创建了path
列表的一个浅拷贝(shallow copy)。
具体来说:
path
是一个列表。path[:]
表示取path
从开始到结束的所有元素- 这个操作返回一个新的列表,它包含与
path
相同的元素,但是是两个独立的对象。对path[:]
的修改不会影响原始的path
列表,反之亦然。
在你提供的代码中,result.append(path[:])
这行代码的目的是将当前路径path
的一个拷贝添加到结果列表result
中,而不是直接添加path
的引用。这样做是为了避免在后续的递归调用中修改path
时影响到已经添加到result
中的路径。如果不使用path[:]
而是直接result.append(path)
,那么result
中存储的将是path
的引用,而不是一个独立的副本。当path
在后续的递归调用中被修改时,result
中存储的所有路径都会受到影响,因为它们实际上指向的是同一个列表对象。
使用path[:]
确保了result
中存储的是每个路径在找到时的独立状态,而不是它们被最后修改时的状态。
思路:
首先回忆一下如何根据两个顺序构造一个唯一的二叉树,相信理论知识大家应该都清楚,就是以 后序数组的最后一个元素为切割点,先切中序数组,根据中序数组,反过来再切后序数组。一层一层切下去,每次后序数组最后一个元素就是节点元素。
说到一层一层切割,就应该想到了递归。
来看一下一共分几步:
-
第一步:如果数组大小为零的话,说明是空节点了。
-
第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。
-
第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点
-
第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
-
第五步:切割后序数组,切成后序左数组和后序右数组
-
第六步:递归处理左区间和右区间
本题和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]:
# 第一步: 特殊情况讨论。如果后序遍历的数组为空,说明树为空,直接返回None。
if not postorder:
return None
# 第二步: 找到根节点。在后序遍历中,最后一个元素总是当前(子)树的根节点值。因此,我们创建了一个新的TreeNode对象作为当前(子)树的根节点。
root_val = postorder[-1]
root = TreeNode(root_val)
# 第三步: 找到中序遍历中的根节点位置。使用index方法找到根节点值在中序遍历数组中的(下标)位置。
separator_idx = inorder.index(root_val)
# 第四步: 切割中序遍历数组。根据根节点的位置,将中序遍历数组切割为左子树和右子树的中序遍历数组。
inorder_left = inorder[:separator_idx]
inorder_right = inorder[separator_idx + 1:]
# 第五步: 切割后序遍历数组。后序遍历数组也需要切割为左子树和右子树的后序遍历数组。注意,这里不包括后序遍历的最后一个元素(即当前子树的根节点),因为该元素已经在第二步中使用了。
postorder_left = postorder[:len(inorder_left)]
postorder_right = postorder[len(inorder_left): len(postorder) - 1]
# 第六步: 递归构建左右子树。递归地调用buildTree方法来构建当前根节点的左子树和右子树。
root.left = self.buildTree(inorder_left, postorder_left) # 传入左中序,左后序
root.right = self.buildTree(inorder_right, postorder_right) # 传入右中序,右后序
# 第七步: 返回答案。返回当前子树的根节点。
return root
时间复杂度:O(n)
空间复杂度:O(n)
从前序与中序遍历序列构造二叉树
# 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数组. 得到inorder数组的左,右半边.
inorder_left = inorder[:separator_idx]
inorder_right = inorder[separator_idx + 1:]
# 第五步: 切割preorder数组. 得到preorder数组的左,右半边.
# ⭐️ 重点1: 中序数组大小一定跟前序数组大小是相同的.
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