关卡名 |
二叉树的经典面试问题 |
我会了✔️ |
|
1.判断两棵树是否相同 |
✔️ |
2.判断两个树是否对称 |
✔️ | |
3.合并两颗二叉树 |
✔️ | |
4.寻找二叉树的所有路径 |
✔️ | |
5.路径总和问题 |
✔️ | |
6.翻转二叉树 |
✔️ |
所谓的双指针就是定义了两个变量,在二叉树中有时候也需要至少定义两个变量才能解决问题,这两个指针可能针对一棵树,也可能针对两棵树,我们姑且也称之为“双指针”吧。这些问题一般是与对称、反转和合并等类型相关,我们接下来就看一下相关高频问题。
LeetCode100:给你两棵二叉树的根节点 p 和 q ,编写一个函数来检验这两棵树是否相同。如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
示例1:
输入:p = [1,2,3], q = [1,2,3]
输出:true
示例2:
输入:p = [1,2], q = [1,null,2]
输出:false
这个貌似就是两个二叉树同时进行前序遍历,先判断根节点是否相同, 如果相同再分别判断左右子节点是否相同,判断的过程中只要有一个不相同就返回 false,如果全部相同才会返回true。其实就是这么回事。看代码:
public boolean isSameTree(TreeNode p, TreeNode q) {
//如果都为空我们就认为他是相同的
if (p == null && q == null)
return true;
//如果一个为空,一个不为空,很明显不可能是相同的树,直接返回false即可
if (p == null || q == null)
return false;
//如果对应位置的两个结点的值不相等,自然也不是同一个棵树
if (p.val != q.val)
return false;
//走到这一步说明节点p和q是完全相同的,接下来需要再判断其左左和右右是否满足要求了
return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}
LeetCode101 给定一个二叉树,检查它是否是镜像对称的。例如下面这个就是对称二叉树:
1
/ \
2 2
/ \ / \
3 4 4 3
但是下面这个 [1,2,2,null,3,null,3] 则不是对称的:
1
/ \
2 2
\ \
3 3
如果树是镜像的,下面这个图更直观一些:
因为我们要通过递归函数的返回值来判断两个子树的内侧节点和外侧节点是否相等,所以准确的来说是一个树的遍历顺序是左右中,一 个树的遍历顺序是右左中。这里的关键还是如何比较和如何处理结束条件。 单层递归的逻辑就是处理左右节点都不为空,且数值相同的情况。
接下来就是合并和进一步简化:
class Solution {
//主方法
public boolean isSymmetric(TreeNode root) {
if(root==null){
return true;
}
return check(root.left, root.right);
}
public boolean check(TreeNode p,TreeNode q){
if (p == null && q == null) {
return true;
}
if (p == null || q == null) {
return false;
}
if( p.val != q.val){
return false;
}
return check(p.left, q.right) && check(p.right, q.left);
}
}
输入:
Tree 1: Tree 2:
1 2
/ \ / \
3 2 1 3
/ \ \
5 4 7
输出合并后的树:
3
/ \
4 5
/ \ \
5 4 7
两个二叉树的对应节点可能存在以下三种情况,对于每种情况使用不同的合并方式。
对一个节点进行合并之后,还要对该节点的左右子树分别进行合并:
public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
if (t1 == null) {
return t2;
}
if (t2 == null) {
return t1;
}
TreeNode merged = new TreeNode(t1.val + t2.val);
merged.left = mergeTrees(t1.left, t2.left);
merged.right = mergeTrees(t1.right, t2.right);
return merged;
}
如果这个感觉还是想不明白,可以直接对照题干给的例子来验证一下。
最后我们来造个题:前面我们研究了两棵树相等和一棵树对称的情况,我们可以造一道题,判断两棵树是否对称的。如下就是一个对称的二叉树,那该如何写代码实现呢?请你思考。
思考后发现:其实就是1.2的对称,劈成两半
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
public boolean isSymmetric(TreeNode root1, TreeNode root2) {
if (root1 == null && root2 == null) {
return true;
}
if (root1 == null || root2 == null) {
return false;
}
if (root1.val != root2.val) {
return false;
}
return isSymmetric(root1.left, root2.right) && isSymmetric(root1.right, root2.left);
}
关于二叉树有几道与路径有关的题目,我们统一看一下。初次接触你会感觉有些难,但是这是在为回溯打基础,因为很多回溯问题就是在找路径,甚至要找多条路径。
LeetCode257:给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。
叶子节点是指没有子节点的节点。
输入:root = [1,2,3,null,5]
输出:["1->2->5","1->3"]
我们可以注意到有几个叶子节点,就有几条路径,那如何找叶子节点呢?我们知道深度优先搜索就是从根节点开始一直找到叶子结点,我们这里可以先判断当前节点是不是叶子结点,再决定是不是向下走,如果是叶子结点,我们就增加一条路径,就像下面图中这样:
这里还有个问题,当得到一个叶子结点容易,那这时候怎么知道它所在的完整路径是什么呢?例如上图中得到D之后,怎么知道其前面的A和B呢?简单,增加一个String类型的变量中,访问每个节点访问的时候先存到String中,到叶子节点的时候再添加到集合里 :
public List binaryTreePaths(TreeNode root) {
List res = new ArrayList<>();
dfs(root, "", res);
return res;
}
void dfs(TreeNode root, String path, List res) {
if (root == null) return;
if (root.left == null && root.right == null) {
res.add(path + root.val);
return;
}
dfs(root.left, path + root.val + "->", res);
dfs(root.right, path + root.val + "->", res);
}
本题可以作为回溯的入门问题,我们到回溯时再讲解。
上面我们讨论的找所有路径的方法,那我们是否可以再找一下哪条路径的和为目标值呢?这就是LeetCode112题:给你二叉树的根节点 root 和一个表示目标和的整数 targetSum ,判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum。叶子节点 是指没有子节点的节点。
输入:root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22
输出:true
输入:root = [1,2,3], targetSum = 5
输出:false
本题询问是否存在从当前节点 root 到叶子节点的路径,满足其路径和为 sum,假定从根节点到当前节点的值之和为 val,我们可以将这个大问题转化为一个小问题:是否存在从当前节点的子节点到叶子的路径,满足其路径和为 sum - val。
不难发现这满足递归的性质,若当前节点就是叶子节点,那么我们直接判断 sum 是否等于 val 即可(因为路径和已经确定,就是当前节点的值,我们只需要判断该路径和是否满足条件)。若当前节点不是叶子节点,我们只需要递归地询问它的子节点是否能满足条件即可。
public boolean hasPathSum(TreeNode root, int sum) {
if (root == null) {
return false;
}
if (root.left == null && root.right == null) {
return sum == root.val;
}
boolean left= hasPathSum(root.left, sum - root.val) ;
boolean right=hasPathSum(root.right, sum - root.val);
return left|| right;
}
本题还有个拓展,既然找到是否存在路径了,那能把找到的路径打印出来吗?当然可以,这就是LeetCode113题,我们将本题做为入门题在回溯部分讲解。
来看 LeetCode226 翻转二叉树,将二叉树整体反转。如下图所示:
这个题也是剑指offer27题的要求,根据上图,可以发现想要翻转树,就是把每一个节点的左右孩子交换一下。关键在于遍历顺序,前中后序应该选哪一种遍历顺序。遍历的过程中去翻转每一个节点的左右孩子就可以达到整体翻转的效果。注意只要把每一个节点的左右孩子翻转一下,就可以达到整体翻转的效果。
这是一道很经典的二叉树问题。显然,我们从根节点开始,递归地对树进行遍历,并从叶子节点先开始翻转。如果当前遍历到的节点 root 的左右两棵子树都已经翻转,那么我们只需要交换两棵子树的位置,即可完成以 root 为根节点的整棵子树的翻转。
先看前序交换:
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
TreeNode temp=root.left;
root.left=root.right;
root.right=temp;
invertTree(root.left);
invertTree(root.right);
return root;
}
再看后序:
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
TreeNode left = invertTree(root.left);
TreeNode right = invertTree(root.right);
root.left = right;
root.right = left;
return root;
}
这道题目使用前序遍历和后序遍历都可以,主要区别是是前序是先处理当前节点再处理子节点,是自顶向下,后序是先处理子结点最后处理自己,一个是自下而上的。观察下图就明白了:
本题还可以使用层次遍历实现,核心思想是元素出队时,先将其左右两个孩子不是直接入队,而是先反转再放进去,代码如下:
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) {
return null;
}
//将二叉树中的节点逐层放入队列中,再迭代处理队列中的元素
LinkedList queue = new LinkedList();
queue.add(root);
while (!queue.isEmpty()) {
//每次都从队列中拿一个节点,并交换这个节点的左右子树
TreeNode tmp = queue.poll();
TreeNode left = tmp.left;
tmp.left = tmp.right;
tmp.right = left;
//如果当前节点的左子树不为空,则放入队列等待后续处理
if (tmp.left != null) {
queue.add(tmp.left);
}
//如果当前节点的右子树不为空,则放入队列等待后续处理
if (tmp.right != null) {
queue.add(tmp.right);
}
}
return root;
}
}