路径总和
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 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:
self.ans = []
def pathSum(root, path):
if root is None:
return
print(root.val)
if root.left is None and root.right is None:
self.ans.append(sum(path)+root.val)
return
pathSum(root.left, path+[root.val])
pathSum(root.right, path+[root.val])
pathSum(root, [])
print(self.ans)
for s in self.ans:
if s == targetSum:
return True
return False
终止条件:
- 叶子节点肯定是一直终止条件,当遍历到叶子节点的时候拿到了所有的路径值,也就可以返回了
root.left is None and root.right is None
- 但是还有一种情况是节点是None,例如一个节点的左节点是None,右节点不为None,那么递归遍历左节点的时候也要停止递归并且返回
二叉树的停止条件主要就是叶子节点和None两种情况
可以引入一个外部变量来存储中间的计算结果
# 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:
self.ans = False
def pathSum(root, path):
if root is None:
return
if root.left is None and root.right is None:
if sum(path) + root.val == targetSum:
self.ans = True
return
pathSum(root.left, path+[root.val])
pathSum(root.right, path+[root.val])
pathSum(root,[])
return self.ans
直接根据子问题还原出原问题,子问题:
- 左子树是否存在某条路径的和满足target-root.val
- 右子树是否存在某条路径的和满足target-root.val
原问题:如果左子树存在或者右子树存在,那么就存在
终止条件:
- 子节点:如果到了子节点,那么只需要判断子节点与targetSum是否相等即可,也可以认为当前就只有一个节点
- 节点为None,如果节点为None的话,那肯定就是不满足情况,返回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:
if root is None:
return False
if root.left is None and root.right is None:
return root.val == targetSum
l = self.hasPathSum(root.left, targetSum-root.val)
r = self.hasPathSum(root.right, targetSum-root.val)
return l or r
路径总和 II
给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
要把所有搜索路径存储下来,那么必然是把之前的节点都记录下来并且传递下去,这样后续的节点才可以拿到之前的节点
路径总和 III
给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
对于不是必须经过根节点的问题,可以将每个节点看作是根节点来搜索原问题。
# 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) -> int:
self.ans = []
# 求一个二叉树中存在路径和为targetSum的路径
def everyNodePathSum(root, targetSum, path):
if root is None:
return
if root.left is None and root.right is None:
if root.val == targetSum:
self.ans.append(path + [root.val])
return
everyNodePathSum(root.left, targetSum - root.val, path + [root.val])
everyNodePathSum(root.right, targetSum - root.val,path + [root.val])
# 遍历一个二叉树,求每个节点下存在路径为targetSum的路径
def dfs(root):
if root is None:
return
everyNodePathSum(root,targetSum,[])
dfs(root.left)
dfs(root.right)
dfs(root)
print(self.ans)
终止条件发生了改变,不一定是到叶子节点才终止,只要在遍历的过程中存在和为targetSum的路径就存储下来
发现了一个问题,叶子节点并不是终止条件,只能说是一个判断条件,当到达叶子节点时候判断是否需要将路径加进来
[1,-1,[2,-2],[1,-1,2,-2]
都是满足targetSum的路径,因此即使中间结果满足了targetSum也不能直接返回,还需要接着往下遍历
# 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) -> int:
self.ans = []
def everyNodePathSum(root, targetSum, path):
if root is None:
return
# 不需要再叶子节点判断,每个节点都可以判断
# 并且删除return,因为一个路径可能存在多种解
if root.val == targetSum:
self.ans.append(path + [root.val])
everyNodePathSum(root.left, targetSum - root.val, path + [root.val])
everyNodePathSum(root.right, targetSum - root.val,path + [root.val])
def dfs(root):
if root is None:
return
everyNodePathSum(root,targetSum,[])
dfs(root.left)
dfs(root.right)
dfs(root)
print(self.ans)
做一个小修改来满足题解
# 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) -> int:
self.ans = 0
def everyNodePathSum(root, targetSum, path):
if root is None:
return
if root.val == targetSum:
self.ans += 1
everyNodePathSum(root.left, targetSum - root.val, path + [root.val])
everyNodePathSum(root.right, targetSum - root.val,path + [root.val])
def dfs(root):
if root is None:
return
everyNodePathSum(root,targetSum,[])
dfs(root.left)
dfs(root.right)
dfs(root)
return self.ans
二叉树的最大深度
二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。
根节点到叶子节点,我们同样可以把路径记录下来,当到达叶子节点的时候直接统计就可以
# 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 maxDepth(self, root: Optional[TreeNode]) -> int:
self.ans = []
def getPath(root, path):
if root is None:
return
if root.left is None and root.right is None:
self.ans.append(path + [root.val])
# 将当前节点添加到path中,并传递给左子树
getPath(root.left, path + [root.val])
# 将当前节点添加到path中,并传递给左子树
getPath(root.right, path + [root.val])
getPath(root, [])
if len(self.ans) == 0:
return 0
return max(map(len, self.ans))
我们是求最大路径,其实并不需要将路径记录下来,只需要将路过的节点数记录下来就可以了
# 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 maxDepth(self, root: Optional[TreeNode]) -> int:
self.ans = 0
def getPath(root, depth):
if root is None:
return
if root.left is None and root.right is None:
self.ans = max(self.ans, depth+1)
# 之前的个数+1就是当前的深度,然后传递下去,左子树就是知道之前路过了几个节点
getPath(root.left, depth+1)
# 之前的个数+1就是当前的深度,然后传递下去,右子树就是知道之前路过了几个节点
getPath(root.right, depth+1)
getPath(root, 0)
return self.ans
这个问题也可以直接考虑子问题
子问题:左子树的最大深度和右子树的最大深度
还原原问题:max(左子树最大深度,右子树最大)+1就是当前节点的最大深度
通常考虑后序遍历,
# 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 maxDepth(self, root: Optional[TreeNode]) -> int:
if root is None:
return 0
l = self.maxDepth(root.left)
r = self.maxDepth(root.right)
return max(l,r)+1
二叉树的直径
二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。
最开始的想法是假如求出了左子树直径和右子树的直径,那么当前节点的直径就是左子树直径+右子树直径+1。但是会发现左子树的直径和右子树的直径并不能直接拼接成根节点的直径。
左子树的直径是[4,3,2,6],右子树的直径是[8,7,9],预期根节点的直径是[4,3,2,6]+[1]+[8,7,9],而根节点的实际直径是[4,3,2]+[1]+[7,9]。也就是说子问题的解无法合并成原问题,我们这种划分就是不对的。
观察后发现,根节点的直径是左子树的深度+1+右子树的深度。不只是根节点,每个节点的直径都是左子树的深度+1+右子树的深度。也就是说我们只要遍历每个节点的左右子树深度就可以了
# 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 diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int:
self.ans = 0
def depth(root):
if root is None:
return 0
l = depth(root.left)
r = depth(root.right)
d = max(l,r) + 1
self.ans = max(self.ans, l+r)
return d
depth(root)
return self.ans
这样左子树的最大深度一定是左节点一直往下到叶子节点的一条路径/,右子树的最大深度也是右节点一直往下到一条路径\,(不会出现/\这样的路径),然后加上根节点就是直径了。
二叉树中的最大路径和
路径和 是路径中各节点值的总和
这个问题也很有意思,一个简单的想法就是求出左子树的最大路径和,求出右子树的最大路径和,那么根节点的最大路径和就是root.val + max(0,左子树的路径和) + max(0,右子树的路径和),对左右子树取max的原因是路径和可以为负数,当为负数的时候就不要加在根节点上。
同样最大路径和不一定过根节点,也就是要遍历所有的节点,可以用全局变量存储最大值
ans = float('-inf')
def dfs(root):
if root is None:
return 0
l = max(0, dfs(root.left))
r = max(0, dfs(root.right))
v = l + r + root.val
ans = max(ans,v)
return v
这种写法是有问题的
本质还是因为左右子树的最大和路径不一定能拼接到根节点,也就是说子问题无法还原出原问题。考虑过根节点的最大路径和,也就是从根节点一路向下得到的路径和,根节点的最大路径和就是 左边路径和右边路径求出哪个大,然后跟0比较,如果小于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 maxPathSum(self, root: Optional[TreeNode]) -> int:
self.ans = float('-inf')
def dfs(root):
if root is None:
return 0
l = max(dfs(root.left),0)
r = max(dfs(root.right),0)
# 过跟节点的最大路径和
v = l + r + root.val
self.ans = max(self.ans, v)
# 返回的是根节点的最大路径
return max(l,r) + root.val
dfs(root)
return self.ans
二叉树的直径和二叉树的最大路径本质都是在求路径,也就是要从根节点走到叶子节点,但是有两个比较难理解的地方
- 最终结果并不一定过根节点,任何一个节点都有可能是一个解,因此要遍历每个节点
- 简单的子问题无法还原出原问题的解,需要将问题转化一下,并且原始问题的解是在遍历的时候以中间结果记录下来的
总结
- 二叉树在递归的时候会遍历每一个节点,走完所有的路径,我们可以把之前遍历的节点信息放到path中传递下去,一直到叶子节点就拿到了所有的路径
def dfs(root, path):
# 如果为None就直接返回,比如一个节点左孩子是None,右孩子非空
# dfs(None), dfs(root.right)
if root is None:
return
# 搜索路径就是根节点到叶子节点,直接打印路径
if root.left is None and root.right is None:
print(path)
# 可以return也可以不return,因为根节点的左右子树都是None
# 继续往下执行就是左子树dfs(None),右子树dfs(None)
# return
dfs(root.left, path + [root.val])
dfs(root.right, path + [root.val])
- 每次在传递path的时候,其实发生了一次拷贝,递归左子树,会拷贝一份path传下去,递归右子树,同样拷贝一份path传递下去。左子树如何修改path都不会影响右子树的path
3. 二叉树明明只是遍历了一遍所有的节点为什么可以得到所有的路径呢?一开始我的想法是先序遍历到叶子节点,停止后再回溯遍历右子树,然后再网上回溯。这种想法其实是不太完善。从递归树上可以知道,左右子树其实都执行了dfs,然后子树的子树也都执行了dfs,在递进的时候其实每个节点就已经都执行了处理
def dfs(root):
if root is None:
return
before_process()
dfs(root.left)
dfs(root.right)
after_process()
说明白了每个节点都会执行before_process和after_process,区别在于before_process是递归之前调用的,我们先对数据进行处理,然后再递归,处理完成的数据可以继续传递下去,例如path,而after_process是归的时候执行的,处理完成的结果逐步向上返回
先序遍历
max_depth = 0
def depth(root,d=0):
# 节点为空就直接返回,无需继续执行
if root is None:
return
# 当前节点不为None,路径长度+1
d += 1 # 之前经历过了多少个节点
max_depth = max(max_depth, d)
depth(root.left, d) # 处理好的数据传递下去
depth(root.right, d) # 处理好的数据传递下去
后序遍历
def depth(root):
# 节点为空就直接返回,无需继续执行
if root is None:
return 0
l = depth(root.left) # 处理好的数据传递下去
r = depth(root.right) # 处理好的数据传递下去
d = max(l,r) # 后序处理完成后向上返回
return d
后序遍历就是把小的子问题逐步还原出原问题的结果。
二叉树直径:从某个节点到叶子节点的最长路径,其实就是最大深度
def depth(root):
if root is None:
return 0
l = depth(root.left)
r = depth(root.right)
d = max(l,r)
return d
最大深度还是比较好理解的,一路递归下去(到叶子节点)如果碰到None则返回0,在遍历的过程中其实会记录所有的路径,我们在这些路径中选择一条最长的也就是最大深度
def depth(root):
if root is None:
return 0
l = depth(root.left)
r = depth(root.right)
d = max(l,r)
return d