目录
力扣 222. 完全二叉树的节点个数
有三种解法,前两种是普通的递归和迭代法,第三种是针对完全二叉树特点的解法,时空复杂度相对于前两种更优。
//递归法(时间O(n),空间O(logn)--递归工作栈)
class Solution {
public int countNodes(TreeNode root) {
if(root==null)return 0;
return 1+countNodes(root.left)+countNodes(root.right);
}
}
//迭代法(层序遍历,时空效率均为O(n))
class Solution {
public int countNodes(TreeNode root) {
if(root==null)return 0;
int res=0;
Queue<TreeNode>que=new LinkedList<>();
que.offer(root);
while(!que.isEmpty()){
int len=que.size();
while(len-->0){
TreeNode cur=que.poll();
res++;
if(cur.left!=null)que.offer(cur.left);
if(cur.right!=null)que.offer(cur.right);
}
}
return res;
}
}
//利用完全二叉树的特点(时间O(logn*logn),空间O(logn))
class Solution {
public int countNodes(TreeNode root) {
if(root==null)return 0;
int lefthigh=getDepth(root.left);
int righthigh=getDepth(root.right);
//若左右子树高度相同,此时左子树一定是满二叉树,左子树结点数:2^lefthigh-1;加上当前根节点root的1个,为2^lefthigh;最后加上右子树的结点数。
if(lefthigh==righthigh)return (1<<lefthigh)+countNodes(root.right);
//若左右子树高度不同,说明右子树比左子树少一层,但是右子树一定是满二叉树,同理右子树结点数加根结点为2^righthigh,最后加上左子树结点数;
else return (1<<righthigh)+countNodes(root.left);
}
//计算当前结点所具有的高度,完全二叉树高度只要一直遍历左孩子就行
public int getDepth(TreeNode root){
int depth=0;
while(root!=null){
depth++;
root=root.left;
}
return depth;
}
}
力扣 110. 平衡二叉树
对于这道题,需要区分好二叉树的高度和深度。
二叉树的高度:该结点到叶子结点的最长的简单路径边的条数。
二叉树的深度:从根节点到该结点的最长最长简单路径边的条数。
如图(引用自代码随想录公众号):
即高度是指从当前结点往下走到叶子结点的长度,深度是指从根结点往下走到当前结点的长度。
力扣中认为根结点的深度为1,暂且以leetcode为准。在力扣104.二叉树的最大深度一题中,根结点的最大深度其实就是相当于根结点的最大高度。
求深度可以从上到下去查,需要前序遍历;求高度只能从下到上(因为自顶向下的遍历会造成重复的递归遍历),需要用后序遍历。
这题用递归即可,用迭代法性能不高,虽然优化后时间复杂度也可以到O(n),但是比较麻烦。
思路:需要定义一个求结点高度的函数,对二叉树进行自底向上的递归遍历,自底向上递归的做法类似于后序遍历,对于当前遍历到的节点,先递归地判断其左右子树是否平衡,再判断以当前节点为根的子树是否平衡。如果一棵子树是平衡的,则返回其高度(高度一定是非负整数),否则返回 −1。如果存在一棵子树不平衡,则整个二叉树一定不平衡。
class Solution {
public boolean isBalanced(TreeNode root) {
//只要出现不平衡的情况,就会返回-1,平衡则返回根节点高度;所以只要判断返回值是非负就行了
return getHeight(root)>=0;
}
//计算当前结点高度,对于不平衡情况,返回高度没意义;平衡情况返回当前结点高度
public int getHeight(TreeNode root){
if(root==null)return 0;
//先递归求左右子树的高度,再判断当前结点是否平衡
int lheight=getHeight(root.left);
int rheight=getHeight(root.right);
//当左/右子树不平衡,或当前结点为根的树不平衡,则不是平衡的
if(lheight==-1||rheight==-1||Math.abs(lheight-rheight)>1)return -1;
//否则当前结点对应的树是平衡的,计算当前结点的高度
else return 1+Math.max(lheight,rheight);
}
}
力扣 257. 二叉树的所有路径
class Solution {
//res存储最终结果,每一条path都为String类型(最终结果是很多个条path)
List<String> res = new ArrayList<>();
//path存储记录遍历到的路径(即结点的值),每一项为Integer类
List<Integer> path = new ArrayList<>();
public List<String> binaryTreePaths(TreeNode root) {
traversal(root);
return res;
}
//遍历函数
public void traversal(TreeNode root){
//当前结点为空,返回上一层
if (root == null)return;
//经过前面的判断,当前结点不为空,这条路径里面加入当前结点值
path.add(root.val);
//若当前结点为叶子结点
if (root.left == null && root.right == null){
//sb作为字符缓冲区,拼接路径字符
StringBuilder sb = new StringBuilder();
//遍历path(path里面只有结点值)
for (int i = 0; i < path.size(); i++){
//不是第一个数,后面要加上->符号
if (i != 0)
sb.append("->");
//StringBuilder中的append方法可以拼接各种类型的字符加入sb
sb.append(path.get(i));//get获取path中存储的结点值
}
//走到叶子结点了,这一条路径结束,添加进结果集
//注意StringBuilder类最终需要用toString()变成String类
res.add(sb.toString());
}
//递归遍历左右子树
//如果下一层递归的是空结点,在递归最开始就会return回这层递归
traversal(root.left);
traversal(root.right);
//回溯,走到底之后,删掉叶子结点值,回退到上一层结点
path.remove(path.size() - 1);
}
}
力扣 404. 左叶子之和
重点:题目中的左叶子定义其实是结点左子结点不为空,且左子节点的左右结点为空,那么这个结点就为左叶子。即如下逻辑:
root.left!=null&&root.left.left==null&&root.left.right==null
一、递归法
按照递归三部曲:
1.确定递归参数和返回值:
需要求左叶子结点之和,参数应该为根节点,返回值为int类型,即:
int sumOfLeftLeaves(TreeNode root){}
2.确定终止条件:
走到root为null就停止,并且当前值为0,即:
if(root==null)return 0;
3.单层递归逻辑:
遇到左叶子结点的时候,记录下左叶子结点的值(root.left.val),并且递归右孩子(左孩子已经是左叶子了,没必要递归下去);如果不是左叶子,则递归左右孩子;即:
if(root.left!=null&&root.left.left==null&&root.left.right==null){
res=root.left.val;
return res+sumOfLeftLeaves(root.right);
}else{
return res+sumOfLeftLeaves(root.left)+sumOfLeftLeaves(root.right);
}
完整递归代码如下:
//递归方法
class Solution {
public int sumOfLeftLeaves(TreeNode root) {
if(root==null)return 0;
int res=0;
//增加if-else的不同返回值可以提高递归的效率(内存消耗变少)
//当前根结点的左孩子为左叶子结点,记录结果,并且只要再递归判断右孩子就行
//否则就说明当前结点的左孩子不是左叶子结点,需要递归左右孩子继续判断
if(root.left!=null&&root.left.left==null&&root.left.right==null){
res=root.left.val;
return res+sumOfLeftLeaves(root.right);
}else{
return res+sumOfLeftLeaves(root.left)+sumOfLeftLeaves(root.right);
}
}
}
//时空复杂度均为O(n)
二、迭代法
采用层序遍历模板,在左节点入队时特殊处理即可。
//迭代方法(层序遍历模板)
class Solution {
public int sumOfLeftLeaves(TreeNode root) {
int res=0;
Queue<TreeNode>que=new LinkedList<>();
que.offer(root);
while(!que.isEmpty()){
TreeNode cur=que.poll();
//左节点不为空,还需要判断左节点是否为叶子
if(cur.left!=null){
if(cur.left.left==null&&cur.left.right==null)
res+=cur.left.val;
else que.offer(cur.left);
}
if(cur.right!=null)
que.offer(cur.right);
}
return res;
}
}
//时空复杂度均为O(n)
力扣 513. 找树左下角的值
这题用层序遍历的思想来做会比较容易,并且代码简单:
//递归法(深度优先搜索)
class Solution {
//res记录结果,maxDepth记录当前遍历到的最大深度
int res=0,maxDepth=-1;
public int findBottomLeftValue(TreeNode root) {
dfs(root,1);//根节点深度为1
return res;
}
//深度优先遍历
public void dfs(TreeNode root,int depth){
if(root==null)return;
//如果depth大于maxDepth,说明深度更新,进入下一层
//由于先递归左节点,因此每一层最先更新深度的一定是最左的结点
//记录这层里面第一个遍历到的结点的值,就是结果;
//后面如果还有遍历到这一层的其他的结点,不会满足这个if条件,res不会更新
if(depth>maxDepth){
maxDepth=depth;
res=root.val;
}
//让左节点优先于右结点遍历,这样在递归下一层的时候,深度+1
//res会记录深度最大的第一个结点
dfs(root.left,depth+1);
dfs(root.right,depth+1);
}
}
//迭代法(层序遍历)
class Solution {
public int findBottomLeftValue(TreeNode root) {
Queue<TreeNode>que=new LinkedList<>();
que.offer(root);
TreeNode cur=new TreeNode(-1);
while(!que.isEmpty()){
cur=que.poll();
//这里先让右结点入队,这样下一层中也是右边的结点先出队
//可以保证最后一个出队的一定是最下面、最左边的结点
if(cur.right!=null)que.offer(cur.right);
if(cur.left!=null)que.offer(cur.left);
}
return cur.val;
}
}
力扣 112. 路径总和
原题链接
递归思想和力扣 513. 找树左下角的值这题的递归方法很像,递归函数传参,通过对参数的改变实现回溯;
//递归法
class Solution {
boolean res = false;
public boolean hasPathSum(TreeNode root, int targetSum) {
if (root == null)return false;
dfs(root,targetSum);
return res;
}
private void dfs(TreeNode root,int targetSum) {
if (root == null)return;
//遍历到叶子结点,并且计数器值等于叶子结点的值,说明存在这条路径
if (root.left == null && root.right == null&&targetSum == root.val)
res = true;
//“targetSum-root.val”这样的传参包含回溯的思想
//因为targetSum值并没有改变,若最后发现这条路径不符合会回退
dfs(root.left,targetSum - root.val);
dfs(root.right,targetSum - root.val);
}
}
//迭代双栈版本
//不改变结点的值,而是用另一个栈记录遍历路径的累加值
//结点和到该点的累加值一起操作(一起出入栈)
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
if(root==null)return false;
Stack<TreeNode> stack1 = new Stack<>();
Stack<Integer> stack2 = new Stack<>();
stack1.push(root);stack2.push(root.val);
while(!stack1.isEmpty()){
int size = stack1.size();
for(int i=0;i<size;i++){
TreeNode node = stack1.pop();
int sum=stack2.pop();
// 如果该节点是叶子节点了,同时该节点的路径数值等于sum,那么就返回true
if(node.left==null && node.right==null && sum==targetSum)return true;
//如果还有孩子结点,继续往下走
// 右节点,压进去一个节点的时候,将该节点的路径数值也记录下来
if(node.right!=null){
stack1.push(node.right);stack2.push(sum+node.right.val);
}
// 左节点,压进去一个节点的时候,将该节点的路径数值也记录下来
if(node.left!=null){
stack1.push(node.left);stack2.push(sum+node.left.val);
}
}
}
return false;
}
}
//迭代版本(单队列,更省空间)
//层序遍历,在遍历过程中改变结点的值
//每个结点的值代表从根结点到这个结点的累加值
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
if(root==null)return false;
Queue<TreeNode>que=new LinkedList<>();
que.offer(root);
while(!que.isEmpty()){
int len = que.size();
while(len-->0){
TreeNode cur = que.poll();
//如果该节点是叶子节点
//同时到该节点的路径累加值等于sum,那么存在这条路径
if(cur.left==null&&cur.right==null&&cur.val==targetSum)
return true;
//如果有左孩子,入队,更新累加路径值到左孩子
if(cur.left!=null){
que.offer(cur.left);
cur.left.val+=cur.val;
}
if(cur.right!=null){
que.offer(cur.right);
cur.right.val+=cur.val;
}
}
}
return false;//遍历结束都没有找到路径,则不存在
}
}
力扣 113. 路径总和 II
递归方法:用一个累加值记录到每个结点的路径累加值,并且把结点值加入路径,判断当前结点若是叶子结点时,累加值是否等于目标总和,是的话则把路径加入res中;否则继续向下遍历,走到最后都不符合则返回上一层递归(走到空节点都不符合直接返回,走到叶子结点不符合则累加值减掉当前叶子结点值,并且路径去掉这个结点,回到上一层继续递归遍历);
需要注意的是,List是引用类型,在将path加入res之前需要创建副本,把副本加入res;否则后续对path的修改会改变原来已经加入res的数据;
//递归法
class Solution {
List<List<Integer>> res=new ArrayList<>();
List<Integer>path =new ArrayList<>();
int sum=0;
public List<List<Integer>> pathSum(TreeNode root, int targetSum){
dfs(root,targetSum);
return res;
}
public void dfs(TreeNode root,int targetSum){
if(root==null)return ;
//先加入当前路径
sum+=root.val;
path.add(root.val);
//List是引用类型,添加进res数组后
//对path修改还是会改变已经加入res的path内容,所以需要拷贝一份
if(root.left==null&&root.right==null&&sum==targetSum)
res.add(new ArrayList<Integer>(path));
//不用先判空,因为进入下一层递归,在最开始会判空,直接返回
dfs(root.left,targetSum);
dfs(root.right,targetSum);
//下一层递归结束没找到路径,说明当前结点的这条路径不行
//删掉当前结点值,回退一步
sum-=root.val;
path.remove(path.size()-1);
}
}
力扣 106. 从中序与后序遍历序列构造二叉树
//从中序到后序遍历构造二叉树
/*思路:6步走
1、数组大小为0的时候,空节点,数组大小为1的时候,返回节点
2、数组不为空的时候,取后序数组的最后一个为节点
3、找到后序数组最后一个元素在中序数组的位置
4、根据找到的位置切割中序数组
5、同理,切割后序数组
6、递归左右子树
*/
/** */
class Solution {
Map<Integer,Integer> inorderMap = new HashMap<Integer,Integer>();
public TreeNode buildTree(int[] inorder, int[] postorder) {
//对中序数组建立哈希表,方便根据结点值快速定位其在中序数组索引位置
//若不用哈希表,在每次递归中都要遍历中序数组定位分割点位置,时间复杂度高
for (int i=0;i<inorder.length;i++)inorderMap.put(inorder[i], i);
return buildTree1(inorder,0,inorder.length,postorder,0,postorder.length);
}
public TreeNode buildTree1(int [] inorder,int inleft,int inright,int [] postorder,int postleft,int postright){
//1、分割出来的中序数组区间大小为0的时候,返回空节点
if(inright-inleft<1) return null;
//分割的中序数组区间只有一个元素时,为叶子结点,值为中序数组左区间
if(inright-inleft==1) return new TreeNode(inorder[inleft]);
//2、数组区间大于1的情况下,取后序数组最后一个元素根节点
int rootvalue=postorder[postright-1];
TreeNode root=new TreeNode(rootvalue);
//3、根据结点值,找到rootvalue在中序数组的位置(切割点)
int rootindex=inorderMap.get(rootvalue);
//4-6、切分中序、后序数组,递归
//中序数组切割左区间(左闭右开)
//后序数组左区间(大小和中序数组左区间一样)
root.left=buildTree1(inorder,inleft,rootindex,postorder,postleft,postleft+(rootindex-inleft));
//中序数组(左闭右开)切割右区间
//后序数组右区间,去掉最后一个元素(当前的根)
root.right=buildTree1(inorder,rootindex+1,inright,postorder,postleft+(rootindex-inleft),postright-1);
return root;
}
}
力扣 105. 从前序与中序遍历序列构造二叉树
原题链接
与106. 从中序与后序遍历序列构造二叉树是一样的思路,只不过每次从前序数组中取根节点作为中序数组的分割点。
class Solution {
//构造中序数组对应的哈希表,方便找到前序数组根节点值在中序数组中的索引
Map<Integer,Integer>inorderMap=new HashMap<>();
public TreeNode buildTree(int[] preorder, int[] inorder) {
for(int i=0;i<inorder.length;i++)inorderMap.put(inorder[i],i);
return buildTree1(inorder,0,inorder.length,preorder,0,preorder.length);
}
public TreeNode buildTree1(int[] inorder,int inleft,int inright,int[] preorder,int preleft,int preright){
if(inright-inleft<1)return null;
if(inright-inleft==1)return new TreeNode(inorder[inleft]);
int rootValue=preorder[preleft];
TreeNode root=new TreeNode(rootValue);
int rootIndex=inorderMap.get(rootValue);
root.left=buildTree1(inorder,inleft,rootIndex,preorder,preleft+1,preleft+1+(rootIndex-inleft));
root.right=buildTree1(inorder,rootIndex+1,inright,preorder,preleft+1+(rootIndex-inleft),preright);
return root;
}
}
力扣 654. 最大二叉树
原题链接
思路:
1.遍历数组(left:right-1) 找到数组中最大值和对应的下标
2.根据最大值构造更新节点最大值
3.调用 traversal(nums, left, maxValueIndex); 创建根节点的左孩子。递归执行此操作,创建根节点的整个左子树。
4类似的,调用 traversal(nums, maxValueIndex + 1, right); 创建根节点的右子树。
class Solution {
public TreeNode constructMaximumBinaryTree(int[] nums) {
//题目提供的函数没有传入区间边界的参数,自己构造一个
return constructMaximumBinaryTree1(nums,0,nums.length);
}
public TreeNode constructMaximumBinaryTree1(int[] nums,int leftIndex,int rightIndex){
//若左右边界相等(左闭右开)或左边界大于右边界,说明分割出来的区间没有元素,返回空节点
if(rightIndex<=leftIndex)return null;
//不用单独判断差值等于1的情况,因为后面的处理可以包含;
//差值>=1,分割区间大于等于1个元素,需要找到最大值作为根节点
//初始化,记录当前数组区间的最大值及其下标
int maxIndex=leftIndex;
int maxValue=nums[maxIndex];
//区间第一个元素已经初始化为最大值,遍历区间找到真正最大值
for(int i=leftIndex+1;i<rightIndex;i++){
if(nums[i]>maxValue){
maxValue=nums[i];
maxIndex=i;
}
}
TreeNode root=new TreeNode(maxValue);
//左闭右开,以最大元素作为分割点
root.left=constructMaximumBinaryTree1(nums,leftIndex,maxIndex);
root.right=constructMaximumBinaryTree1(nums,maxIndex+1,rightIndex);
return root;
}
}
力扣 617. 合并二叉树
·递归的思路:
两个二叉树的对应节点可能存在以下三种情况,对于每种情况使用不同的合并方式。
如果两个二叉树的对应节点都为空,则合并后的二叉树的对应节点也为空;
如果两个二叉树的对应节点只有一个为空,则合并后的二叉树的对应节点为其中的非空节点;
如果两个二叉树的对应节点都不为空,则合并后的二叉树的对应节点的值为两个二叉树的对应节点的值之和,此时需要显性合并两个节点。
对一个节点进行合并之后,还要对该节点的左右子树分别进行合并。这是一个递归的过程。
//递归法
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
//root1为空只要看root2的结点就行了,若root2也是空那直接返回空
if(root1==null)return root2;
if(root2==null)return root1;
//复用root1结点,直接对其值进行修改
root1.val+=root2.val;
root1.left=mergeTrees(root1.left,root2.left);
root1.right=mergeTrees(root1.right,root2.right);
return root1;
}
}
//迭代法(队列,层序遍历)
class Solution {
public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
if(root1==null)return root2;
if(root2==null)return root1;
Queue<TreeNode> que=new LinkedList<>();
que.offer(root1);
que.offer(root2);
while(!que.isEmpty()){
//先进先出
TreeNode node1=que.poll();
TreeNode node2=que.poll();
//复用树1的结点(会修改树1的结点值)
node1.val+=node2.val;
//对当前遍历到的两个结点的左孩子进行判断
//若均不为空,需要等待下一次合并,先入队
if(node1.left!=null&&node2.left!=null){
que.offer(node1.left);
que.offer(node2.left);
}
//若node2左孩子不为空,说明node1左孩子空
//直接把node2左子树赋给node1左孩子
//后续无需再对该结点的孩子进行操作,因为该结点后续的链接关系已经确定
else if(node2.left!=null)
node1.left=node2.left;
//不用考虑node1左孩子不空,node2左孩子空的情况
//因为本来就是复用树1的结点,这种情况直接用原来的结点就行
//对当前遍历到的两个结点的右孩子进行判断,思路同上
if(node1.right!=null&&node2.right!=null){
que.offer(node1.right);
que.offer(node2.right);
}else if(node2.right!=null)
node1.right=node2.right;
}
return root1;
}
}