Leetcode 算法之数据结构:树(树的递归) —— Java 题解

179 阅读12分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情

树是一种层次关系的数据结构,它具有如下特点:

  • 每个节点要么无子节点,要么有有限个子节点
  • 没有父节点的节点称为根结点
  • 非根结点都有且只有一个父节点
  • 除了根结点外,每个子节点都可以分为不相交的子树
  • 树里面没有环路

最常见的树的种类是二叉树,每个节点最多只有两个子节点。

本文整理了 LeetCode 10 道使用递归求解的关于树这一数据结构的题。

104. 二叉树的最大深度 - 简单

给定一个二叉树,找出其最大深度。

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

说明: 叶子节点是指没有子节点的节点。

示例:

给定二叉树 [3,9,20,null,null,15,7]

    3
   / \
  9  20
    /  \
   15   7

返回它的最大深度 3 。

题解:

  1. 深度遍历根结点,取得左右子树的深度,取其最大值加一,即为当前树的最大深度
  2. 如果当前结点为空,那么其深度为 0。
  3. 不为空,那么计算左子树的最大深度 left ,再计算右子树的最大深度 right ,取较大值 + 1,即为当前子树的最大深度

代码:

public int maxDepth(TreeNode root) {
    return root == null ? 0 : Math.max(maxDepth(root.left), maxDepth( root.right))+1;
}

//===================== if-else 版本 =================
public int maxDepth(TreeNode root) {
    if (root == null) {
        return 0;
    }
    int left = maxDepth(root.left);
    int right = maxDepth(root.right);
    return Math.max(left, right)+1;
}

559. N 叉树的最大深度 - 简单

给定一个 N 叉树,找到其最大深度。

最大深度是指从根节点到最远叶子节点的最长路径上的节点总数。

N 叉树输入按层序遍历序列化表示,每组子节点由空值分隔(请参见示例)。

示例:

narytreeexample.png

输入:root = [1,null,3,2,4,null,5,6]

输出:3

题解:

解法与 104. 二叉树的最大深度 - 简单 类似

对根结点进行深度遍历,如果当前结点为空,那么当前结点的最大深度为0。

否则,定义一个变量 depth 记录当前结点的子树的最大深度,初始为 0。

遍历当前结点的子节点,递归计算其子节点的最大深度,同时维护变量 depth,即 depth = Math.max(depth, 子结点的最大深度)

遍历子节点结束后,返回 depth+1,即为当前遍历结点的最大深度。

代码:

class Solution {
    public int maxDepth(Node root) {
        if (root == null) {
            return 0;
        }
        int depth = 0;
        for (int i = 0; i < root.children.size(); i++) {
            depth = Math.max(depth, maxDepth(root.children.get(i)));
        }
        return depth+1;
    }
}

110. 平衡二叉树 - 简单

给定一个二叉树,判断它是否是高度平衡的二叉树。

本题中,一棵高度平衡二叉树定义为:

一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。

示例:

balance_1.jpg

输入:root = [3,9,20,null,null,15,7]

输出:true

题解:

递归求解,本题解法和 104. 二叉树的最大深度 类似。

  1. 递归遍历二叉树,求其左右子树的深度
  2. 如果根结点为空,那么返回 0
  3. 根结点不为空,求其左子树的深度 left,右子树的深度 right,如果深度为 -1,那么不是平衡二叉树
  4. 如果其中一棵子树不是“平衡二叉树”,那么返回 -1,或者两棵子树的深度之差大于 1,那么当前树也不是平衡二叉树,返回 -1
  5. 如果其左右子树都是平衡二叉树,那么返回当前树的最大深度

代码:

class Solution {

    public boolean isBalanced(TreeNode root) {
        return dfs(root) != -1;
    }

    public int dfs(TreeNode root) {
        if (root == null) {
            return 0;
        }
        int left = dfs(root.left), right = dfs(root.right);
        if (left == -1 || right == -1 || Math.abs(left-right) >= 1) {
            return -1;
        }
        return Math.max(left, right)+1;
    }

}

111. 二叉树的最小深度 - 简单

给定一个二叉树,找出其最小深度。

最小深度是从根节点到最近叶子节点的最短路径上的节点数量。

**说明:**叶子节点是指没有子节点的节点。

示例:

ex_depth.jpg

输入:root = [3,9,20,null,null,15,7]

输出:2

题解:

深度优先遍历

  1. 对根结点进行深度遍历,如果根结点为空,那么深度为零

  2. 不为空,那么递归遍历左子树的最小深度 l,右子树的最小深度 r

  3. 如果当前结点为叶子结点,那么 lr 的值都为 0;或者如果当前结点的其中一个子树为空,那么 lr 中其中一个值为 0。因此这两种情况中,当前结点的最小深度可以表示为 l+r+1。如果当前结点的两个子树都不为空,那么当前结点的最小深度即为两个子树的深度的较小值+1

广度优先搜索

如果根结点为空,那么最小深度为 0。

否则,维护一个变量深度 depth,初始为 1。

使用队列辅助我们广度优先遍历,即对树进行层次遍历。初始时队列中的元素为根结点。

开始层次遍历二叉树,如果当前遍历结点为叶子结点,返回 depth

否则,将其非空的子结点添加到队列中。

如果当前层次遍历完毕,树的深度 depth 递增。

代码:

// 深度优先遍历
class Solution {
    public int minDepth(TreeNode root) {
        if (root == null) {
            return 0;
        }
        int l = minDepth(root.left);
        int r = minDepth(root.right);
        // 如果左右子树有其中一个为空(包含了叶子结点这种情况),那么返回深度 l+r+1
        // 如果左右叶子结点都不为空,那么返回较小深度的值+1
        return root.left == null || root.right == null ? l+r+1 : Math.min(l,r)+1;
    }
}

// 广度优先遍历
class Solution {
    public int minDepth(TreeNode root) {
        if (root == null) {
            return 0;
        }

        int depth = 1;
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);

        while (!queue.isEmpty()) {
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                TreeNode node = queue.poll();
                if (node.left == null && node.right == null) {
                    return depth;
                }
                if (node.left != null) {
                    queue.offer(node.left);
                }
                if (node.right != null) {
                    queue.offer(node.right);
                }
            }
            depth++;
        }
        return depth;
    }
}

543. 二叉树的直径 - 简单

给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。

示例:

给定二叉树

      1
     / \
    2   3
   / \     
  4   5    

返回 3, 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3]。

题解:

深度遍历,遍历当前结点的左子树的最大深度 l 和右子树的最大深度 r,那么穿过当前结点的直径的最大长度为 l + r

因此,找到题只需要在 104. 二叉树的最大深度 - 简单 的基础上修改下即可。

使用一个变量 diameter 维护二叉树的直径。

遍历根结点,如果当前根结点为空,那么其深度为 0。

递归计算其左子树的长度 l 和右子树的长度 r,如果 l+r 大于 diameter,那么穿过当前结点的最大直径即为最新的二叉树直径。

随后,返回当前子树的最大深度。

代码:

class Solution {
    private int diameter;

    public int diameterOfBinaryTree(TreeNode root) {
        helper(root);
        return diameter;
    }

    public int helper(TreeNode root) {
        if (root == null) {
            return 0;
        }
        int l = helper(root.left), r = helper(root.right);
        diameter = Math.max(l+r, diameter);
        return Math.max(l, r) + 1;
    }
}

112. 路径总和 - 简单

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。

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

示例:

pathsum1.jpg

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

输出:true

解释:等于目标和的根节点到叶节点路径如上图所示。

题解:

递归遍历:

递归遍历结点的子树。

  1. 如果结点为空,那么没有相应的路径和等于目标和,返回 false

  2. 如果当前结点是叶子结点,判断该叶子结点的值是否等于目标和

  3. 递归判断左子树的路径和、右子树的路径和是否等于目标和减去当前结点的值,只要其中一个满足,那么返回 true

广度优先遍历:

  1. 如果根结点为空,直接返回 false
  2. 使用两个队列,nodeQ 负责遍历树结点,pathQ 负责遍历路径和,将根结点和其值分别加入对应的队列
  3. 开始广度遍历,取出 nodeQ 的队首元素 nodepathQ 的队首元素 path
  4. 如果 node 是叶子结点,并且 path 等于目标和,那么返回 true,否则,继续下一次遍历
  5. 如果 node 不是叶子结点,将其非空子结点加入队列,并将子节点的值加上 path 加入到 pathQ 队列
  6. 广度优先遍历结束,没有找到路径和等于目标和,返回 false

代码:

// 递归
public boolean hasPathSum(TreeNode root, int targetSum) {
    if (root == null) {
        return false;
    }
    if (root.left == null && root.right == null) {
        return targetSum == root.val;
    }
    return hasPathSum(root.left, targetSum-root.val) || hasPathSum(root.right, targetSum-root.val);
}

// 广度遍历
public boolean hasPathSum(TreeNode root, int targetSum) {
    if (root == null) {
        return false;
    }
    Deque<TreeNode> nodeQ = new LinkedList<>();
    Deque<Integer> pathQ = new LinkedList<>();
    nodeQ.offer(root);
    pathQ.offer(root.val);
    while (!nodeQ.isEmpty()) {
        int size = nodeQ.size();
        for (int i = 0; i < size; i++) {
            int path = pathQ.poll();
            TreeNode node = nodeQ.poll();
            if (node.left == null && node.right == null ) {
                if (path == targetSum) {
                    return true;
                }
                continue;
            }

            if (node.left != null) {
                nodeQ.offer(node.left);
                pathQ.offer(path + node.left.val);
            }
            if (node.right != null) {
                nodeQ.offer(node.right);
                pathQ.offer(path + node.right.val);
            }
        }
    }

    return false;
}

113. 路径总和 II - 中等

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

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

示例:

pathsumii1.jpg

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

输出:[[5,4,11,2],[5,8,4,5]]

题解:

递归、回溯

使用一个结果集 res 保存满足条件的路径,使用一个链表 path 保存当前遍历路径,使用双向链表可以更方便地增加、删除节点。

每次向下递归,都将目标和减去当前结点的值。

  1. 开始递归遍历根结点 root

  2. 如果当前遍历结点为空,返回上一层。

  3. 将当前结点添加到路径 path 中。

  4. 如果当前结点是叶子结点,并且当前结点的值等于目标和 sum,将路径添加到结果集 res

  5. 否则,将目标和减去当前结点的值,递归遍历当前结点的左子树,随后遍历当前结点的右子树,最后将当前结点从路径 path 中删除,往上回溯。

  6. 递归结束,返回结果集

代码:

class Solution {

    public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
        List<List<Integer>> res = new ArrayList<>();
        dfs(root, targetSum, res, new LinkedList<Integer>());
        return res;
    }

    private void dfs(TreeNode root, int sum, List<List<Integer>> res, LinkedList<Integer> path) {
        // 空节点,递归结束
        if (root == null) {
            return;
        }
        // 将当前结点添加到路径中
        path.addLast(root.val);
        // 叶子结点,并且满足路径和条件,添加到结果集,同时回溯
        if (root.left == null && root.right == null && root.val == sum) {
            res.add(new LinkedList<>(path));
            path.removeLast();
            return;
        }
        // 递归遍历左子树和右子树
        dfs(root.left, sum-root.val, res, path);
        dfs(root.right, sum-root.val, res, path);
        // 回溯
        path.removeLast();
    }
}

437. 路径总和 III - 中等

给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum 的 路径 的数目。

路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

示例:

pathsum3-1-tree.jpg

输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8

输出:3

解释:和等于 8 的路径有 3 条,如图所示。

题解:

递归

  1. 首先判断根结点是否为空。如果为空,那么路径数为 0,返回 0。否则,计算以该节点为起点的路径和为 targetSum 的路径和总数,再递归计算加上以其左子树、右子树为起点的路径和为 targetSum 的路径和总数,即为结果。
  2. 计算路径和总数,如果根结点为空,路径数为 0
  3. 否则,路径数 count 初始为 0
  4. 如果当前节点的值等于目标和,那么路径数 count 递增,继续计算左右子树的路径数,后面的节点和可能为 0,是新的路径
  5. 计算新的目标和 val,其值为目标和减去当前节点的值
  6. 递归计算左子树、右子树目标和为 val 的路径数,累加到 count
  7. 返回 count

代码:

class Solution {
    public int pathSum(TreeNode root, int targetSum) {
        // 计算以当前节点为起点的路径数,递归计算以左子树、右子树根结点为起点的路径数,累加结果即为答案
        return root == null ? 0 : helper(root, targetSum)
            + pathSum(root.left, targetSum) + pathSum(root.right, targetSum);
    }
	
    // long 类型变量防止溢出
    private int helper(TreeNode root, long targetSum) {
        if (root == null) {
            return 0;
        }
        int count = 0;
        if (root.val == targetSum) {
            count++;
        }
        count += helper(root.left, targetSum-root.val);
        count += helper(root.right, targetSum-root.val);
        return count;
    }

}

101. 对称二叉树 - 简单

给你一个二叉树的根节点 root , 检查它是否轴对称。

示例:

symtree1.jpg

输入:root = [1,2,2,3,4,4,3]

输出:true

题解:

深度递归,递归判断根结点的左子树和右子树的值是否相等,递归过程需要注意一些细节

  1. 如果左节点和右节点都为空,那么该树对称
  2. 如果左节点和右节点分别有一个为空,或者左节点和右节点的值不相等,那么该树不对称
  3. 否则,该节点的左结点和右节点值相等,接下来递归判断,左子树的左结点和右子树的右节点是否对称,左子树的右节点和右子树的左节点是否对称,如果都对称,那么该树是对称二叉树

代码:

class Solution {
    public boolean isSymmetric(TreeNode root) {
        return dfs(root.left, root.right);
    }

    private boolean dfs(TreeNode left, TreeNode right) {
        if (left == null && right == null) {
            return true;
        }
        if (left == null || right == null || left.val != right.val) {
            return false;
        }
        return dfs(left.left, right.right) && dfs(left.right, right.left);
    }
}

1110. 删点成林 - 中等

给出二叉树的根节点 root,树上每个节点都有一个不同的值。

如果节点值在 to_delete 中出现,我们就把该节点从树上删去,最后得到一个森林(一些不相交的树构成的集合)。

返回森林中的每棵树。你可以按任意顺序组织答案。

示例:

screen-shot-2019-07-01-at-53836-pm.png

输入:root = [1,2,3,4,5,6,7], to_delete = [3,5]

输出:[[1,2,null,4],[6],[7]]

题解:

递归,这里要注意一个细节,就是树的根结点也被删除了。

我们从叶子结点逐渐向上删除节点,为了方便令其父节点的子树指向原来的节点,或者指向删除后的空节点,我们令递归函数返回待处理节点,这样,无论节点是否删除,父节点都能指向正确的元素。

  1. 利用哈希表 Set 保存待删除节点的值,方便处理节点时快速索引
  2. 递归处理二叉树,令二叉树 root 指向处理后的元素
  3. 如果二叉树为空,那么根节点被删除,不加入到结果集,否则加入到结果集,最后返回结果集

递归处理的流程如下

  1. 如果 待处理节点 为空,那么直接返回节点。否则
  2. 递归处理左子树,并令左子树指向处理后的节点
  3. 递归处理右子树,并令右子树指向处理后的节点
  4. 递归到终点后,处理当前结点,如果当前节点不是待删除节点,直接返回当前节点
  5. 当前节点时待删除节点,那么
  6. 如果当前节点的左子树非空,将其左子树加入到结果集
  7. 如果当前节点的右子树非空,将其右子树加入到结果集
  8. 将当前节点置空,即删除当前节点

代码:

class Solution {
    Set<Integer> set = new HashSet<>();
    List<TreeNode> res = new ArrayList<>();

    public List<TreeNode> delNodes(TreeNode root, int[] to_delete) {
        for (int node : to_delete) {
            set.add(node);
        }
        root = dfs(root);
        if (root != null) {
            res.add(root);
        }
        return res;
    }

    private TreeNode dfs(TreeNode root) {
        if (root == null) {
            return root;
        }
        root.left = dfs(root.left);
        root.right = dfs(root.right);
        if (set.contains(root.val)) {
            if (root.left != null) {
                res.add(root.left);
            }
            if (root.right != null) {
                res.add(root.right);
            }
            root = null;
        }
        return root;
    }

}