高频算法面试题(三十七)- 二叉树中和为某一值的路径(二)

231 阅读4分钟

「这是我参与11月更文挑战的第 22 天,活动详情查看:2021最后一次更文挑战

刷算法题,从来不是为了记题,而是练习把实际的问题抽象成具体的数据结构或算法模型,然后利用对应的数据结构或算法模型来进行解题。个人觉得,带着这种思维刷题,不仅能解决面试问题,也能更多的学会在日常工作中思考,如何将实际的场景抽象成相应的算法模型,从而提高代码的质量和性能

二叉树中和为某一值的路径(二)

题目来源LeetCode-113. 路径总和 II

题目描述

给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径

叶子节点 是指没有子节点的节点

示例

示例 1

1.png

输入:root = [5,4,8,11,null,13,4,7,2,null,null,5,1], targetSum = 22
输出:[[5,4,11,2],[5,8,4,5]]

示例 2

2.png

输入:root = [1,2,3], targetSum = 5
输出:[]

示例 3

输入:root = [1,2], targetSum = 0
输出:[]

提示:

  • 树中节点总数在范围 [0, 5000] 内
  • 1000 <= Node.val <= 1000
  • 1000 <= targetSum <= 1000

解题

解法一:深度优先搜索

思路

这道题和前边的求【根节点到叶子结点的数字之和】差不多,思路一样。其实核心就是,我们需要遍历每一条从根节点到叶子节点的路径。这就很容易想到深度优先了

深度优先搜索:随意选择一个岔路口来走,走着走着发现走不通的时候,就回退到上一个岔路口,重新选择一条路继续走,直到走完所有的情况

要求跟结点到叶子节点的路径和,跟求跟根节点的子节点到叶子节点的路径和,计算方式是一样的,因此就可以用递归了。遍历过程中,记录每个结点的值,并且累加路径和

  • 遍历的过程中,如果该节点的左右子节点均为空,且路径和为目标值,则这条路径是有效的
  • 如果左右节点不为空,继续遍历

一看代码就明白

代码

//深度优先搜索
func pathSum(root *TreeNode, targetSum int) [][]int {
	var res [][]int
	eachPath := []int{}
	var dfsPathSum func(root *TreeNode, last int)
	dfsPathSum = func(root *TreeNode, last int) {
		if root == nil {
			return
		}
		last -= root.Val
		eachPath = append(eachPath, root.Val)
		defer func() {
			eachPath = eachPath[:len(eachPath)-1] //一条路径走完之后,回退到上一步的路径,继续往下走,所以需要去掉路径中的最后一个
		}()
		if root.Left == nil && root.Right == nil && last == 0 {
			res = append(res, append([]int(nil), eachPath...))
			return
		}

		dfsPathSum(root.Left, last)
		dfsPathSum(root.Right, last)
	}
	dfsPathSum(root, targetSum)

	return res
}

解法二:广度优先搜索

思路

前边已经遇到很多树的题,都用到了深度优先搜索,和广度优先搜索,也说了用深度优先搜索能解的题,基本上都是可以用广度优先搜索解决(反之亦然)

广度优先搜索:是一种“地毯式”层层推进的搜索策略,即先查找离起始顶点最近的,然后是次近的,依次往外搜索

深度优先搜索在树上的提现本质上就是一种层序遍历。那本题用广度优先搜索的难点就是,如果回推出路径,因为广度优先并不会一路搜索到底,是一层一层往下搜索。因此,当我们搜索到左右子节点都为空,且得到了我们的目标值,就应该能够根据当前结点,往回倒退到跟结点,这样就能得到路径了

因此,需要我们在搜索的过程中,记录每个结点的父节点,方便后边推路径。有了这个思路,代码就好写了

加句废话:你可能刷了十几甚至二十几到树的题,遇到变形的树的题还是不会做(比如我),挺挫败的。真的,别慌,刷下去,一定要总结,当把一些解法类型的题归类总结之后,相信一定可以掌握的(我目前在分类的刷树的题,大概这两周刷的差不多,然后开始对前边树的题进行详细总结)

不会就看答案,敲不出来就多敲几便,没有一道题是敲一遍就永远记住的。思路和思考过程的重要性,远大于结果

代码

//广度优先搜索
type pairs struct {
	node *TreeNode
	left int
}
func pathSum2(root *TreeNode, targetSum int) (res [][]int) {
	if root == nil {
		return [][]int{}
	}

	parent := map[*TreeNode]*TreeNode{} //用于记录每个节点的父节点

	//通过叶子结点,回推到根节点,记录路径
	getPath := func(node *TreeNode) (path []int) {
		for ;node != nil; node = parent[node] {
			path = append(path, node.Val)
		} //这个路径是叶子结点到根节点的,所以需要逆序一下

		for i, j := 0, len(path)-1;i < j; i++ {
			path[i], path[j] = path[j], path[i]
			j--
		}
		return
	}

	//常规的广度优先搜索思路,借助队列来记录每一层的结点

	bfsQueue := []pairs{{root, targetSum}}
	for len(bfsQueue) != 0 {
		pair := bfsQueue[0]
		bfsQueue = bfsQueue[1:]
		node := pair.node
		left := pair.left - node.Val
		if node.Left == nil && node.Right == nil {
			if left == 0 {
				res = append(res, getPath(node))
			}
		} else {
			if node.Left != nil {
				parent[node.Left] = node
				bfsQueue = append(bfsQueue, pairs{node.Left, left})
			}
			if node.Right != nil {
				parent[node.Right] = node
				bfsQueue = append(bfsQueue, pairs{node.Right, left})
			}
		}
	}

	return res
}