文章目录
深度优先搜索是图论中的经典算法,利用深度优先搜索算法可以产生目标图的相应拓扑排序表,利用拓扑排序表可以方便的解决很多相关的图论问题,如最大路径问题等等。
1.二叉树的最大深度
这道题是根基。多写点方案,从不同角度来理解递归总是有好处的。
public int maxDepth(TreeNode root) {
int depth = 0;
return dfs(root, depth);
}
private int dfs(TreeNode root, int depth) {
if (root == null)
return depth;
int left = dfs(root.left, depth + 1);
int right = dfs(root.right, depth + 1);
return Math.max(left,right);
}
public int maxDepth(TreeNode root) {
return dfs(root);
}
private int dfs(TreeNode root) {
if (root == null)
return 0;
// if(root.left == null && root.right == null) {
// return 1;
// }
int left = dfs(root.left);
int right = dfs(root.right);
return Math.max(left,right) + 1;
}
感觉还是有点乱,从这样的代码还是不容易看清楚,得写出一种更明晰的代码:
思路1:自顶向下(前序遍历:根左右)
private int answer;
public int maxDepth(TreeNode root) {
int depth = 1;
dfs(root, depth);
return answer;
}
private void dfs(TreeNode root, int depth) {
if (root == null)
return;
if (root.left == null && root.right == null)
answer = Math.max(answer, depth);
dfs(root.left, depth + 1);
dfs(root.right, depth + 1);
}
这样就很好,搜索就只干搜索的事,不要考虑返回值。我认为初学者写代码,可读性与规范性是>简洁性的。
思路2:自底向上(后序遍历:左右根)
必须完全掌握这套代码,因为后续遇到很多题目都是基于这个模版的。
public int maxDepth(TreeNode root) {
return dfs(root);
}
private int dfs(TreeNode root) {
if (root == null)
return 0;
int left = dfs(root.left);
int right = dfs(root.right);
return Math.max(left,right) + 1;
}
这道题,两种思路都很简单,也容易理解。
2.二叉树的最小深度
思路1:自顶向下(前序遍历:根左右)
写法1:
private int answer = Integer.MAX_VALUE;
public int minDepth(TreeNode root) {
int depth = 1;
dfs(root, depth);
return answer == Integer.MAX_VALUE ? 0 : answer;
}
private void dfs(TreeNode root, int depth) {
if (root == null)
return;
if (root.left == null && root.right == null)
answer = Math.min(answer, depth);
dfs(root.left, depth + 1);
dfs(root.right, depth + 1);
}
写法2:
private int answer = Integer.MAX_VALUE;
public int minDepth(TreeNode root) {
if (root == null)
return 0;
int depth = 1;
dfs(root, depth);
return answer;
}
private void dfs(TreeNode root, int depth) {
if (root.left == null && root.right == null)
answer = Math.min(answer, depth);
if (root.left != null)
dfs(root.left, depth + 1);
if (root.right != null)
dfs(root.right, depth + 1);
}
思路2:自底向上(后序遍历:左右根)
public int minDepth(TreeNode root) {
if (root == null)
return 0;
return dfs(root);
}
private int dfs(TreeNode root) {
// if (root == null)
// return 0;
if (root.left == null && root.right == null)
return 1;
int minDepth = Integer.MAX_VALUE;// 当然你可以定义为成员变量,但类变量一般都是ans,即返回结果,所以我觉得还是定义成局部变量好点。
if (root.left != null)
minDepth = Math.min(minDepth, dfs(root.left));
if (root.right != null)
minDepth = Math.min(minDepth, dfs(root.right));
return minDepth + 1;
}
显然,自顶向下需要思考的情况少了很多,更推荐使用。
思考一下为什么最大深度改成最小深度会变难写,因为最小深度要考虑一个树的左子树或右子树为空的情况,Math.min会直接选0。所以得想办法排除。
3.二叉树的所有路径
思路1:自顶向下(前序遍历:根左右)
private List<String> answer;
public List<String> binaryTreePaths(TreeNode root) {
answer = new ArrayList<>();// 引用类型的成员变量,默认初始化为null。我们应该返回**空列表**而不是null
if (root == null)
return new ArrayList();// 不能return null
String str = "";
dfs(root, str);
return answer;
}
private void dfs(TreeNode root, String str) {
if (root == null)
return;
str += root.val;
if (root.left == null && root.right == null)
answer.add(str);
dfs(root.left, str + "->");
dfs(root.right, str + "->");
}
4.二叉树的直径
注意到直径的两端一定是两个不同的叶子结点,因为答案就是左右子树的深度和。用后序遍历来做。
自底向上:
private int ans;
public int diameterOfBinaryTree(TreeNode root) {
// ans = 0;
dfs(root);
return ans;
}
private int dfs(TreeNode root) {
if (root == null)
return 0;
int left = dfs(root.left);
int right = dfs(root.right);
ans = Math.max(ans, left + right);// 注意到了吗?后序遍历的更新变量是在两个dfs后的
return Math.max(left,right) + 1;
}
从这题可以看出,自底向上也可能需要全局变量,也可能需要更新变量。感觉是,如果你要特别处理dfs的结果,那么自底向上会方便点。
5.最长同值路径
跟直径题差不多,自底向上,后序遍历即可,注意不要犯注释里的错误。
private int ans;
public int longestUnivaluePath(TreeNode root) {
dfs(root);
return ans;
}
public int dfs(TreeNode root) {
if (root == null)
return 0;
int left = dfs(root.left);
if (root.left != null && root.val != root.left.val)
left = 0;
int right = dfs(root.right);
if (root.right != null && root.val != root.right.val)
right = 0;
// 错误示范2:
// int left = root.left == null || root.val != root.left.val ? 0 : dfs(root.left);
// int right = root.right == null || root.val != root.right.val ? 0 : dfs(root.right);
// 错误示范1:
// if (root.left != null && root.val == root.left.val) {
// int left = dfs(root.left);
// }
// if (root.right != null && root.val == root.right.val) {
// int right = dfs(root.right);
// }
ans = Math.max(ans, left + right);
return Math.max(left, right) + 1;
}
6.路径总和
自顶向下:
private boolean flag;
public boolean hasPathSum(TreeNode root, int sum) {
dfs(root, sum);
return flag;
}
private void dfs(TreeNode root, int sum) {
if (root == null)
return;
if (root.left == null && root.right == null) {
if (!flag)
flag = sum == root.val;
}
dfs(root.left, sum - root.val);
dfs(root.right, sum - root.val);
}
自底向上:
public boolean hasPathSum(TreeNode root, int sum) {
return dfs(root, sum);
}
private boolean dfs(TreeNode root, int sum) {
if (root == null)
return false;
if (root.left == null && root.right == null)
return sum == root.val;
return dfs(root.left, sum - root.val) || dfs(root.right, sum - root.val);
}
7.二叉树中的最大路径和
我一开始觉得这道题不是和第6题差不多嘛,都是跟路径和有关的,那么就用了自顶向下,后来觉得行不通。
制定自顶向下的关键策略是:
- 能确定一些参数,从该节点自身解决出发寻找答案
- 使用这些参数和节点本身的值来决定什么应该是传递给它子节点的参数
而且,自顶向下往往会在叶子结点做出相应的更新变量操作。这道题全都不满足。
自底向上的强大就体现出来了,自底向上的唯一策略是:
- 对于树中的任意一个节点,如果知道它子节点的答案,能计算出该节点的答案。
这道题中,如果某子树的值小于0,就选择舍弃。
直径题是求路径的最大值,这道题是求路径值和的最大值。
如果是直径,代码:
private int ans;
public int maxPathSum(TreeNode root) {
dfs(root);
return ans;
}
private int dfs(TreeNode root) {
if (root == null)
return 0;
int lsum = dfs(root.left);
int rsum = dfs(root.right);
ans = Math.max(ans, lsum + rsum);
return Math.max(lsum, rsum) + 1;
}
来改造一下,就变成该题答案:
private int ans = Integer.MIN_VALUE;
public int maxPathSum(TreeNode root) {
dfs(root);
return ans;
}
private int dfs(TreeNode root) {
if (root == null)
return 0;
int lsum = Math.max(0, dfs(root.left));
int rsum = Math.max(0, dfs(root.right));
ans = Math.max(ans, lsum + rsum + root.val);
return Math.max(lsum, rsum) + root.val;
}
8.路径总和 II
自顶向下:
private List<List<Integer>> ans = new ArrayList();
private Deque<Integer> path = new LinkedList();
public List<List<Integer>> pathSum(TreeNode root, int sum) {
dfs(root, sum);
return ans;
}
private void dfs(TreeNode root, int sum) {
if (root == null) {
return;
}
path.offerLast(root.val);
if (root.left == null && root.right == null) {
if (sum == root.val) {
ans.add(new ArrayList(path));
}
}
dfs(root.left, sum - root.val);
dfs(root.right, sum - root.val);
path.pollLast();
}
9.路径总和Ⅲ
题目路径是向下的,只能从父节点到子节点,暗示了先序遍历。
自顶向下:
private int ans;
public int pathSum(TreeNode root, int sum) {
preorder(root, sum);
return ans;
}
private void preorder(TreeNode root, int sum) {
if (root == null)
return;
dfs(root, sum);
preorder(root.left, sum);
preorder(root.right, sum);
}
private void dfs(TreeNode root, int sum) {
if (root == null)
return;
if (sum == root.val)
ans++;
dfs(root.left, sum - root.val);
dfs(root.right, sum - root.val);
}
10.根到叶路径上的不足节点
public TreeNode sufficientSubset(TreeNode root, int limit) {
boolean flag = dfs(root, limit);
if (flag)
return null;
return root;
}
private boolean dfs(TreeNode root, int limit) {
if (root == null)
return true;
if (root.left == null && root.right == null) {
if(limit - root.val > 0)
return true;
else
return false;
}
boolean ldel = dfs(root.left, limit - root.val);
boolean rdel = dfs(root.right, limit - root.val);
if (ldel)
root.left = null;
if (rdel)
root.right = null;
// return limit - root.val > 0; // 错误
return ldel && rdel;
}
11.二叉树剪枝
public TreeNode pruneTree(TreeNode root) {
if (dfs(root))
return null;
return root;
}
private boolean dfs(TreeNode root) {
if (root == null)
return true;
if (root.left == null && root.right == null) {
if (root.val == 0)
return true;
}
boolean left = dfs(root.left);
boolean right = dfs(root.right);
if (left)
root.left = null;
if (right)
root.right = null;
return root.val == 0 && left && right;
}
12.相同的树
private boolean flag = true;
public boolean isSameTree(TreeNode p, TreeNode q) {
dfs(p, q);
return flag;
}
private void dfs(TreeNode p, TreeNode q) {
if (p == null && q == null) {
return;
}
if (p == null || q == null) {
flag = false;
return;
}
if (p.val != q.val) {
flag = false;
return;
}
dfs(p.left, q.left);
dfs(p.right, q.right);
}
闲谈
很正统的德式钢琴协奏曲:Piano Concerto in F Minor, Op. 114:III. Allegretto con spirito
如果你喜欢巴赫和勃拉姆斯,不要错过这位才华横溢的作曲家。