二叉树的经典算法题
一、二叉树里的双指针
双指针就是定义了两个变量,在二叉树中有时候也需要至少定义两个变量才能解决问题,这两个指针可能针对一棵树,也可能针对两棵树,姑且也称之为“双指针”吧。一般是与对称、反转和合并等类型相关。
1.1 判断两棵树是否相同
LeetCode100:给你两棵二叉树的根节点p和q,编写一个函数来检验这两棵树是否相同。如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
可以对两个二叉树同时进行前序遍历,先判断根节点是否相同,如果相同再分别判断左右子节点是否相同,判断的过程中只要有一个不相同就返回false,如果全部相同才会返回true。
注意返回true的条件是p,q结点同时为null
代码如下:
class Solution {
public boolean isSameTree(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 isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
}
}
1.2 对称二叉树
LeetCode101给定一个二叉树,检查它是否是镜像对称的。
true false
因为我们要通过递归函数的返回值来判断两个子树的内侧节点和外侧节点是否相等,所以准确的来说是一个树的遍历顺序是左右中,一个树的遍历顺序是右左中。这里的关键还是如何比较和如何处理结束条件。
单层递归的逻辑就是处理左右节点都不为空,且数值相同的情况。
1.比较二叉树外侧是否对称:传入的是左节点的左孩子,右节点的右孩子。
2.比较内侧是否对称,传入左节点的右孩子,右节点的左孩子。
3.如果左右都对称就返回true,有一侧不对称就返回false。
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root == null) return true;
return judge(root.left, root.right);
}
public boolean judge(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 judge(p.left, q.right) && judge(p.right, q.left);
}
}
1.3 合并二叉树
LeetCod617.给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为NULL的节点将直接作为新二叉树的节点。
两个二叉树的对应节点可能存在以下三种情况,对于每种情况使用不同的合并方式。
1.如果两个二叉树的对应节点都为空,则合并后的二叉树的对应节点也为空;
2.如果两个二叉树的对应节点只有一个为空,则合并后的二叉树的对应节点为其中的非空节点;
3.如果两个二叉树的对应节点都不为空,则合并后的二叉树的对应节点的值为两个二叉树的对应节点的值之和,此时需要显性合并两个节点。
对一个节点进行合并之后,还要对该节点的左右子树分别进行合并。
代码如下:
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if(root1 == null) return root2;
if(root2 == null) return root1;
TreeNode merge = new TreeNode(root1.val + root2.val);
merge.left = mergeTrees(root1.left, root2.left);
merge.right = mergeTrees(root1.right, root2.right);
return merge;
}
}
二、路径专题
2.1 二叉树的所有路径
LeetCode257:给你一个二叉树的根节点root,按任意顺序,返回所有从根节点到叶子节点的路径。叶子节点是指没有子节点的节点。
我们可以注意到有几个叶子节点,就有几条路径,那如何找叶子节点呢?我们知道深度优先搜索就是从根节点开始一直找到叶子结点,我们这里可以先判断当前节点是不是叶子结点,再决定是不是向下走,如果是叶子结点,我们就增加一条路径,就像下面图中这样:
这里还有个问题,当得到一个叶子结点容易,那这时候怎么知道它所在的完整路径是什么呢?例如上图中得到D之后,怎么知道其前面的A和B呢?简单,增加一个String类型的变量中,访问每个节点访问的时候先存到String中,到叶子节点的时候再添加到集合里:
class Solution {
public List<String> binaryTreePaths(TreeNode root) {
List<String> list = new ArrayList();
dfs(root,"",list);
return list;
}
public void dfs(TreeNode root, String path, List<String> list){
if(root == null) return;
if(root.left == null && root.right == null){
list.add(path + root.val);
return;
}
dfs(root.left, path + root.val +"->",list);
dfs(root.right, path + root.val +"->",list);
}
}
2.2 路径总和
上面我们讨论的找所有路径的方法,那我们是否可以再找一下哪条路径的和为目标值呢?
LeetCode112题:给你二叉树的根节点root和一个表示目标和的整数targetSum,判断该树中是否存在根节点到子节点的路径,这条路径上所有节点值相加等于目标和targetSum。叶子节点是指没有子节点的节点。
本题询问是否存在从当前节点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;
}
自己根据上一道题写的
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
Set<Integer> set = new HashSet();
int sum = 0;
dfs(root, set, sum);
if(set.contains(targetSum)) return true;
return false;
}
public void dfs(TreeNode root, Set<Integer> set, int sum){
if(root == null){
return;
}
if(root.left == null && root.right == null){
sum += root.val;
set.add(sum);
return;
}
sum += root.val;
dfs(root.left, set, sum);
dfs(root.right, set, sum);
}
}
三、翻转的妙用
LeetCode:226翻转二叉树,将二叉树整体反转。如下图所示:
根据上图,可以发现想要翻转树,就是把每一个节点的左右孩子交换一下。关键在于遍历顺序,前中后序应该选哪一种遍历顺序?遍历的过程中去翻转每一个节点的左右孩子就可以达到整体翻转的效果。注意只要把每一个节点的左右孩子翻转一下,就可以达到整体翻转的效果。
这是一道很经典的二叉树问题。显然,我们从根节点开始,递归地对树进行遍历,并从叶子节点先开始翻转。如果当前遍历到的节点oot的左右两棵子树都已经翻转,那么我们只需要交换两棵子树的位置,即可完成以root为根节点的整棵子树的翻转。
先看前序交换:
class Solution {
public TreeNode invertTree(TreeNode root) {
if(root == null) return root;
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
invertTree(root.left);
invertTree(root.right);
return root;
}
}
后序交换
class Solution {
public TreeNode invertTree(TreeNode root) {
if(root == null) return root;
invertTree(root.left);
invertTree(root.right);
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
return root;
}
}
这道题目使用前序遍历和后序遍历都可以,主要区别是:前序是先处理当前节点再处理子节点,是自顶向下;后序是先处理子结点最后处理自己,一个是自下而上的。观察下图就明白了:
本题还可以使用层次遍历实现,核心思想是元素出队时,先将其左右两个孩子不是直接入队,而是先反转再放进去,代码如下:
class Solution{
public TreeNode invertTree(TreeNode root){
if(root == null) return null;
//将二叉树中的节点逐层放入队列中,再迭代处理队列中的元素
Qeque<TreeNode> queue = new LinkedList();
queue.add() = root;
while (queue.size() > 0){
//每次都从队列中拿一个节点,并交换这个节点的左右子树
TreeNode node = queue.remove();
TreeNode temp = node.left;
node.left = node.right;
node.right = temp;
//如果当前节点的左子树不为空,则放入队列等待后续处理
if (node.left != null) queue.add(node.left);
//如果当前节点的右子树不为空,则放入队列等待后续处理
if (node.right != null) queue.add(node.right);
}
return root;
}