二叉树的“查”
前言:今天来重点讲一下二叉树的“查”,二叉树也是一种数据存储方式,类比于数组来说,最基本的“查”应该有以下几种:
- 二叉树的大小即节点个数
- 二叉树的最大深度和最小深度
- 二叉树的最近公共祖先
方法论:
本文中主要用到了两种思维模式,如下:
- 是否遍历一遍二叉树就可以得到结果,如果可以,只需要对二叉树的递归遍历方式或者迭代遍历方式便可以可到结果,这种思维模式称为
遍历
思维模式 - 是否需要借助子树的返回值?如果需要可以定义一个递归函数求解(注重逻辑的自洽),这种思维模式下:
递归函数需要返回值
,代码通常写在后序位置
,这种思维模式称为分治
思维模式。
无论那种思维模式,你都需要思考:
如果单独抽出一个节点,他需要做什么事情,需要在前序、中序、后序哪个位置做事情?不同位置的代码仅能接收到该代码之前的结果,前序位置的代码自然接收不到子树的消息,因为前序代码执行的时候,二叉树还没有开始遍历。
一、二叉树的节点个数
1. 普通二叉树的节点个数
1.1 遍历的思维模式
- 递归方式
class Solution {
int count = 0;
public int countNodes(TreeNode root) {
if(root == null) return 0;
count(root);
return count;
}
void count(TreeNode root) {
if(root == null) return;
count(root.left);
count(root.right);
count++;//count++的位置在前、中、后序都可以放置,本质就是数组中的循环体中的count++
}
}
- 迭代方式
迭代方式,深度迭代和广度迭代都可以,只需要将迭代代码中的result.add(curNode.val)改为count++即可,具体代码可参考我的上一篇文章二叉树遍历。
1.2 分治的思维模式
首先思考:如果你是一个树节点,你如何知道以你为父节点的二叉树节点个数?你的子树需要给你返回什么?
我的思考:二叉树的节点个数 = 父节点 + 左子树节点个数 + 右子树节点大小
class Solution {
public int countNodes(TreeNode root) {
if(root == null) return 0;
return 1 + countNodes(root.left) + countNodes(root.right);
}
}
2. 完全二叉树的节点个数
说完普通二叉树的节点个数,其实完全二叉树、满二叉树的节点个数也可以求,但是没有利用完全二叉树和满二叉树的性质,去提高减少代码的时间复杂度和空间复杂度。
既然要利用完全二叉树的性质,那就需要知道他的性质是什么:
- 完全二叉树的子树必定可以分解为多个满二叉树,这里的子树也可以是子树的子树,单个节点也可以是满二叉树。
- 满二叉树的节点个数: 2^h-1,这里的h是层数。root节点为第一层。
知道了以上的性质,我们就不需要遍历左右子树,只需要知道满二叉子树的高度即可。
class Solution {
public int countNodes(TreeNode root) {
if(root == null) return 0;
int hl = 1; int hr = 1;//考虑极端情况,root.left == null或者root.right == null;
TreeNode left = root.left;
TreeNode right = root.right;
while(left != null) {
left = left.left;
hl++;
}
while(right != null) {
right = right.right;
hr++;
}
if(hl == hr) return (2<<(hl-1)) - 1;//如果是满二叉树,按照2^h-1计算,2<<1等于2^2;
//如果不是满二叉树,按照普通二叉树计算
return 1 + countNodes(root.left) + countNodes(root.right);
}
}
3. 满二叉树的节点个数
class Solution {
public int countNodes(TreeNode root) {
if(root == null) return 0;
int h = 0;//考虑极端情况,root.left == null&&root.right == null;
while(root != null) {
root = root.left;
h++;
}
return (2<<(h-1)) - 1;
}
}
关于完全二叉树和满二叉树求深度h的代码总结:
明确一下几点:
1、root节点是谁?是root还是root.left还是root.right,即你所求二叉树的root节点是谁?
2、二叉树的h从0还是从1开始,我觉得这里的h代表的是所求二叉树的父节点层数-1
,为什么减去1,是因为紧接着下面h++
,这也就意味着只要你的root != null,就会+1
。
二、二叉树的最大深度和最小深度
1. 二叉树的最大深度
1.1 遍历的思维模式
- 递归方式
- 一遍遍历是否可以得到结果?可以,可以维护一个全局变量用来记录深度。
- 记录什么的深度?自然是每个叶子节点的深度,然后取深度的最大值就是二叉树的最大深度。
- 站在一个树节点的角度,深度该如何计算?进入节点的时候深度加1,离开节点的时候深度减1。
class Solution {
int depth = 0;//当前叶子结点深度
int max = 0;//全局最大深度
public int maxDepth(TreeNode root) {
traverse(root);
return max;
}
void traverse(TreeNode root) {//
if(root == null) {
max = Math.max(depth,max);
return;
}
depth++;//进入节点,深度+1
traverse(root.left);
traverse(root.right);
depth--;//离开节点,深度-1
}
}
- 迭代方式
采用层序迭代的方式用来计算二叉树的最大深度最方便,因为层序迭代代码的内部会维护当前队列的大小,以一层为一个循环,仅需要维护一个全局变量depth,每次循环后加1便可以计算出二叉树的最大深度。
class Solution {
public int maxDepth(TreeNode root) {
if(root == null) return 0;
LinkedList<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int depth = 0;//维护全局深度变量,初始为层数-1,即1-1=0;
while(!queue.isEmpty()) {
int n = queue.size();
for(int i=0; i<n; i++) {
TreeNode curNode = queue.poll();
if(curNode.left != null) queue.offer(curNode.left);
if(curNode.right != null) queue.offer(curNode.right);
}
depth++;
}
return depth;
}
}
只要涉及到深度相关的问题,要着重考虑初始深度h的取值,考虑极端情况去对h取值。
1.2 分治的思维模式
首先思考:如果你是一个树节点,你如何知道以你为父节点的二叉树深度?你的子树需要给你返回什么?
我的思考:二叉树的最大深度 = 父节点深度 + 左右子树深度的最大值
class Solution {
public int maxDepth(TreeNode root) {
if(root == null) return 0;
return 1 + Math.max(maxDepth(root.left),maxDepth(root.right));
}
}
2. 二叉树的最小深度[重点复习
😵]
最小深度:最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
这道题初看和求最大深度没有什么差别,只需要将max函数改为min就可以,但是实则不一样,最大深度的判断条件是root == null
,如果按照这个判断条件:
max集合中包括了非叶子节点的深度和叶子结点的深度,但是最大深度肯定在叶子结点中间,所以无伤大雅。
同样,min集合包括了非叶子节点的深度和叶子结点的深度,但是最小深度也有可能是非叶子节点,所以需要多加一层限制,让min集合中只有叶子结点,即root.left == null && root.right == null
。
1.1 遍历的思维模式
- 迭代方式
class Solution {
public int minDepth(TreeNode root) {
if(root == null) return 0;
LinkedList<TreeNode> queue = new LinkedList<>();
queue.offer(root);
//维护全局深度变量,下面代码存在一个判断条件,导致depth有可能不返回,所以depth为层数
int depth = 1;
while(!queue.isEmpty()) {
int n = queue.size();
for(int i=0; i<n; i++) {
TreeNode curNode = queue.poll();
//判断是否为叶子结点
if(curNode.left == null && curNode.right == null) return depth;
if(curNode.left != null) queue.offer(curNode.left);
if(curNode.right != null) queue.offer(curNode.right);
}
depth++;
}
return depth;
}
}
1.2 分治的思维模式
首先思考:如果你是一个树节点,你如何知道以你为父节点的二叉树深度?你的子树需要给你返回什么?
我的思考:二叉树的最小深度 = 父节点深度 + 左右子树深度的最小值,但是有个例,如果左右子树某一个为null,最小值应该去另一个子树寻找,而不是直接取为0。
class Solution {
public int minDepth(TreeNode root) {
if(root == null) return 0;
if(root.left == null && root.right == null) return 1;
if(root.left == null && root.right != null) {
return 1 + minDepth(root.right);
}
if(root.left != null && root.right == null) {
return 1 + minDepth(root.left);
}
return 1 + Math.min(minDepth(root.left),minDepth(root.right));
}
}
三、二叉树的公共祖先
最近的公共祖先:Lowest Common Ancestor,简称LCA
如何判断是否是p、q两个节点的最近公共祖先:如果在一个节点的左右子树中能够分别找到p、q,那么该节点就是p、q节点的最近公共祖先。
1. 二叉树节点值不重复,如何判断二叉树中是否有个节点
注:以下代码引自labuladong的算法小抄
// 定义:在以 root 为根的二叉树中寻找值为 val 的节点
TreeNode find(TreeNode root, int val) {
// base case
if (root == null) {
return null;
}
// 看看 root.val 是不是要找的
if (root.val == val) {
return root;
}
// root 不是目标节点,那就去左子树找
TreeNode left = find(root.left, val);
if (left != null) {
return left;
}
// 左子树找不着,那就去右子树找
TreeNode right = find(root.right, val);
if (right != null) {
return right;
}
// 实在找不到了
return null;
}
进一步做修改,但是修改之后效率降低了,因为遍历了左右子树,但是当root恰好等于p时除外。
TreeNode find(TreeNode root, int val) {
if (root == null) {
return null;
}
// 前序位置
if (root.val == val) {
return root;
}
// root 不是目标节点,去左右子树寻找
TreeNode left = find(root.left, val);
TreeNode right = find(root.right, val);
// 看看哪边找到了
return left != null ? left : right;
}
进一步修改,代码效率在此降低,这次百分百要遍历左右子树。
TreeNode find(TreeNode root, int val) {
if (root == null) {
return null;
}
// 先去左右子树寻找
TreeNode left = find(root.left, val);
TreeNode right = find(root.right, val);
// 后序位置,看看 root 是不是目标节点
if (root.val == val) {
return root;
}
// root 不是目标节点,再去看看哪边的子树找到了
return left != null ? left : right;
}
但是为什么要提出上面三种方法呢?继续往下看。
先抛出一个找两个节点的框架:
// 定义:在以 root 为根的二叉树中寻找值为 val1 或 val2 的节点
TreeNode find(TreeNode root, int val1, int val2) {
// base case
if (root == null) {
return null;
}
// 前序位置,看看 root 是不是目标值
if (root.val == val1 || root.val == val2) {
return root;
}
// 去左右子树寻找
TreeNode left = find(root.left, val1, val2);
TreeNode right = find(root.right, val1, val2);
// 后序位置,已经知道左右子树是否存在目标值
return left != null ? left : right;
}
2. 二叉树节点值不重复,且p、q存在于二叉树中
情况1:p、q其中一个节点是另一个节点的祖先
情况2:p、q两个节点能找到最近公共祖先,但不是他们其中一个
分析:你站在一个节点处,你需要知道p、q在不在你的左右,或者是否有一个节点就是你自己,这个是需要你前面节点给你反馈的,分治模式的思想就是分而治之,需要借助子树的力量,很明显该问题分治模式更适用,一次遍历模式我暂时未想到解法。
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null) return null;
if(root == p || root == q) return root;//情况1
//情况2:左右子树来回找,直到找到,因为p、q都存在于二叉树中,最坏的情况就是最近公共祖先是root。
TreeNode left = lowestCommonAncestor(root.left,p,q);
TreeNode right = lowestCommonAncestor(root.right,p,q);
if(left == null) {
return right;
} else if (right == null) {
return left;
} else {
return root;
}
}
}
3. 二叉树节点值不重复,求多个节点的最近公共祖先,且节点组存在于二叉树中
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode[] nodes) {
if(root == null) return null;
//情况1
for(TreeNode node : nodes) {
if(root == node) {
return root;
}
}
//情况2
TreeNode left = lowestCommonAncestor(root.left,nodes);
TreeNode right = lowestCommonAncestor(root.right,nodes);
if(left == null) {
return right;
} else if(right == null) {
return left;
} else {
return root;
}
}
}
4. 二叉树节点值不重复,且p、q不一定存在于二叉树中[😵]
写了一种方法,不过超时了:
class Solution {
HashSet<TreeNode> set = new HashSet<>();
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null) return null;
set = traverser(root,set);
if(root == p || root == q) {
if(set.contains(p) && set.contains(q)) {
return root;
} else {
return null;
}
}
TreeNode left = lowestCommonAncestor(root.left,p,q);
TreeNode right = lowestCommonAncestor(root.right,p,q);
if(left == null) {
return right;
} else if(right == null) {
return left;
} else {
return root;
}
}
HashSet<TreeNode> traverser(TreeNode root, HashSet<TreeNode> set) {
if(root == null) return new HashSet<TreeNode>();
set.add(root);
traverser(root.left,set);
traverser(root.right,set);
return set;
}
}
那么该如何不超时呢?在递归过程中去判断p、q是否同时存在于二叉树中
class Solution {
boolean foundP = false;
boolean foundQ = false;
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
TreeNode res = find(root,p,q);
if(!foundP || !foundQ ) {
return null;
} else {
return res;
}
}
TreeNode find(TreeNode root, TreeNode p, TreeNode q) {
if(root == null) return null;
TreeNode left = find(root.left,p,q);
TreeNode right = find(root.right,p,q);
//放在后序位置上,是为了遍历到全部的节点,以便foundP、foundQ不会判断失误。
if(root == p || root == q) {
if(root == p) foundP = true;
if(root == q) foundQ = true;
return root;
}
if(left == null) {
return right;
} else if(right == null) {
return left;
} else {
return root;
}
}
}
5. 二叉搜索树节点值不重复,且p、q一定存在于二叉树中
二叉树的方法可用,但是为了提高效率,需要用到二叉搜索树的性质
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
if(root == null) return null;
if(p.val > q.val) {
return lowestCommonAncestor(root,q,p);
}
//如果root.val比p.val都小,说明p、q都在右子树
if(root.val < p.val) {
return lowestCommonAncestor(root.right,p,q);
}
//如果root.val比q.val都大,说明p、q都在左子树
if(root.val > q.val) {
return lowestCommonAncestor(root.left,p,q);
}
//如果root.val处在p.val和q.val之间,说明分布在左右子树
//if(root.val >= p.val && root.val <= q.val)
return root;
}
}
6. 二叉树节点值不重复,且p、q存在于二叉树中,但是二叉树节点包含指向父节点的指针
暂时还不会,会了补
题目链接
leetcode-222:完全二叉树的节点个数
leetcode-104:二叉树的最大深度
leetcode-111:二叉树的最小深度
leetcode-236:二叉树的最近公共祖先
leetcode-1676:二叉树的最近公共祖先IV
leetcode-1644:二叉树的最近公共祖先II
leetcode-235:二叉搜索树的最近公共祖先
声明:以上内容均来源于对labuladong的算法小抄以及代码随想录相关内容的理解。
环环无敌大可爱
😄