文章目录
104.二叉树的最大深度
题目大意
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
解题思路
递归来实现。分别递归的计算左子树和右子树的高度,找出他们中的一个最大值,再加上1也就是根节点,就是整棵树的最大深度了。
代码实现
class Solution {
public int maxDepth(TreeNode root) {
if(root==null){
return 0;
}
return Math.max(maxDepth(root.left)+1,maxDepth(root.right)+1);
}
}
110.平衡树
题目大意
给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:
一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。
解题思路
利用递归来解决:
如果左右子树的高度差大于1就不满足平衡二叉树的条件,直接返回false。在满足高度差大于1的情况下,左右子树分别也是平衡二叉树,那么整棵树才是一颗平衡二叉树。
代码实现
class Solution {
public boolean isBalanced(TreeNode root) {
if(root==null){
return true;
}
if(Math.abs(height(root.left)-height(root.right))>1){
return false;
}else if(isBalanced(root.left)&&isBalanced(root.right)){
return true;
}
return false;
}
public int height(TreeNode root){
if(root==null){
return 0;
}
return Math.max(height(root.left),height(root.right))+1;
}
}
543.二叉树的直径
题目大意
给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。
解题思路
一条路径的长度等于该路径上的节点数减去一,所以求路径等效于求节点数的最大值减一。任意一条路径都可以看作以某个节点为起点,从其左儿子和右儿子向下遍历的路径拼接得到。
假如对于某节点的左儿子向下遍历经过的最多节点数为L(2),右儿子向下遍历得到最多的节点数为R(3),那么以该节点为起点的路径经过的节点数最大可以为L+R+1(2+3+1)。则以该节点为起点的最大路径长度为L+R+1-1。
我们记节点node为起点的路径经过节点数的最大值为dnode,那么二叉树的直径就是所有节点dnode的最大值减去1。
算法流程:定义一个递归函数depth(node)计算dnode,函数返回该节点为根的子树的深度。先递归调用左儿子和右儿子求得他们为根的子树的深度L和R,则该节点为根的子树的深度即为max(L,R)+1。该节点的dnode值为L+R+1。
递归搜索每个节点并设一个全局变量ans记录dnode的最大值,最后返回ans-1,即为树的直径。
实现代码
class Solution {
private int ans=0;
public int diameterOfBinaryTree(TreeNode root) {
depth(root);
return ans;
}
//里面已经实现了递归调用
public int depth(TreeNode root){
if(root==null){
return 0;
}
//左子树的高度
int leftDepth=depth(root.left);
//右子树的高度
int rightDepth=depth(root.right);
ans=Math.max(ans,leftDepth+rightDepth);
return Math.max(leftDepth,rightDepth)+1;
}
}
226.翻转二叉树
题目大意
翻转一棵二叉树。
解题思路
利用递归,首先将左子树保存,然后将右子树翻转赋值给左子树,再将保存的左子树翻转赋值给右子树。
代码实现
class Solution {
public TreeNode invertTree(TreeNode root) {
if(root==null){
return null;
}
TreeNode left=root.left;
root.left=invertTree(root.right);
root.right=invertTree(left);
return root;
}
}
617.合并二叉树
题目描述
给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。
你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。
解题思路
(一)递归实现
递归的终止条件是root1的节点为null或者是root2的节点为null。
时间复杂度O(N)
空间复杂度O(h),h为树的高度。
(二)迭代实现
迭代实现用的是广度优先算法,需要借助额外的数据结构来实现,可以借助栈或者队列来实现。
只要两棵树的左节点都不为null,就将他放入到队列中,同理,只要两棵树的右节点都不为null,也放入到队列中。然后不断地将他们在队列中去除,相加。
如果出现树1的left节点为null树2的left节点不为null,就直接将树2的left直接赋值给树1的left。同理,如果树1的right为null而树2的right不为null,就直接将树2的right赋值给树1的right。
时间复杂度:O(N)
空间复杂度:O(N),对于满二叉树,要保存所有的叶子节点,需要N/2个节点。
代码实现
(一)递归实现
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
//dfs
if(root1==null&&root2==null){
return null;
}
return dfs(root1,root2);
}
public TreeNode dfs(TreeNode root1,TreeNode root2){
if(root1==null||root2==null){
return root1==null?root2:root1;
}
root1.val+=root2.val;
root1.left=dfs(root1.left,root2.left);
root1.right=dfs(root1.right,root2.right);
return root1;
}
}
(二)迭代实现(广度优先算法)
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
//迭代实现
if(root1==null||root2==null){
return root1==null?root2:root1;
}
LinkedList<TreeNode> queue=new LinkedList<>();
queue.add(root1);
queue.add(root2);
while(queue.size()>0){
TreeNode r1=queue.remove();
TreeNode r2=queue.remove();
r1.val+=r2.val;
if(r1.left!=null&&r2.left!=null){
queue.add(r1.left);
queue.add(r2.left);
}else if(r1.left==null){
r1.left=r2.left;
}
if(r1.right!=null&&r2.right!=null){
queue.add(r1.right);
queue.add(r2.right);
}else if(r1.right==null){
r1.right=r2.right;
}
}
return root1;
}
}
112. 路经总和
题目大意
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum ,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和 targetSum 。
叶子节点 是指没有子节点的节点。
解题思路
注意终止条件的设定是targetSum等于当前根节点的值,而不是0。
代码实现
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
if(root==null){
return false;
}
if(root.left==null&&root.right==null&&targetSum==root.val){
return true;
}
return hasPathSum(root.left,targetSum-root.val)||hasPathSum(root.right,targetSum-root.val);
}
}
437.路径总和III
题目大意
给定一个二叉树,它的每个结点都存放着一个整数值。
找出路径和等于给定数值的路径总数。
路径不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
二叉树不超过1000个节点,且节点数值范围是 [-1000000,1000000] 的整数。
解题思路
(一)递归实现
(二)前缀和
前缀和就是到达当前元素的路径上,之前所有元素的和。
如果前缀总和为currSum,在节点A和节点B处相差target,则位于节点A和节点B之间的元素之和为target。
如果一条路径上存在环,那么前缀法就不能用了。
抵达当前节点(即节点B)后,将前缀和累加,然后查找在前缀和上有没有前缀和currSum-target的节点(即A节点),存在即表示从到B有一条路径之和满足条件的情况。
结果加上满足前缀和currSum-target的节点的数量。然后递归进入左右子树。
左右子树遍历完成之后,回到当前层,需要把当前节点添加的前缀和去除,避免回溯之后影响上一层,因为思想是前缀和,不属于前缀的,我们就要去掉他。
(1)前缀和定义
一个节点的前缀和就是该节点到根之间的路径和。
(2)前缀对于本题的作用
题目要求给出路径和等于给定数值的路径总数,而两节点间的路径和=两节点的前缀和之差。所以我们遍历整棵树一次,记录每个节点的前缀和,并查询该节点的祖先节点中符合条件的个数,将这个数量加到最终的结果上。
(3)HashMap中存的是什么
HashMap的key是前缀和,value是该前缀和的数量,记录数量是因为有出现复数路径的可能。
(4)恢复状态的意义
由于题目要求:路径方向必须是向下的,所以只能是从父节点到子节点。
当我们讨论两个节点的前缀差值时,有一个前提:一个节点必须是另一个节点的祖先节点换句话说,当我们把一个节点的前缀和信息更新到map里时,它应当只对子节点们有效。状态恢复代码的作用就是:在遍历完一个节点的所有子节点后,将其从map中除去。
getOrDefault() 方法获取指定 key 对应对 value,如果找不到 key ,则返回设置的默认值。
代码实现
(一)递归实现
class Solution {
public int pathSum(TreeNode root, int sum) {
if(root==null){
return 0;
}
int count=0;
count=pathSumStartWithRoot(root,sum)+pathSum(root.left,sum)+pathSum(root.right,sum);
return count;
}
public int pathSumStartWithRoot(TreeNode root,int sum){
int ans=0;
if(root==null){
return 0;
}
if(sum==root.val){
ans++;
}
ans+= pathSumStartWithRoot(root.left,sum-root.val)+pathSumStartWithRoot(root.right,sum-root.val);
return ans;
}
}
(二)前缀法
class Solution {
//key表示前缀和,value表示前缀和出现的次数
Map<Integer,Integer> prefixMap;
int target;
public int pathSum(TreeNode root, int sum) {
prefixMap=new HashMap<>();
target=sum;
prefixMap.put(0,1);
//找以root为根节点,和为0的路径数
return recur(root,0);
}
private int recur(TreeNode node,int curSum){
if(node==null){
return 0;
}
//res用来记录路径数
int res=0;
//curSum用来记录当前的前缀和
curSum+=node.val;
//获取指定key对应的value
//获取前缀和为curSum-target的节点数,当前节点的前缀和为curSum
res+=prefixMap.getOrDefault(curSum-target,0);
//将前缀和为curSum的节点数加一
prefixMap.put(curSum,prefixMap.getOrDefault(curSum,0)+1);
//求左孩子的前缀和为curSum的节点数目
int left=recur(node.left,curSum);
//求右孩子的前缀和为curSum的节点数目
int right=recur(node.right,curSum);
//res记录前缀和为curSum的总的节点的数目
res=res+left+right;
//状态恢复
prefixMap.put(curSum,prefixMap.get(curSum)-1);
return res;
}
}
572.另一个树的子树
题目大意
给定两个非空二叉树 s 和 t,检验 s 中是否包含和 t 具有相同结构和节点值的子树。s 的一个子树包括 s 的一个节点和这个节点的所有子孙。s 也可以看做它自身的一棵子树。
解题思路
递归来实现,如果根节点相同,就继续判断下面的节点,如果不同就判断是不是左子树或者右子树的子树。
代码实现
class Solution {
public boolean isSubtree(TreeNode s, TreeNode t) {
if(s==null){
return false;
}
return isSame(s,t)||isSubtree(s.left,t)||isSubtree(s.right,t);
}
public boolean isSame(TreeNode s,TreeNode t){
if(s==null&&t==null){
return true;
}else if(s==null||t==null){
return false;
}
if(s.val!=t.val){
return false;
}
return isSame(s.left,t.left)&&isSame(s.right,t.right);
}
}
101.对称二叉树
题目大意
给定一个二叉树,检查它是否是镜像对称的。
解题思路
递归来解决:递归的来判断左右子树的值是否相同。
代码实现
class Solution {
public boolean isSymmetric(TreeNode root) {
if(root==null){
return true;
}
return isSame(root.left,root.right);
}
public boolean isSame(TreeNode root1,TreeNode root2){
if(root1==null&&root2==null){
return true;
}else if(root1==null||root2==null){
return false;
}
if(root1.val!=root2.val){
return false;
}
return isSame(root1.left,root2.right)&&isSame(root1.right,root2.left);
}
}
111.二叉树的最小深度
题目大意
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明:叶子节点是指没有子节点的节点。
解题思路
递归来解决:递归求左右子树的最小高度,有一点需要注意的是,当整棵树只存在左子树或者右子树的时候,需要特殊处理一下,不能只返回两棵子树中的最小值了,因为会导致结果为0。
if(root.left==null||root.right==null){
return left+right+1;
}
代码实现
class Solution {
public int minDepth(TreeNode root) {
if(root==null){
return 0;
}
int left=minDepth(root.left);
int right=minDepth(root.right);
if(root.left==null||root.right==null){
return left+right+1;
}
return Math.min(left,right)+1;
}
}
404.左叶子之和
题目大意
计算给定二叉树的所有左叶子之和。
解题思路
判断左子树是否是一个叶子节点,如果是的话就将左叶子节点的值和右子树的左叶子节点的值,如果不是叶子节点的话就递归的将左右子树左叶子节点的值相加。
代码实现
class Solution {
public int sumOfLeftLeaves(TreeNode root) {
if(root==null){
return 0;
}
if(isLeaf(root.left)){
return root.left.val+sumOfLeftLeaves(root.right);
}
return sumOfLeftLeaves(root.left)+sumOfLeftLeaves(root.right);
}
public boolean isLeaf(TreeNode root){
if(root==null){
return false;
}
if(root.left==null&&root.right==null){
return true;
}
return false;
}
}
687.最长同值路径
题目大意
给定一个二叉树,找到最长的路径,这个路径中的每个节点具有相同值。 这条路径可以经过也可以不经过根节点。
解题思路
递归的找到左右子树的具有相同值的最长路径。
代码实现
class Solution {
int ans=0;
public int longestUnivaluePath(TreeNode root) {
if(root==null){
return 0;
}
arrowLength(root);
return ans;
}
//返回从root节点延伸出得最长箭头得长度
public int arrowLength(TreeNode root){
if(root==null){
return 0;
}
int left=arrowLength(root.left);
int right=arrowLength(root.right);
int leftLength=0;
int rightLength=0;
if(root.left!=null&&root.val==root.left.val){
leftLength+=left+1;
}
if(root.right!=null&&root.val==root.right.val){
rightLength+=right+1;
}
ans=Math.max(ans,leftLength+rightLength);
return Math.max(leftLength,rightLength);
}
}
337.打家劫舍 III
题目大意
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
解题思路
(一)暴力递归-最优子结构
(二)针对重复子问题进行优化
斐波那契数列使用记忆化进行优化,使用数组将每次得计算结果保存起来,下次如果再来计算就在缓存中取,不再计算了,这样保证了每个数字只计算一次。由于二叉树不适合拿数组来当缓存,所以使用哈希表来存储结果,TreeNode当作key,能偷的钱当作value。
(三)终极解法
上面两种解法用到了孙子节点,计算爷爷节点能偷的钱还要同时去计算孙子节点投的钱,虽然有了记忆化,但是还是有性能损耗。
定义一个长度为2的数组:int[] res=new int[2];0代表不偷,1代表偷,任何一个节点能偷到的最大钱的状态可以定义为:
- 当前节点选择不偷:当前节点能偷到的最大钱数=左孩子能偷到的最大钱数(不管偷不偷)+右孩子能偷到的最大钱数(不管偷不偷)。
- 当前节点选择偷:当前节点能偷到的最大钱数=左孩子选择不偷时能得到的最大钱数+右孩子选择不偷时能得到的最大钱数。
代码实现
(一)暴力递归-最优子结构
class Solution {
public int rob(TreeNode root) {
if(root==null){
return 0;
}
int val1=root.val;
if(root.left!=null){
val1+=rob(root.left.left)+rob(root.left.right);
}
if(root.right!=null){
val1+=rob(root.right.left)+rob(root.right.right);
}
int val2=rob(root.left)+rob(root.right);
return Math.max(val1,val2);
}
}
(二)针对重复子问题进行优化
class Solution {
public int rob(TreeNode root) {
//定义一个HashMap,其中key值存放节点,value存放盗取的最高金额
HashMap<TreeNode,Integer> map=new HashMap<>();
return robInternal(root,map);
}
public int robInternal(TreeNode root,HashMap<TreeNode,Integer> map){
if(root==null){
return 0;
}
if(map.containsKey(root)){
return map.get(root);
}
int val1=root.val;
if(root.left!=null){
val1+=robInternal(root.left.left,map)+robInternal(root.left.right,map);
}
if(root.right!=null){
val1+=robInternal(root.right.left,map)+robInternal(root.right.right,map);
}
int val2=robInternal(root.left,map)+robInternal(root.right,map);
int res=Math.max(val1,val2);
map.put(root,res);
return res;
}
}
(三)终极版
class Solution {
public int rob(TreeNode root) {
int[] res=robInternal(root);
return Math.max(res[0],res[1]);
}
public int[] robInternal(TreeNode root){
if(root==null){
return new int[2];
}
int[] left=robInternal(root.left);
int[] right=robInternal(root.right);
int[] res=new int[2];
//当前节点选择不偷
res[0]=Math.max(left[0],left[1])+Math.max(right[0],right[1]);
//当前节点选择偷
res[1]=left[0]+right[0]+root.val;
return res;
}
}
671.二叉树中第二小的节点
题目大意
给定一个非空特殊的二叉树,每个节点都是正数,并且每个节点的子节点数量只能为 2 或 0。如果一个节点有两个子节点的话,那么该节点的值等于两个子节点中较小的一个。
更正式地说,root.val = min(root.left.val, root.right.val) 总成立。
给出这样的一个二叉树,你需要输出所有节点中的第二小的值。如果第二小的值不存在的话,输出 -1 。
解题思路
根节点永远保存整棵树的最小值。
如果根节点的值左子节点的值相等,就去找左子树的第二最小值,如果不存在就直接返回右子节点的值。如果存在就去找左右子树中的较小的那个第二最小值。同理对于右子树也是同样的做法。
代码实现
class Solution {
public int findSecondMinimumValue(TreeNode root) {
//根节点永远保存整棵树的最小值
if(root==null){
return -1;
}
if(root.left==null&&root.right==null){
return -1;
}
int leftVal=root.left.val;
int rightVal=root.right.val;
//求左子树的第二小值
if(root.val==leftVal){
leftVal=findSecondMinimumValue(root.left);
}
//求右子树的第二小值
if(root.val==rightVal){
rightVal=findSecondMinimumValue(root.right);
}
if(leftVal!=-1&&rightVal!=-1){
return Math.min(leftVal,rightVal);
}
if(leftVal==-1){
return rightVal;
}
return leftVal;
}
}