持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情
树是一种层次关系的数据结构,它具有如下特点:
- 每个节点要么无子节点,要么有有限个子节点
- 没有父节点的节点称为根结点
- 非根结点都有且只有一个父节点
- 除了根结点外,每个子节点都可以分为不相交的子树
- 树里面没有环路
最常见的树的种类是二叉树,每个节点最多只有两个子节点。
本文整理了 LeetCode 10 道使用递归求解的关于树这一数据结构的题。
104. 二叉树的最大深度 - 简单
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树
[3,9,20,null,null,15,7]
,
3 / \ 9 20 / \ 15 7
返回它的最大深度 3 。
题解:
- 深度遍历根结点,取得左右子树的深度,取其最大值加一,即为当前树的最大深度
- 如果当前结点为空,那么其深度为 0。
- 不为空,那么计算左子树的最大深度
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 叉树输入按层序遍历序列化表示,每组子节点由空值分隔(请参见示例)。
示例:
输入: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 。
示例:
输入:root = [3,9,20,null,null,15,7]
输出:true
题解:
递归求解,本题解法和 104. 二叉树的最大深度 类似。
- 递归遍历二叉树,求其左右子树的深度
- 如果根结点为空,那么返回
0
- 根结点不为空,求其左子树的深度
left
,右子树的深度right
,如果深度为-1
,那么不是平衡二叉树- 如果其中一棵子树不是“平衡二叉树”,那么返回
-1
,或者两棵子树的深度之差大于 1,那么当前树也不是平衡二叉树,返回 -1- 如果其左右子树都是平衡二叉树,那么返回当前树的最大深度
代码:
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. 二叉树的最小深度 - 简单
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
**说明:**叶子节点是指没有子节点的节点。
示例:
输入:root = [3,9,20,null,null,15,7]
输出:2
题解:
深度优先遍历
对根结点进行深度遍历,如果根结点为空,那么深度为零
不为空,那么递归遍历左子树的最小深度
l
,右子树的最小深度r
如果当前结点为叶子结点,那么
l
和r
的值都为0
;或者如果当前结点的其中一个子树为空,那么l
和r
中其中一个值为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 。
叶子节点 是指没有子节点的节点。
示例:
输入:root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22
输出:true
解释:等于目标和的根节点到叶节点路径如上图所示。
题解:
递归遍历:
递归遍历结点的子树。
如果结点为空,那么没有相应的路径和等于目标和,返回
false
如果当前结点是叶子结点,判断该叶子结点的值是否等于目标和
递归判断左子树的路径和、右子树的路径和是否等于目标和减去当前结点的值,只要其中一个满足,那么返回
true
广度优先遍历:
- 如果根结点为空,直接返回
false
- 使用两个队列,
nodeQ
负责遍历树结点,pathQ
负责遍历路径和,将根结点和其值分别加入对应的队列- 开始广度遍历,取出
nodeQ
的队首元素node
,pathQ
的队首元素path
- 如果
node
是叶子结点,并且path
等于目标和,那么返回true
,否则,继续下一次遍历- 如果
node
不是叶子结点,将其非空子结点加入队列,并将子节点的值加上path
加入到pathQ
队列- 广度优先遍历结束,没有找到路径和等于目标和,返回
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 ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。
叶子节点 是指没有子节点的节点。
示例:
输入: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
保存当前遍历路径,使用双向链表可以更方便地增加、删除节点。每次向下递归,都将目标和减去当前结点的值。
开始递归遍历根结点
root
如果当前遍历结点为空,返回上一层。
将当前结点添加到路径
path
中。如果当前结点是叶子结点,并且当前结点的值等于目标和
sum
,将路径添加到结果集res
中否则,将目标和减去当前结点的值,递归遍历当前结点的左子树,随后遍历当前结点的右子树,最后将当前结点从路径
path
中删除,往上回溯。递归结束,返回结果集
代码:
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 的 路径 的数目。
路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
示例:
输入:root = [10,5,-3,3,2,null,11,3,-2,null,1], targetSum = 8
输出:3
解释:和等于 8 的路径有 3 条,如图所示。
题解:
递归
- 首先判断根结点是否为空。如果为空,那么路径数为 0,返回 0。否则,计算以该节点为起点的路径和为
targetSum
的路径和总数,再递归计算加上以其左子树、右子树为起点的路径和为targetSum
的路径和总数,即为结果。- 计算路径和总数,如果根结点为空,路径数为 0
- 否则,路径数
count
初始为 0- 如果当前节点的值等于目标和,那么路径数
count
递增,继续计算左右子树的路径数,后面的节点和可能为 0,是新的路径- 计算新的目标和
val
,其值为目标和减去当前节点的值- 递归计算左子树、右子树目标和为
val
的路径数,累加到count
上- 返回
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
, 检查它是否轴对称。
示例:
输入:root = [1,2,2,3,4,4,3]
输出:true
题解:
深度递归,递归判断根结点的左子树和右子树的值是否相等,递归过程需要注意一些细节
- 如果左节点和右节点都为空,那么该树对称
- 如果左节点和右节点分别有一个为空,或者左节点和右节点的值不相等,那么该树不对称
- 否则,该节点的左结点和右节点值相等,接下来递归判断,左子树的左结点和右子树的右节点是否对称,左子树的右节点和右子树的左节点是否对称,如果都对称,那么该树是对称二叉树
代码:
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 中出现,我们就把该节点从树上删去,最后得到一个森林(一些不相交的树构成的集合)。
返回森林中的每棵树。你可以按任意顺序组织答案。
示例:
输入:root = [1,2,3,4,5,6,7], to_delete = [3,5]
输出:[[1,2,null,4],[6],[7]]
题解:
递归,这里要注意一个细节,就是树的根结点也被删除了。
我们从叶子结点逐渐向上删除节点,为了方便令其父节点的子树指向原来的节点,或者指向删除后的空节点,我们令递归函数返回待处理节点,这样,无论节点是否删除,父节点都能指向正确的元素。
- 利用哈希表
Set
保存待删除节点的值,方便处理节点时快速索引- 递归处理二叉树,令二叉树
root
指向处理后的元素- 如果二叉树为空,那么根节点被删除,不加入到结果集,否则加入到结果集,最后返回结果集
递归处理的流程如下
- 如果 待处理节点 为空,那么直接返回节点。否则
- 递归处理左子树,并令左子树指向处理后的节点
- 递归处理右子树,并令右子树指向处理后的节点
- 递归到终点后,处理当前结点,如果当前节点不是待删除节点,直接返回当前节点
- 当前节点时待删除节点,那么
- 如果当前节点的左子树非空,将其左子树加入到结果集
- 如果当前节点的右子树非空,将其右子树加入到结果集
- 将当前节点置空,即删除当前节点
代码:
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;
}
}