递归基础训练-路径总和

路径总和

给你二叉树的根节点 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

二叉树的直径和二叉树的最大路径本质都是在求路径,也就是要从根节点走到叶子节点,但是有两个比较难理解的地方

  1. 最终结果并不一定过根节点,任何一个节点都有可能是一个解,因此要遍历每个节点
  2. 简单的子问题无法还原出原问题的解,需要将问题转化一下,并且原始问题的解是在遍历的时候以中间结果记录下来的

总结

  1. 二叉树在递归的时候会遍历每一个节点,走完所有的路径,我们可以把之前遍历的节点信息放到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])
	
  1. 每次在传递path的时候,其实发生了一次拷贝,递归左子树,会拷贝一份path传下去,递归右子树,同样拷贝一份path传递下去。左子树如何修改path都不会影响右子树的path

在这里插入图片描述
3. 二叉树明明只是遍历了一遍节点为什么可以得到所有的路径呢?一开始我的想法是先序遍历到叶子节点,停止后再回溯遍历右子树,然后再网上回溯。这种想法其实是不太完善。从递归树上可以知道,左右子树其实都执行了dfs,然后子树的子树也都执行了dfs,在递进的时候其实每个节点就已经都执行了处理
对于节点7和节点8,都拿到了之前的路径,

def dfs(5,[1,2]):
	dfs(7,[1,2,5])
	dfs(8,[1,2,5])

可以看到并不需要所谓的回溯,节点7拿到了之前所有的路径,并执行dfs记录了一条路径,而节点8也执行了dfs,拿到之前的路几个并记录了一条路径。也可以认为7和8是同时遍历的

def dfs(5,[1,2]):
	thread(dfs(7,[1,2,5])) 
	thread(dfs(8,[1,2,5]))
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

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值