LeetCode二叉树问题全解析(中)

目录

一、二叉树的修改与改造问题

1、翻转二叉树

2、从中序和后序序列构造二叉树

3、最大二叉树

 4、合并二叉树

 二、二叉搜索树问题

1、二叉搜索树中树的搜索

1)递归法:

2)迭代法:

 2、验证二叉搜索树

3、二叉搜索树的最小绝对差

 4、二叉搜索树中的众数

 5、把二叉搜索树转为累加树


一、二叉树的修改与改造问题

1、翻转二叉树

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

这道题目大家第一次做可能会觉得它很简单,左换右,一直递归不就行了

但如果沉下心来思考这道问题的本质 我相信大家一定有所收获!

之前我们介绍了各种遍历二叉树的方式,如果要翻转这颗二叉树 我们应该采取哪一种遍历方式呢?

其实这道题采用前,中,后都可以 只要在遍历时交换左右节点即可

但是中序的时候有些麻烦 因为 我们左子树的节点已经完成了翻转 而此时左右交换,中序遍历恰好需要到右子树遍历 这就导致原先的左子树被翻转了两次  而右子树被交换到了左子树 还没有完成翻转!

前序遍历代码:

 public TreeNode invertTree(TreeNode root) {
        if(root==null){//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){//root为Null的时候我们什么都不需要做
            return null;
        }
        
        invertTree(root.left);
        invertTree(root.right);
        
        TreeNode temp=root.left;//交换左右节点
        root.left=root.right;
        root.right=temp;
        return root;
    }

中序遍历实现交换的代码也很简单,不过是 左 中 右 的顺序改为 左 中 左  因为交换过后右子树被交换到了左子树  我们需要对左子树进行处理

public TreeNode invertTree(TreeNode root) {
        if(root==null){//root为Null的时候我们什么都不需要做
            return null;
        }

        invertTree(root.left);
        TreeNode temp=root.left;//交换左右节点
        root.left=root.right;
        root.right=temp;
        
        invertTree(root.left);
        return root;
        
    }

 层序遍历的代码也是一样的 只要在遍历到结点时 交换它的左右子树就可以了

注意:很多同学写熟了层序遍历之后 经常把从队列中弹出的元素赋值给root,这样做本身是没有任何问题的,但本题要求最后返回根节点 而经过层序遍历后 我们的root已经变为了队列中最后一个结点 因此我们一开始就要记录一个根节点 

 public TreeNode invertTree(TreeNode root) {
        if(root==null){//root为Null的时候我们什么都不需要做
            return null;
        }
        Deque<TreeNode> queue=new LinkedList<>();
        queue.offer(root);
        TreeNode res=root;
        while (!queue.isEmpty()){
            int size=queue.size();
            for (int i=0;i<size;i++){
                root=queue.poll();

                TreeNode temp=root.left;//交换左右节点
                root.left=root.right;
                root.right=temp;

                if (root.left!=null){
                    queue.offer(root.left);
                }
                if (root.right!=null){
                    queue.offer(root.right);
                }
            }
        }
        return res;

    }

 这道题当然还可以用非递归的写法来解,本质上其实还是前中后的迭代写法,对迭代遍历印象不深的同学建议看我的上一篇文章,这里就不再过多赘述。

2、从中序和后序序列构造二叉树

首先回忆一下如何根据两个顺序构造一个唯一的二叉树,相信理论知识大家应该都清楚,就是以 后序数组的最后一个元素为切割点,先切中序数组,根据中序数组,反过来在切后序数组。一层一层切下去,每次后序数组最后一个元素就是节点元素。

如果让我们肉眼看两个序列,画一颗二叉树的话,应该分分钟都可以画出来。

我们看一下这棵树的中序 和后序遍历的数组:

来看一下一共分几步:

  • 第一步:如果数组大小为零的话,说明是空节点了。

  • 第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。

  • 第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点

  • 第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)

  • 第五步:切割后序数组,切成后序左数组和后序右数组

  • 第六步:递归处理左区间和右区间

  • 题解来源:图解构造二叉树之中序+后序 - 从中序与后序遍历序列构造二叉树 - 力扣(LeetCode) (leetcode-cn.com)https://leetcode-cn.com/problems/construct-binary-tree-from-inorder-and-postorder-traversal/solution/tu-jie-gou-zao-er-cha-shu-wei-wan-dai-xu-by-user72/ 

    public TreeNode buildTree(int[] inorder, int[] postorder) {
            int is=0;
            int ie=inorder.length-1;
            int ps=0;
            int pe=postorder.length-1;
            TreeNode root=buildTree(inorder,postorder,is,ie,ps,pe);
            return root;
        }
        public TreeNode buildTree(int[] inorder,int [] postorder,int is,int ie,int ps,int pe){
            if (ie<is||pe<ps){//数组中没有元素,不再需要构造
                return null;
            }
            int rootVal=postorder[pe];//后序的最后一个元素
            int ri=0;//在中序中寻找后序最后一个元素的 下标
            for (int i=0;i<inorder.length;i++){  //遍历中序数组 寻找下标
                if (inorder[i]==rootVal){
                    ri=i;
                }
            }
            TreeNode root=new TreeNode(rootVal);//新建的根节点,我们在此需要把两个数组都划分为左右两块
            //左子树的中序下标  is为is ie为ri-1  左子树的总长度为 ri-is  左子树的后序下标为 ps是ps pe是 ps+左子树总长度减一 也就是
            // ps+ri-is-1  pe为
            //右子树的中序下标  is为ri+1 ie为 ie  右子树的后序下标为 ps为左子树的ps+1 也就是 ps+ri-is pe为pe-1
            root.left=buildTree(inorder,postorder,is,ri-1,ps,ps+ri-is-1);
            root.right=buildTree(inorder,postorder,ri+1,ie,ps+ri-is,pe-1);
            return root;//返回构造好的根节点
        }

    但是这个代码还存在一些问题,由于我们要拿后序数组中最后一个元素在中序中查找(每一个元素) 这样的效率是很低的 因此我们在一开始就把所有的元素和它在中序中的下标存在hash表里 要用的时候直接取!

  • HashMap<Integer,Integer> map=new HashMap<>();
        public TreeNode buildTree(int[] inorder, int[] postorder) {
            int is=0;
            int ie=inorder.length-1;
            int ps=0;
            int pe=postorder.length-1;
            for (int i=0;i<inorder.length;i++){
                map.put(inorder[i],i);
            }
            TreeNode root=buildTree(inorder,postorder,is,ie,ps,pe);
            return root;
        }
        public TreeNode buildTree(int[] inorder,int [] postorder,int is,int ie,int ps,int pe){
            if (ie<is||pe<ps){//数组中没有元素,不再需要构造
                return null;
            }
            int rootVal=postorder[pe];//后序的最后一个元素
            int ri=map.get(rootVal);//在中序中寻找后序最后一个元素的 下标
            
            TreeNode root=new TreeNode(rootVal);//新建的根节点,我们在此需要把两个数组都划分为左右两块
            //左子树的中序下标  is为is ie为ri-1  左子树的总长度为 ri-is  左子树的后序下标为 ps是ps pe是 ps+左子树总长度减一 也就是
            // ps+ri-is-1  pe为
            //右子树的中序下标  is为ri+1 ie为 ie  右子树的后序下标为 ps为左子树的ps+1 也就是 ps+ri-is pe为pe-1
            root.left=buildTree(inorder,postorder,is,ri-1,ps,ps+ri-is-1);
            root.right=buildTree(inorder,postorder,ri+1,ie,ps+ri-is,pe-1);
            return root;//返回构造好的根节点
        }

    这道题做完后,大家可以尝试从前序和中序序列构造二叉树 本质上和本题没有任何区别,无非是后序从后找根,前序从前找根

  •  HashMap<Integer,Integer> map=new HashMap<>();
        public TreeNode buildTree(int[] preorder, int[] inorder) {
            //通过前序中序其实和中序后序没有任何区别
            //中序后序是取后序的最后一个元素
            //前序中序自然是取前序的第一个元素
            for (int i=0;i<inorder.length;i++){//老样子,先把中序的值和下标存在hash表
                map.put(inorder[i],i);
            }
            int ps=0;
            int pe=preorder.length-1;
            int is=0;
            int ie=inorder.length-1;
            return buildTree(preorder,inorder,ps,pe,is,ie);
        }
        public TreeNode buildTree(int[] preorder,int[] inorder,int ps,int pe,int is,int ie){
            if (pe<ps||ie<is){
                return null;
            }
            int rootVal=preorder[ps];//前序的第一个元素
            int ri=map.get(rootVal);
            TreeNode root=new TreeNode(rootVal);
            //对root的左子树和右子树进行构造
            //左子树:前序:ps=ps+1 通过ri获得前序数组的长度 ri pe=ps+ri-ps-1  中序:is=is ie=ri-1
            //右子树:前序:ps=左子树的pe+1 ps=ps+ri  pe=pe 中序等于is=ri+1 ie=ie
    
    
              root.left=buildTree(preorder,inorder,ps+1,ps+ri-is,is,ri-1);
            root.right=buildTree(preorder,inorder,ps+ri-is+1,pe,ri+1,ie);
            return root;
        }

    3、最大二叉树

这道题一看问题的描述 ,就知道和上一道从前序 中序构造二叉树的问题本质没有任何区别,都是要分割数组,不断递归。

  • 树的递归很多时候都可以套路解决,就一个模版,递归套路三部曲:
  1. 找终止条件:当l>r时,说明数组中已经没元素了,自然当前返回的节点为null。
  2. 每一级递归返回的信息是什么:返回的应该是当前已经构造好了最大二叉树的root节点。
  3. 一次递归做了什么:找当前范围为[l,r]的数组中的最大值作为root节点,然后将数组划分成[l,bond-1]和[bond+1,r]两段,并分别构造成root的左右两棵子最大二叉树。
    public class Solution {
        public TreeNode constructMaximumBinaryTree(int[] nums) {
            return helper(nums,0,nums.length-1);
        }
        public TreeNode helper(int[] nums,int s,int e){
            if (e<s){
                return null;//当前数组没有元素,当然返回null
            }
            int max=Integer.MIN_VALUE;//定义一个最小的max,每次都在数组中寻找最大值作为根节点
            int maxIndex=s;//最大值下标
            for (int i=s;i<=e;i++){//在 s~e 左闭右闭 寻找最大值
                if (nums[i]>max){
                    maxIndex=i;
                    max=nums[i];
                }
            }
            TreeNode root=new TreeNode(max);//递归的构造左右子树
            root.left=helper(nums,s,maxIndex-1);
            root.right=helper(nums,maxIndex+1,e);
            return root;
    
    
        }
    }

     4、合并二叉树

 

这道题十分简单,我们只需要确保两颗二叉树的遍历方式是相同的,在遍历到一个结点时,新建一个结点替代这两个结点,然后分别遍历左右子树即可。

这里仅给出前序遍历的代码(中序,后序只是顺序上的不一致)

public class Solution {
    public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
        if (root1==null&&root2==null){//如果两颗树都是空 那么就返回空
            return null;
        }
        if (root1==null){//只有一棵树不为空时 返回不为空的那颗树
            return root2;
        }
        if (root2==null){
            return root1;
        }
        TreeNode node=new TreeNode(root1.val+root2.val);//新建结点 然后构建左右子树  本质上是前序遍历
        node.left=mergeTrees(root1.left,root2.left);
        node.right=mergeTrees(root1.right,root2.right);
        return node;
    }
}

 二、二叉搜索树问题

在我们遇到二叉搜索树问题时,我们必须要记住一点:二叉搜索树的中序遍历是有序的,这一点是我们求解二叉搜索树的关键!

二叉搜索树是一个有序树:

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;

  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;

  • 它的左、右子树也分别为二叉搜索树

这就决定了,二叉搜索树,递归遍历和迭代遍历和普通二叉树都不一样。

1、二叉搜索树中树的搜索

 

1)递归法:

  1. 确定递归函数的参数和返回值

递归函数的参数传入的就是根节点和要搜索的数值,返回的就是以这个搜索数值所在的节点。

代码如下:

TreeNode* searchBST(TreeNode* root, int val)
  1. 确定终止条件

如果root为空,或者找到这个数值了,就返回root节点。

if (root == NULL || root->val == val) return root;
  1. 确定单层递归的逻辑

看看二叉搜索树的单层递归逻辑有何不同。

因为二叉搜索树的节点是有序的,所以可以有方向的去搜索。

如果root->val > val,搜索左子树,如果root->val < val,就搜索右子树,最后如果都没有搜索到,就返回NULL。

代码如下:

if (root->val > val) return searchBST(root->left, val); // 注意这里加了return
if (root->val < val) return searchBST(root->right, val);
return NULL;

这里可能会疑惑,在递归遍历的时候,什么时候直接return 递归函数的返回值,什么时候不用加这个 return呢。

我们在之前讲了,如果要搜索一条边,递归函数就要加返回值,这里也是一样的道理。

因为搜索到目标节点了,就要立即return了,这样才是找到节点就返回(搜索某一条边),如果不加return,就是遍历整棵树了。

整体代码如下:

class Solution {
 public TreeNode searchBST(TreeNode root, int val) {
        if(root==null||root.val==val) return root;
        if (root.val<val) return searchBST(root.right,val);
        return searchBST(root.left,val);
    }
}

2)迭代法:

提到二叉树遍历的迭代法,可能立刻想起使用栈来模拟深度遍历,使用队列来模拟广度遍历。

对于二叉搜索树可就不一样了,因为二叉搜索树的特殊性,也就是节点的有序性,可以不使用辅助栈或者队列就可以写出迭代法。

对于一般二叉树,递归过程中还有回溯的过程,例如走一个左方向的分支走到头了,那么要调头,在走右分支。

对于二叉搜索树,不需要回溯的过程,因为节点的有序性就帮我们确定了搜索的方向。

例如要搜索元素为3的节点,我们不需要搜索其他节点,也不需要做回溯,查找的路径已经规划好了。

中间节点如果大于3就向左走,如果小于3就向右走,如图:

 

所以迭代法代码如下:


public class Solution {
    public TreeNode searchBST(TreeNode root, int val) {
        while (root!=null){
            if (val>root.val){
                root=root.right;
            }
            if (val<root.val){
                root=root.left;
            }
            return root;
        }
        return null;
    }
}

 2、验证二叉搜索树

 写到这道题的时候,很多同学直接想到 我只要遍历每个结点 保证左小于它 右大于它 不就好了

这样的思路看似完美 但其实有很大问题 因为二叉搜索树要求左子树所有结点都小于它 而右子树所有结点都大于它

我们可以对上述思路很轻易的举出反例:

6明显大于10,所以不能出现在右子树

因此我们想到利用二叉搜索树进行中序遍历 定义前一个pre 只要当前结点<=它我们就返回false,否则每个节点都遍历完成 确认无误后我们返回true

这道题其实有很多坑 很多同学写下如下的代码:

public class Solution {
    int pre=Integer.MIN_VALUE;
    public boolean isValidBST(TreeNode root) {
        if (root==null) return true;
        isValidBST(root.left);
        if (root.val<=pre) return false;
        pre=root.val;
        return isValidBST(root.right);
    }
}

 这代码其实有很多问题 他真正能检测出一棵树是不是二叉搜索树吗?并不能!为什么?

这的确是中序遍历 但是 ,假如左子树已经不是二叉搜索树了  而我们返回的却永远是右子树的检测!因此我们需要定义一个变量将左子树的检测保存起来!

public class Solution {
    int pre=Integer.MIN_VALUE;
    public boolean isValidBST(TreeNode root) {
        if (root==null) return true;
        boolean left=isValidBST(root.left);
        if (root.val<=pre) return false;
        pre=root.val;
        boolean right=isValidBST(root.right);
        return left&&right;
    }
}

但仍然存在问题,官方的测试用例中有第一个结点的值为integer的最小值的可能,那我们换成long的最小值不就好了,确实可以这么做,但假如我们第一个结点恰好是long的最小值呢?

所以这种方案缺乏严密性,我们应把pre定义为前驱结点 默认他是null 

TreeNode pre=null;
    public boolean isValidBST(TreeNode root) {
        if (root==null) return true;
        boolean left=isValidBST(root.left);
        if (pre==null||root.val>pre.val){
            pre=root;
        }else {
            return false;
        }
        boolean right=isValidBST(root.right);
        return left&&right;
    }

3、二叉搜索树的最小绝对差

这道题看起来十分复杂,不同结点之间的最小差值,那么我需要找每一种结点的匹配项 这个复杂度稳稳地O(N^2) 但其实我们发现本题是一颗二叉搜索树 它的中序遍历一定是升序地 而最小地差值一定出现在两个相邻地元素

public class Solution {
    public int min=Integer.MAX_VALUE;//定义最小差值
    public TreeNode pre=null;//定义前一个结点
    public int getMinimumDifference(TreeNode root) {
        if (root==null){
            return -1;//本质上我们什么都不需要做
        }
        getMinimumDifference(root.left);
        if (pre==null){//中序遍历地第一个结点
            pre=root;
        }else {
            min=Math.min(min,root.val-pre.val);
            pre=root;
        }
        getMinimumDifference(root.right);
        return min;
    }
}

 4、二叉搜索树中的众数

这道题看起来似乎要用到hashmap存储对应的值和出现的次数然后再逐一比较出现的次数大小 最后返回众数 但还是由于它是二叉搜索树,所以中序遍历是有序的 既然有序 那么相同的数 肯定是连在一起的 因此我们很好统计数量

 

public class Solution {
    List<Integer> answer=new ArrayList<>();//用于加入元素 最后返回数组
    int base,count,max;
    public int[] findMode(TreeNode root) {
        dfs(root);
        int[] res=new int[answer.size()];//把list中的元素加到数组中
        for (int i=0;i<answer.size();i++){
            res[i]=answer.get(i);
        }
        return res;
    }
    public void dfs(TreeNode root){//本质上是中序遍历
        if (root==null) return;
        dfs(root.left);
        upDate(root.val);
        dfs(root.right);
    }

    public void upDate(int val){
        if (val==base){//还是统计当前值的个数
            count++;
        }else {//新的值了
            count=1;
            base=val;
        }
        if (count==max){//相等的时候需要把元素加进去
            answer.add(base);
        }
        if (count>max){//大于需要清空 然后加进去
            max=count;//更新最大值
            answer.clear();
            answer.add(base);
        }
    }




}

 5、把二叉搜索树转为累加树

一看到累加树,相信很多小伙伴都会疑惑:如何累加?遇到一个节点,然后在遍历其他节点累加?怎么一想这么麻烦呢。

然后再发现这是一颗二叉搜索树,二叉搜索树啊,这是有序的啊。

那么有序的元素如果求累加呢?

其实这就是一棵树,大家可能看起来有点别扭,换一个角度来看,这就是一个有序数组[2, 5, 13],求从后到前的累加数组,也就是[20, 18, 13],是不是感觉这就简单了。

为什么变成数组就是感觉简单了呢?

因为数组大家都知道怎么遍历啊,从后向前,挨个累加就完事了,这换成了二叉搜索树,看起来就别扭了一些是不是。

那么知道如何遍历这个二叉树,也就迎刃而解了,从树中可以看出累加的顺序是右中左,所以我们需要反中序遍历这个二叉树,然后顺序累加就可以了。

本题依然需要一个pre指针记录当前遍历节点cur的前一个节点,这样才方便做累加。

  • 递归函数参数以及返回值

这里很明确了,不需要递归函数的返回值做什么操作了,要遍历整棵树。

同时需要定义一个全局变量pre,用来保存cur节点的前一个节点的数值,定义为int型就可以了。

代码如下:

int pre; // 记录前一个节点的数值
void traversal(TreeNode* cur)
  • 确定终止条件

遇空就终止。

if (cur == NULL) return;
  • 确定单层递归的逻辑

注意**要右中左来遍历二叉树**, 中节点的处理逻辑就是让cur的数值加上前一个节点的数值。

 

class Solution {
   TreeNode pre=null;
    public TreeNode convertBST(TreeNode root) {
        if (root==null){
            return null;
        }
        convertBST(root.right);
        if (pre==null){
            //root的val不变
        }else {
            root.val+=pre.val;
        }
        pre=root;
        convertBST(root.left);
        return root;
    }
}

评论 43
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值