算法套路七——二叉树递归
- 如何思考二叉树相关问题?
不要一开始就陷入细节,而是思考整棵树与其左右子树的关系。 - 为什么需要使用递归?
子问题和原问题是相似的,他们执行的代码也是相同的(类比循环),但是子问题需要把计算结果返回给上一级,这更适合用递归实现。 - 为什么这样写就一定能算出正确答案?
由于子问题的规模比原问题小,不断“递”下去,总会有个尽头,即递归的边界条件 ( base case ),直接返回它的答案“归”;
类似于数学归纳法(多米诺骨牌),n=1时类似边界条件;n=m时类似往后任意一个节点 - 计算机是怎么执行递归的?
当程序执行“递”动作时,计算机使用栈保存这个发出“递”动作的对象,程序不断“递”,计算机不断压栈,直到边界时,程序发生“归”动作,正好将执行的答案“归”给栈顶元素,随后程序不断“归”,计算机不断出栈,直到返回原问题的答案,栈空。 - 另一种递归思路
维护全局变量,使用二叉树遍历函数,不断更新全局变量最大值。
算法示例一:LeetCode100. 相同的树
给你两棵二叉树的根节点 p 和 q ,编写一个函数来检验这两棵树是否相同。如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
判断两个树是否是同一颗树,需要满足以下条件:
1.两棵树根节点值相同
2.两棵树的左子树是同一棵树
3.两棵树的右子树是同一棵树
那么对于2,3采用递归判断
是否是同一颗树
且注意考虑特殊情况,即p或q为空时。
class Solution:
def isSameTree(self, p: Optional[TreeNode], q: Optional[TreeNode]) -> bool:
if p is None or q is None:
return p is q
return p.val==q.val and self.isSameTree(p.left,q.left) and self.isSameTree(p.right,q.right)
算法示例二:LeetCode104. 二叉树的最大深度
给定一个二叉树,找出其最大深度。二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
法一:直接递归
求一棵树的最大深度,可以这么求
1.求左子树的最大深度
2.求右子树的最大深度
3.该树的最大深度为左右子树最大的深度+1
4.若当前结点为空,则深度为0,若当前结点左右子树为空,则深度为1
那么对于1,2可以采用递归
求最大深度
class Solution:
def maxDepth(self, root: Optional[TreeNode]) -> int:
if root is None: return 0
l_depth = self.maxDepth(root.left)
r_depth = self.maxDepth(root.right)
return max(l_depth, r_depth) + 1
法二:使用全局变量记录
另一种递归思路即维护全局变量,使用全局变量ans记录已经递归过的节点的目标最值
,且使用全局变量时往往需要使用cur记录当前节点的值
,并与全局变量ans比较
,对ans进行更新,因此需要新定义一个遍历函数dfs(root,cur)并增加新参数cur,其中cur用来记录当前遍历的节点
个人感觉这种思路适合于题目较复杂或题目要求记录已经遍历过的节点时
,此时用全局变量记录目标可以方便理解及代码编写
class Solution:
def maxDepth(self, root: Optional[TreeNode]) -> int:
ans = 0
def dfs(node, cnt):
if node is None:
return
cnt += 1
nonlocal ans
ans = max(ans, cnt)
dfs(node.left, cnt)
dfs(node.right, cnt)
dfs(root, 0)
return ans
算法练习一:LeetCode101. 对称二叉树
给你一个二叉树的根节点 root , 检查它是否轴对称。树中节点数目在范围 [1, 1000] 内
对于根节点已经是对称二叉树,所以判断左右子树p,q是否对称
那么根据示例进行修改,判断是否是对称树,那么对于p,q两棵树需满足以下要求
1.p,q两棵树根节点相同
2.p的左子树与q的右子树是对称子树
3.p的右子树与q的左子树是对称子树
那么对于2,3采用递归判断
是否是对称子树
且注意考虑特殊情况,即p或q为空时。
func isSymmetric(root *TreeNode) bool {
return isSymmetricTree(root.Left, root.Right)
}
func isSymmetricTree(p, q *TreeNode) bool {
if p == nil || q == nil {
return p == q
}
return p.Val == q.Val && isSymmetricTree(p.Left, q.Right) && isSymmetricTree(p.Right, q.Left)
}
算法练习二:110. 平衡二叉树
给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。
根据示例进行修改,判断root是否是平衡树,那么对于root树需满足以下要求
1.root的左右子树的最大高度差的绝对值不超过 1
2.root的左子树为平衡树
3.root的右子树为平衡树
那么对于2,3采用递归判断
是否是对称子树
且注意考虑特殊情况,即root为空时。
func isBalanced(root *TreeNode) bool {
if root==nil{
return true
}
lDepth:=maxDepth(root.Left)
rDepth := maxDepth(root.Right)
if abs(rDepth-lDepth)>1{
return false
}else{
return isBalanced(root.Left)&&isBalanced(root.Right)
}
}
func maxDepth(root *TreeNode) int {
if root == nil {
return 0
}
lDepth := maxDepth(root.Left)
rDepth := maxDepth(root.Right)
return max(lDepth, rDepth) + 1
}
func max(a, b int) int {
if b > a {
return b }; return a }
func abs(a int) int {
if a > 0 {
return a }; return -a }
算法练习三:111. 二叉树的最小深度
给定一个二叉树,找出其最小深度。最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
法一:不使用全局变量
根据示例进行修改,求root树的最小深度,那么可以采取一下求法
1.求root左子树的最小深度
2.求root右子树的最小深度
3.比较左右子树深度,最小的深度+1即为root的最小深度
4.若当前结点为空,则深度为0,若当前结点左右子树为空,则深度为1
那么对于1,2采用递归判断
求最小深度
func minDepth(root *TreeNode) int {
if root==nil{
return 0
}else if root.Left==nil&&root.Right==nil{
return 1
}else if root.Left==nil{
return minDepth(root.Right)+1
}else if root.Right==nil{
return minDepth(root.Left)+1
}
return min(minDepth(root.Right),minDepth(root.Left))+1
}
func min(a,b int) int {
if a>b {
return b};return a}
法二:使用全局变量
使用全局变量minD记录遍历结点的最值,使用参数cur记录当前的深度,每次遍历时若为叶子结点则比较cur与minD看是否更新minD
var minD int =1000000
func minDepth(root *TreeNode) int {
if root==nil{
return 0
}
dfs(root,1)
return minD
}
func dfs(node *TreeNode,cur int){
if node==nil{
return
}
if node.Left==nil&&node.Right==nil{
minD = min(minD,cur)
}
dfs(node.Left,cur+1)
dfs(node.Right,cur+1)
}
func min(a,b int) int {
if a>b {
return b};return a}
算法练习四:112. 路径总和
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。
根据示例进行修改,判断root树是否存在 根节点到叶子节点 的路径和等于targetSum
1.root的左子树是否存在根节点到叶子节点 的路径和等于targetSum-root.Val
2.root的右子树是否存在根节点到叶子节点 的路径和等于targetSum-root.Val
3.若左右子树存在一个满足条件,则root树也满足条件
那么对于1,2采用递归判断
是否满足条件
且注意考虑特殊情况,即root为空时。
func hasPathSum(root *TreeNode, targetSum int) bool {
if root==nil{
return false
}
if root.Val==targetSum&&root.Left==nil&&root.Right==nil{
return true
}
return hasPathSum(root.Left,targetSum-root.Val)||hasPathSum(root.Right,targetSum-root.Val)
}
算法练习五:226. 翻转二叉树
给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。
根据示例进行修改,翻转一颗二叉树root,可以分以下步骤
1.将root的左右指针调换
2.翻转root的左子树
3.r翻转root的右子树
那么对于2,3采用递归
翻转子树
且注意考虑特殊情况,即root为空时。
func invertTree(root *TreeNode) *TreeNode {
if root==nil{
return nil
}
root.Left, root.Right = root.Right, root.Left
invertTree(root.Left)
invertTree(root.Right)
return root
}
算法进阶一:257. 二叉树的所有路径
给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。
根据示例进行修改,该题要求我们返回所有的路径,因此我们可以使用全局变量paths的递归思路,构造dfs递归函数,并增加参数path记录遍历过的节点
1.将当前结点值记录在path中,并判断当前结点是否是叶子结点
2.若是叶子结点,则将记录的path值添加到全局变量paths
3.若不是叶子结点,则递归该结点的左右子结点
且注意考虑特殊情况,即root为空时。
var paths []string
func binaryTreePaths(root *TreeNode) []string {
paths = []string{
}
dfs(root, "")
return paths
}
func dfs(root *TreeNode, path string) {
//string是值传递,修改只会影响到该函数内部的副本
if root != nil {
pathSB := path
pathSB += strconv.Itoa(root.Val)//将root节点的值转换成字符串并追加到pathSB变量后面。
if root.Left == nil && root.Right == nil {
paths = append(paths, pathSB)
} else {
pathSB += "->"
dfs(root.Left, pathSB)
dfs(root.Right, pathSB)
}
}
}
算法进阶二:129. 求根节点到叶节点数字之和
给你一个二叉树的根节点 root ,树中每个节点都存放有一个 0 到 9 之间的数字。
每条从根节点到叶节点的路径都代表一个数字:例如,从根节点到叶节点的路径 1 -> 2 -> 3 表示数字 123 。
计算从根节点到叶节点生成的 所有数字之和 。
根据示例进行修改,该题要求我们求根节点到叶节点数字之和,因此我们可以使用全局变量ans的递归思路,构造dfs递归函数,并增加参数curnum 记录遍历过的节点值
1.将当前结点值添加到curnum中,并判断当前结点是否是叶子结点
2.若是叶子结点,则将记录的curnum值添加到全局变量ans
3.若不是叶子结点,则继续递归该结点的左右子结点
且注意考虑特殊情况,即root为空时。
var ans int
func sumNumbers(root *TreeNode) int {
ans=0
dfs(root,0)
return ans
}
func dfs(node *TreeNode,curnum int){
if node==nil{
return
}
curnum=curnum*10+node.Val
if node.Left==nil&&node.Right==nil{
ans+=curnum
}else{
dfs(node.Left,curnum)
dfs(node.Right,curnum)
}
}
算法进阶三:113. 路径总和 II
给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
法一:全局变量递归
该题与上题一样,递归思想基本一样,也是使用全局变量进行解决,不过该题在代码书写上值得注意,在go语言中只有值传递,当slice做为参数传递给函数的时候,新建了切片s,只是原始切片slice和参数s切片的底层数组是一样的,与Java不同,在Java中需要使用Lsit进行修改,且在函数结束前需要删除List的最后一个元素
根据示例进行修改,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径,因此我们可以使用全局变量ans的递归思路,构造dfs递归函数,并增加参数path 记录遍历过的节点值
1.将当前结点值添加到path中,并判断当前结点是否是叶子结点
2.若是叶子结点,则将记录的path值添加到全局变量ans
3.若不是叶子结点,则继续递归该结点的左右子结点
那么对于2,3采用递归判断
是否满足条件
且注意考虑特殊情况,即root为空时。
go代码:
var ans [][]int
func pathSum(root *TreeNode, targetSum int) ( [][]int) {
ans = make([][]int, 0)
path := []int{
}
dfs(root, targetSum,path)
return ans
}
func dfs(node *TreeNode, left int,path []int) {
//go语言只有值传递,当slice做为参数传递给函数的时候,新建了切片s,只是原始切片slice和参数s切片的底层数组是一样的,
//即对于本题,则是函数外的path与函数内部的path不是同一个切片,在递归函数中给paht进行了append一个元素,对递归函数外的path没有影响,所以只要不对底层数组进行修改,那么就可以认为是不同的切片
if node == nil {
return
}
left -= node.Val
path = append(path, node.Val)
if node.Left == nil && node.Right == nil && left == 0 {
ans = append(ans, append([]int(nil), path...))//在二维切片中添加一维切片
return
}
dfs(node.Left, left,path)
dfs(node.Right, left,path)
}
Java代码:
class Solution {
private List<List<Integer>> ans = new ArrayList<>();
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
List<Integer> path = new ArrayList<>();
dfs(root, targetSum, path);
return ans;
}
private void dfs(TreeNode node, int left, List<Integer> path) {
if (node == null) {
return;
}
left -= node.val;
path.add(node.val);
if (node.left == null && node.right == null && left == 0) {
ans.add(new ArrayList<>(path));
}
dfs(node.left, left, path);
dfs(node.right, left, path);
path.remove(path.size() - 1);
}
}
法二:二叉树递归+回溯
该题还有另一种代码编写方式,就是使用回溯的思想,在本题中先简要介绍该方法的代码,在之后的套路中会详细介绍
func pathSum(root *TreeNode, targetSum int) (ans [][]int) {
path := []int{
}
var dfs func(*TreeNode, int)
dfs = func(node *TreeNode, left int) {
if node == nil {
return
}
left -= node.Val
path = append(path, node.Val)
defer func() {
path = path[:len(path)-1] }()
//使用defer语句,不仅可以在函数结束前调用函数,还可以在return前调用函数
//如果不使用defer,在ans进行append之后不能直接返回,需要继续运行到最后
if node.Left == nil && node.Right == nil && left == 0 {
ans = append(ans, append([]int(nil), path...))
return//因为使用了defer语句,在return前也会回退path,所以可以直接返回,减少了后两次对null结点的dfs
}
dfs(node.Left, left)
dfs(node.Right, left)
}
dfs(root, targetSum)
return
}
算法进阶四:199. 二叉树的右视图
给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
本题有一定难度,主要在于如何判读当前结点时右视图,那么我们考虑右视图有多少个,易知每一层只有一个右视图结点。
那么在遍历过程中,如果我们每次都从右子树开始遍历,那么在第n层深度遍历到的第一个结点就是右视图结点,且此时len(ans)==Depth,而该层之后的结点都有len(ans)==Depth+1.
那么如果我们每次都从右结点开始遍历,那么可以使用当前结点的深度与全局变量ans的长度
来判断当前结点是否是该深度的右视图结点。
根据示例进行修改,找出所有二叉树的右视图结点,因此我们可以使用全局变量ans的递归思路,构造dfs递归函数,并增加参数curDepth记录遍历的节点深度
1.将当前结点值添加到path中,并判断当前结点的curDepth==len(ans)
2.若是叶子结点,则将当前结点值添加到全局变量ans
3.若不是叶子结点,则先遍历当前结点右子树,再遍历左子树
那么对于2,3采用递归判断
是否满足条件
且注意考虑特殊情况,即root为空时。
var ans []int
func rightSideView(root *TreeNode) []int {
ans=[]int{
}
dfs(root,0)
return ans
}
func dfs(node *TreeNode,curDepth int) {
if(node==nil){
return
}
if curDepth==len(ans){
ans=append(ans,node.Val)
}
dfs(node.Right,curDepth+1)
dfs(node.Left,curDepth+1)
}