二叉树的最大深度
题目详细:LeetCode.104
递归法很容易理解:
- 定义一个全局变量max, 记录二叉树的最大深度
- 在递归函数中增加一个深度参数,表示当前的节点的深度
- 然后对二叉树进行深度优先遍历
- 当遍历到叶子节点时,比较当前节点的深度和记录的最大深度,保持最大值
- 当树遍历完成后,返回记录的最大深度即可
Java解法(递归,深度优先遍历):
class Solution {
private int max = 0;
public int maxDepth(TreeNode root) {
this.dfs(root, 0);
return this.max;
}
public void dfs(TreeNode root, int deep){
if(null == root){
this.max = deep > this.max ? deep : this.max;
return;
}
this.dfs(root.left, deep + 1);
this.dfs(root.right, deep + 1);
}
}
递归法很容易掌握,所以我想尝试用迭代法来解题,但使用前序遍历迭代法来解题好像不是很方便;回到观察问题本身,我发现求二叉树的最大深度,其实就是求叶子节点的最大层数,那么我们也可以利用层序遍历(广度优先遍历)来解题:
Java解法(层序遍历,迭代法,广度优先遍历):
class Solution {
public int maxDepth(TreeNode root) {
return this.bfs(root);
}
public int bfs(TreeNode root){
Queue<TreeNode> queue = new LinkedList<>();
if(null != root) queue.offer(root);
int deep = 0;
while(!queue.isEmpty()){
deep++;
int len = queue.size();
while(len-- > 0){
TreeNode node = queue.poll();
if(null != node.left) queue.offer(node.left);
if(null != node.right) queue.offer(node.right);
}
}
return deep;
}
}
n叉树的最大深度
题目详细:LeetCode.559
这道题和上一题的二叉树的最大深度有异曲同工之妙,区别在于:
- n叉树的多个节点,使用的是指针数组来存储
- 除了根节点可能为空外,在遍历孩子节点过程中,不会遍历到空节点
- 叶子节点的定义有两种情况:
- 孩子节点数组为空(即 node.children == null ,这种情况在样本数据中未出现)
- 孩子节点数组长度为 0
所以这道题利用递归法来解决非常简单,跟求解二叉树的最大深度的过程类似,只是这次改为对每一个孩子节点都进行深度优先遍历,最后保留深度最大值。
Java解法(递归,深度优先遍历):
class Solution {
public int maxDepth(Node root) {
return this.dfs(root);
}
public int dfs(Node root){
int deep = 0;
if(null == root) return deep;
for(Node node: root.children){
deep = Math.max(deep, this.dfs(node));
}
return deep + 1;
}
}
二叉树的最小深度
题目详细:LeetCode.111
这道题与上面找二叉树的最大深度刚好相反,要求找二叉树的最小深度,但是其解题思路是相似的:
- 同样是遍历到树的叶子节点,只不过一个是记录树的最大深度,一个是记录树的最小深度罢了
- 这道题的难点在于如何确定是否到达了叶子节点
- 因为求二叉树的最大深度时,我们并不需要去关心当前节点是否是叶子节点
- 我们只需要注意当遍历到空节点时进行回溯,即空节点的深度 - 1,同时与记录的深度进行比较,保持一个最大值即可
- 但是在求二叉树的最小深度时,就不一样了,因为在遍历到空节点时,会出现这四种情况,都有着各自的处理方式:
- 当前节点的左节点为空,但其右节点不为空,非叶子节点,继续遍历右子树查找叶子节点
- 当前节点的左节点不为空,但其右节点为空,非叶子节点,继续遍历右子树查找叶子节点
- 当前节点的左右节点都不为空 ,继续遍历左右子树查找叶子节点
- 当前节点的左右节点都为空,属于叶子节点,比较当前节点的深度并记录最小深度
- 因为求二叉树的最大深度时,我们并不需要去关心当前节点是否是叶子节点
- 可见,在求二叉树的最小深度的过程中,并不涉及回溯,因为如果遍历到空节点就回溯的话,其回溯到的节点不一定是叶子节点
- 在求解过程中,只有必须判断当前节点是否是叶子节点,才能够更新记录值。
Java解法(递归,深度优先遍历,模拟思路版):
class Solution {
private int min = Integer.MAX_VALUE;
public int minDepth(TreeNode root) {
if(null != root){
this.dfs(root, 1);
return min;
}
return 0;
}
public void dfs(TreeNode root, int deep){
if(null != root && null == root.left && null == root.right){
min = Math.min(min, deep);
return;
}
if(null != root.left) this.dfs(root.left, deep + 1);
if(null != root.right) this.dfs(root.right, deep + 1);
}
}
Java解法(递归,深度优先遍历,精简版):
class Solution {
public int minDepth(TreeNode root) {
return dfs(root);
}
public int dfs(TreeNode root){
if(null == root) return 0;
if(null != root.left && null == root.right)
return 1 + dfs(root.left);
if(null == root.left && null != root.right)
return 1 + dfs(root.right);
return 1 + Math.min(dfs(root.left), dfs(root.right));
}
}
虽然递归法写起来很简单方便,但是经过测试发现,利用递归来解这道题,虽然能够AC,但是速度并不快,为什么呢?
- 因为递归法的算法思想,是深度优先遍历
- 在二叉树中,它需要去对左右子树都进行深度优先遍历,得到各自的最小深度,然后再比较两者选取一个最小值
- 假如二叉树的左子树深度很小,但是右子树的深度很大
- 那么我们本来只需要通过左子树就可以确定最小深度,却还需要去等待右子树的递归结果,才可以得到最终的结果
- 那毫无疑问,递归右子树是在做无用功的
那么也就是说,我们可以按从上到下,从左到右
的顺序来遍历树,假设我们优先在左子树就遍历到了叶子节点得到其深度,那么后面的节点我们都可以不用去关心了,其叶子节点的深度就是二叉树的最小深度,所以优化这道题的关键就在于遍历方式,所以我们可以利用层序遍历来优化这道题:
Java解法(迭代法,层序遍历,广度优先遍历):
class Solution {
public int minDepth(TreeNode root) {
if(root == null) return 0;
return bfs(root);
}
public int bfs(TreeNode root){
Queue<TreeNode> queue = new LinkedList<>();
if(null != root) queue.offer(root);
int deep = 1;
while(!queue.isEmpty()){
int n = queue.size();
while(n-- > 0){
root = queue.poll();
if(null == root.left && null == root.right) return deep;
if(null != root.left) queue.offer(root.left);
if(null != root.right) queue.offer(root.right);
}
deep++;
}
return deep;
}
}
完全二叉树的节点个数
题目详细:LeetCode.222
这道题如果不涉及完全二叉树,那么我们用任何一种遍历方式来统计二叉树中的节点个数都是可以的,只需要一遍遍历,一边个数 + 1就行了,那么我们可以得到非常大智若愚的递归解法:
Java解法(递归,深度优先遍历,前序遍历):
class Solution {
public int countNodes(TreeNode root) {
if(null == root) return 0;
return 1 + countNodes(root.left) + countNodes(root.right);
}
}
那么这道题又何必是求完全二叉树的节点个数呢,为什么不直接让我求二叉树的节点个数就得了?此时,我注意到题目最下面一段小字:
进阶:遍历树来统计节点是一种时间复杂度为 O(n) 的简单解决方案。你可以设计一个更快的算法吗?
确实,我们如果使用遍历树的方法来统计节点,得到的都是时间复杂度为 O(n) 的算法,完全没有利用到样本数据是一棵完全二叉树的特点,关于完全二叉树的概念和特点详细可以查阅:《代码随想录》— 完全二叉树
在了解了完全二叉树的特点之后,我们不难发现,我们可以利用层序遍历
:
- 当遍历到当前节点的某一子节点为空时,那么我们就可以确定其空子节点之前的节点都是非空的
- 那么只需要
累计之前遍历过的节点数量 + 与空子节点同一层的之前的节点长度
,就可以得到该完全二叉树的节点个数了
Java解法(迭代法,广度优先遍历,层序遍历):
class Solution {
public int countNodes(TreeNode root) {
return this.bfs(root);
}
public int bfs(TreeNode root){
Queue<TreeNode> queue = new LinkedList<>();
if(null != root) queue.offer(root);
int sum = 0;
while(!queue.isEmpty()){
int n = queue.size();
while(n-- > 0){
sum++;
root = queue.poll();
if(null != root.left) queue.offer(root.left);
if(null != root.right) queue.offer(root.right);
if(null == root.left || null == root.right){
return sum + queue.size();
}
}
}
return sum;
}
}
前两天对二叉树的题我都做得很详细,所以基础算是牢固,而且通过昨天的联系,也明白了二叉树的题目无非就是两个要点:选择最合适的遍历方法 + 处理节点的逻辑方法,在今天的练习过后,我更加肯定了这一观点,并且对二叉树不同的遍历方法更加重视了。
题目看似不停的变,其实万变不离其宗,只要遍历方法选对了,剩下的只需要根据题目需求来处理目标节点就可以解题了,对二叉树的遍历越理解,以及不同类型二叉树各自的特点越熟悉,解起二叉树的题来简直就是得心应手,令我感叹道:
读书破万卷,下笔如有神。