代码随想录学习笔记——二叉树(下)

文章目录

前言

参考代码随想录/LeetCode作的学习笔记。
特别赞同卡子哥的一句话,初学者刚开始学习算法的时候,看到简单题目没有思路很正常,千万别怀疑自己智商,学习过程都是这样的,大家智商都差不多。慢慢来,一切都会好起来~

二叉树

题目链接 106. 从中序与后序遍历序列构造二叉树 难度级别 Mid

递归思路: 做这种题目要搞清楚以根结点为基准对左右子树需要实现哪些操作。首先后序遍历的最后一个结点是根结点,它先将中序序列划分为左右两个部分,再将后序序列划分为左右两个部分,然后再根据左右部分递归即可,结束条件分为两个,没有元素,也就是为null的情况;只有一个元素,这个很好理解,比如刚开始就一个根结点。

在方法traveldfs中定义inLeft 和 inRight这些int型变量是为了下一次方便再原始数组上遍历左子序列和右子序列。
关键词: 原数组left和right,构造二叉树

class Solution {
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        return traveldfs(inorder, 0,inorder.length, postorder, 0,postorder.length);
    }
    public TreeNode traveldfs(int[] inorder,int inLeft,int inRight,int[] postorder,int postLeft,int postRight){
        
        //没有元素了
        if(inRight - inLeft < 1) return null;

        //只有一个元素了
        if(inRight - inLeft == 1) return new TreeNode(inorder[inLeft]);

        //找到分隔结点
        int rootVal = postorder[postRight-1];
        TreeNode root = new TreeNode(rootVal);
        int rootIndex = 0;
        //分割中序
        for(int i = inLeft;i<inRight;i++){
            if(inorder[i] == rootVal){
                rootIndex = i;
                break;
            } 
        }

        root.left = traveldfs(inorder,inLeft,rootIndex,postorder,
                postLeft,postLeft + (rootIndex - inLeft));
        root.right = traveldfs(inorder,rootIndex+1,inRight,
                postorder,postLeft+(rootIndex-inLeft),postRight-1);

        return root;
    }
}

题目链接 654. 最大二叉树 难度级别 Mid

给定一个不含重复元素的整数数组 nums 。一个以此数组直接递归构建的 最大二叉树 定义如下:

二叉树的根是数组 nums 中的最大元素。
左子树是通过数组中 最大值左边部分 递归构造出的最大二叉树。
右子树是通过数组中 最大值右边部分 递归构造出的最大二叉树。
返回由给定数组 nums 构建的 最大二叉树 。

递归思路: 这个题目和题目链接 106. 从中序与后序遍历序列构造二叉树类似,都是需要构造树,只不过构造规则不同。首先找到最大值和其索引位置,然后构造根结点,再基于左右子序列像构造根结点一样递归构造左右子树。结束条件分两种,一种是结点为空,另一种是仅剩下一个节点。对于循环不变量,数组的区间控制为左闭右开。

关键词: 构造二叉树,循环不变量,原数组start和end

class Solution {
    public TreeNode constructMaximumBinaryTree(int[] nums) {
        return travelDfs(nums,0,nums.length);
    }
    public TreeNode travelDfs(int[] nums,int start,int end){
        if(end - start < 1) return null;

        if(end - start == 1) return new TreeNode(nums[start]);

        int maxIndex = 0,maxVal = 0;
        for(int i = start;i<end;i++){
            if(nums[i] > maxVal){
                maxVal= nums[i];
                maxIndex = i;
            }
        }
        TreeNode root = new TreeNode(nums[maxIndex]);
        root.left = travelDfs(nums,start,maxIndex);
        root.right = travelDfs(nums,maxIndex+1,end);
     
        return root;
    }
}

原题链接 617. 合并二叉树 难度级别 Easy

在这里插入图片描述
递归思路: 确定四种终止条件,然后依次递归遍历左右子树。

关键词: 两颗树

class Solution {
    public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
        TreeNode root = new TreeNode(0);
        if(root1 == null && root2 == null) return null;
        if(root1 != null && root2 == null) return root1;
        if(root1 == null && root2 != null) return root2;
        else root.val = root1.val + root2.val;

        root.left = mergeTrees(root1.left,root2.left);
        root.right = mergeTrees(root1.right,root2.right);

        return root;
    }
}

原题链接 700. 二叉搜索树中的搜索 难度级别 Easy

给定二叉搜索树(BST)的根节点和一个值。 你需要在BST中找到节点值等于给定值的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 NULL。

递归思路: 递归终止条件为 根结点为空或刚好满足为val值,注意遍历子树符合条件立即返回。

关键词: 符合条件立即返回

class Solution {
    public TreeNode searchBST(TreeNode root, int val) {
        if(root == null||root.val == val) return root;//空树或者当前根结点就满足
        //是否直接返回,看是否需要处理返回
        //因为这里是在查找,找到就需要立即返回
        if(root.val > val) return searchBST(root.left,val);
        if((root.val < val)) return searchBST(root.right,val);
        return null;
    }
}

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

class Solution {
    // 迭代,利用二叉搜索树特点,优化,可以不需要栈
    public TreeNode searchBST(TreeNode root, int val) {
        while (root != null)
            if (val < root.val) root = root.left;
            else if (val > root.val) root = root.right;
            else return root;
        return root;
    }

原题链接 98. 验证二叉搜索树 难度级别 Mid

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树

递归思路: 采用中序遍历迭代更新当前的结点值,如果出现不是单调递增的情况立即返回false;如果讨个巧,设置最小值为maxVal,如下这种情况无法通过。

关键词: 合并左右子树遍历结果返回,中序遍历,pre指针
在这里插入图片描述
注意点: 不能单纯的比较左节点小于中间节点,右节点大于中间节点,而是左子树都小于中间节点,右子树都大于中间节点。

class Solution {
    private int maxVal = -(1 << 31);
    public boolean isValidBST(TreeNode root) {
        if(root == null) return true;
        boolean left = isValidBST(root.left);

        //中序遍历,验证遍历的数是否是从小到大!太妙了吧!
        if(maxVal < root.val) maxVal = root.val;
        else return false;

        System.out.println(maxVal);

        boolean right = isValidBST(root.right);
        return left && right;
    }
}

有一种更巧妙的方法,设置一个前驱指针pre。

class Solution {
    TreeNode pre = null;
    public boolean isValidBST(TreeNode root) {
        if(root == null) return true;
        boolean left = isValidBST(root.left);
        
        if(pre!=null && pre.val >= root.val) return false;
        pre = root;//记录前一个节点,然后用它与当前节点的值比较,从而验证遍历的数是否为增序。

        boolean right = isValidBST(root.right);
        return left && right;
    }
}

原题链接 530. 二叉搜索树的最小绝对差 难度级别 Easy

给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值 。

递归思路: 这个题目和原题链接 98. 验证二叉搜索树有些相似,不同的是是否返回问题。验证二叉搜索树,一旦不满足条件就要返回。而本题,要遍历完所有的节点,在这个过程中将结果存储在一个public类型的整型变量result中。同样的采用中序遍历。注意这里的条件,当pre不为空指针,且满足result结果才替换result。

关键词: 中序遍历,pre指针,遍历完返回,全局变量result

class Solution {
    public TreeNode pre = null;
    public int result = 100000;
    public int getMinimumDifference(TreeNode root) {
        if(root == null) return 0;
        travelDfs(root);
        return result;
    }

    public void travelDfs(TreeNode root){

        //中序遍历
        if(root == null) return;
        travelDfs(root.left);

       
        if(pre!=null && ((root.val - pre.val) < result)) result = root.val - pre.val;
        pre = root;
        
        travelDfs(root.right);
    }
}

原题链接 501. 二叉搜索树中的众数 难度级别 Easy

有重复节点值,出现次数最多的,可以有多个这样的节点。

递归思路: 基于二叉搜索数的特性,采用中序遍历,像数组那样访问每个节点和它的值。然后通过一个整型的count记录出现次数最多的节点,并更新列表。

注1,记录count: 本来想着是使用两个数据结构,一个记录节点,一个记录count。但是这是一颗二叉排序树,根本不需要这么做,因为count记录众数,而众数是前后节点值相同统计得来的。这么想,就能明白为什么不是每个节点都记录它出现的次数,仅仅定义了一个count。代码有点长,看注释,每一块内容就清晰明了了。
注2,更新列表: 比如刚开始有3个2,3个3,会记录[2,3],后来出现了5个5,那么就会更新整个列表

关键词: 中序遍历,pre指针,全局变量

class Solution {
    ArrayList<Integer> resList;
    int maxCount;
    int count;
    TreeNode pre;

    public int[] findMode(TreeNode root) {
        resList = new ArrayList<>();
        maxCount = 0;
        count = 0;
        pre = null;
        if(root == null) return null;
        travelDfs(root);//递归遍历
        //将结果替换到数组中
        int[] res = new int[resList.size()];
        for(int i = 0;i<resList.size();i++){
            res[i] = resList.get(i);
        }
        return res;
    }
    public void travelDfs(TreeNode root){
        
        if(root == null) return;

        travelDfs(root.left);
		//记录count
        int rootValue = root.val;
        if(pre == null || pre.val!=rootValue) count = 1;
        else count++;
        
        //更新结果以及maxCount
        if(count > maxCount){
            resList.clear();
            //把结果列表更新,仅当出现大于maxCount的时候才更新,这样不会影响第二个众数的添加
            //比如刚开始有3个2,3个3,会记录[2,3],后来出现了5个5,那么就会更新整个列表
            resList.add(rootValue);
            maxCount = count;
        }else if(count == maxCount){
            resList.add(rootValue);
        }

        pre = root;//记录上一个节点

        travelDfs(root.right);
    }
}

原题链接 236. 二叉树的最近公共祖先 难度级别 Mid

最近公共祖先的定义为:对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)

递归思路: 找祖先需要从下往上找根节点,所以选择后序遍历;如果找到了p或者q或者空节点,就返回当前节点;最后再将结果回溯回去,分为四种情况:(1)左右均不空,则返回root,(2)左右均空返回null,(3)左空右不空返回左,(4)右空左不空返回右

关键词: 公共祖先,后序遍历

 //迭代法不适合模拟回溯的过程故采用递归解法
class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        //后序遍历,找到一个节点满足它的左右子树分别出现两个节点p和q,就是最近公共节点了
        
        //如果找到了p或者q或者空节点,就返回当前节点
        if(root == null || root == p || root == q) return root;
        TreeNode left = lowestCommonAncestor(root.left,p,q);
        TreeNode right = lowestCommonAncestor(root.right,p,q);

        if(left!=null && right!=null) return root;
        if(left == null && right!=null) return right;
        else if(left!=null && right==null) return left;
        else return null;//left && right == null
    }
}

原题链接 235. 二叉搜索树的最近公共祖先 难度级别 easy

递归思路: 二叉搜索树的最近公共祖先就是那个值介于p和q之间的节点。二叉搜索树是有序的,所以我们需要判别p和q节点的值在哪边,然后确定遍历的子树。

关键词: 二叉搜索树,公共祖先

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root == null || root == p || root == q) return root; //root为空或找到p,q返回
        if(root.val >p.val && root.val > q.val) return lowestCommonAncestor(root.left,p,q);
        else if(root.val <p.val && root.val < q.val) return lowestCommonAncestor(root.right,p,q);
        else return root;//满足条件,返回当前结点
    }
}

迭代思路: 根据二叉搜索树的概念,判断遍历子树,满足条件则返回。

class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        while(root!=null) {
            if (root.val > p.val && root.val > q.val) {
                root = root.left;
            } else if (root.val < p.val && root.val < q.val) {
                root = root.right;
            } else {
                return root;
            }
        }
        return null;
    }
}

原题链接 701.二叉搜索树中的插入操作 难度级别 Mid

迭代思路: 简单来想,我们不考虑官方介绍给的另一种情况。思路就很简单了,根据二叉搜索树的性质迭代找到这个值的位置,然后插入即可。

关键词: 二叉树增删节点
在这里插入图片描述
注意点: 要定义一个节点来完成后序节点的遍历和插入,如果仅仅用root来遍历,那么root遍历到插入节点的父节点位置时,输出就不是整棵树了。

class Solution {
    public TreeNode insertIntoBST(TreeNode root, int val) {
        TreeNode newNode = new TreeNode(val);
        if(root == null) return newNode;
        TreeNode cur = root;
        while(cur!=null){
            if(cur.val > val){
                if(cur.left!=null) cur = cur.left;
                else{
                    cur.left = newNode;
                    break;
                }
            }
            else{
                if(cur.right!=null) cur = cur.right;
                else{
                    cur.right = newNode;
                    break;
                }
            }
        }
        return root;
    }
}

原题链接 450. 删除二叉搜索树中的节点 难度级别 Mid

在这里插入图片描述
递归思路: 删除分四种情况:(1)左子树空,右子树也空;(2)左子树空,右子树不空;(3)左子树不空,右子树空;(4)左右子树均不空,根据上图的例子,这种情况需要把当前待删除节点3的左子树2移动到右子树4的最左边。

关键词: 二叉树增删节点

class Solution {
    public TreeNode deleteNode(TreeNode root, int key) {

        //先序遍历,先处理根结点
        if(root == null) return root;
        if(root.val == key){//找到待删除节点
            if(root.left == null && root.right==null){//叶子节点直接删除
                return null;
            }
            else if(root.right == null){//右子树为空,保留左子树,删除根
                root = root.left;
                return root;
            }
            else if(root.left == null){//左子树为空,保留左子树,删除根
                root = root.right;
                return root;
            }
            else{//左右子树均不空
                TreeNode cur =root.right;
                while(cur.left!=null) cur = cur.left;//左右子树均不空,找到右子树的最左面的节点
                cur.left = root.left;//把待删节点的左子树迁过来,直观上,就是将示例中的2迁到4的左边
                root = root.right;//root直接替换成右孩子从而实现删除当前root的效果
                return root;
            }
        }
        if(root.val > key) root.left = deleteNode(root.left,key);
        if(root.val < key) root.right = deleteNode(root.right,key);
        return root;
    }
}

原题链接 669. 修剪二叉搜索树 难度级别 Mid

给你二叉搜索树的根节点 root ,同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在[low, high]中。修剪树不应该改变保留在树中的元素的相对结构(即,如果没有被移除,原有的父代子代关系都应当保留)。 可以证明,存在唯一的答案。

所以结果应当返回修剪好的二叉搜索树的新的根节点。注意,根节点可能会根据给定的边界发生改变。

递归思路: 这个题的思路,卡子哥YYDS!首先比较好想到的是根据两个边界值来划分查找区域,根结点的右孩子和左孩子作相应的修剪处理;然后第二个比较难想,用根结点的左右结点去接上左孩子和右孩子分别递归的结果;注释也做了一些解释。

关键词: 递归函数作为返回值

class Solution {
    public TreeNode trimBST(TreeNode root, int low, int high) {
        //直观上,将不符合的节点和节点的边都删去即可
        //是不是相当于删除多个不在树中的节点啊,我们已经学会了如何删除一个节点了,是不是可以遍历删除?
        //1、中序 + 遍历删除 显然这种解法非常暴力
          
        //来了!正确思路!卡子哥yyds!
        if(root == null) return null;
        //如果当前节点值小于low值,则遍历右子树,返回右子树符合条件的头节点
        if(root.val < low){
            TreeNode right = trimBST(root.right,low,high);
            return right;
        }
        //如果当前节点值大于high值,则遍历左子树,返回左子树符合条件的头节点
        if(root.val > high){
            TreeNode left = trimBST(root.left,low,high);
            return left;
        }

        root.left = trimBST(root.left,low,high);//用根结点的左节点去接上左孩子删除后的头节点
        root.right =trimBST(root.right,low,high);//用根结点的右节点去接上右孩子删除后的头节点
        return root;
    }
}

迭代思路: 首先将root移动到[L,R]范围内;然后分别处理左右子树;对于左孩子,当符合删除条件时,接上左孩子的右子树;对于右孩子,当符合删除条件时,接上右孩子的左子树。

画画或者心里默默跟着程序走走就明白了。比如如下这个二叉搜索树,[3,8],当前根7在范围内;开始剪枝左右子树(分别以3和8为头结点的树);遍历到3时,将3的左孩子替换为1的右孩子nulll;遍历到8时,将8的右孩子替换为9的左孩子。可以再举个复杂点的例子[4,8]走一走,7的左子树为3的右子树,blabla…
在这里插入图片描述

class Solution {
public TreeNode trimBST(TreeNode root, int L, int R) {
        if (root == null) return null;

        // 处理头结点,让root移动到[L, R] 范围内,注意是左闭右闭
        while (root != null && (root.val < L || root.val > R)) {
            if (root.val < L) root = root.right; // 小于L往右走
            else root = root.left; // 大于R往左走
        }
        TreeNode cur = root;
        // 此时root已经在[L, R] 范围内,处理左孩子元素小于L的情况
        while (cur != null) {
            while (cur.left!=null && cur.left.val < L) {//要删除掉不符合条件的cu.left
                cur.left = cur.left.right;
            }
            cur = cur.left;
        }
        cur = root;

        // 此时root已经在[L, R] 范围内,处理右孩子大于R的情况
        while (cur != null) {
            while (cur.right!=null && cur.right.val > R) {
                cur.right = cur.right.left;
            }
            cur = cur.right;
        }
        return root;
    }
};

原题链接 108. 将有序数组转换为二叉搜索树 难度级别 easy

递归思路: 基于二叉搜索树的特性,中间位置的节点即为根结点,然后通过递归函数的返回值来构造二叉树,每一次将中间的节点插入二叉树中,对于循环不变量,构造的数组区间为左闭右闭;

关键词: 构造二叉树,递归函数作为返回值增删节点,循环不变量

class Solution {
    public TreeNode sortedArrayToBST(int[] nums) {
        //根据有序数组,构造高度平衡二叉搜索树
        TreeNode root = traversal(nums,0,nums.length - 1);//左闭右闭
        return root;
        
    }
    public TreeNode traversal(int[] nums,int left,int right){
        if(left > right) return null;

        int mid = left + ((right - left) >> 1);
        TreeNode root = new TreeNode(nums[mid]);
        //通过递归函数的返回值来增删二叉树(每一次构造都是增加一个节点)
        root.left = traversal(nums,left,mid - 1);
        root.right = traversal(nums,mid + 1,right);
        return root;
    }
}

原题链接 538. 把二叉搜索树转换为累加树 难度级别 Mid

给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。
在这里插入图片描述

递归思路: 对于二叉搜索树,它是一个有序的数组,如果在这个数组上操作,比如[6,7,8] ->[21,15,8]。基于这个思想,二叉搜索树上的遍历顺序就明朗了,采用反的中序遍历。逻辑处理,使用一个pre指针,每次加上一个节点的值即可。

关键词: 中序遍历,pre指针

class Solution {
    TreeNode pre = null;
    public TreeNode convertBST(TreeNode root) {
        //思路1:先遍历一遍,将每个节点的值存储到一个数组中,然后依次替换节点值即可
        //思路2:采用递归一边遍历,一边替换
         //开始干!是否有返回值?遍历完再返回根结点root
        //方法 public TreeNode convertBST(TreeNode root)
        //终止条件 if(root == null) return root;
        //单次递归 convertBST(root) 逻辑 每次加上一个节点的值

        //反中序遍历 从右向左 累加值
        if(root == null) return root;
        if(root.right!=null) root.right = convertBST(root.right);

        if(pre!=null) root.val = root.val + pre.val;
        pre = root;

        if(root.left!= null) root.left = convertBST(root.left);

        return root;
    }
}

复盘

处理二叉树问题的一些技巧:

关于什么时候返回结果?
有的时候需要及时返回,就直接return;
有的时候需要遍历完整个树才知道结果,就将结果统计到全局变量中。
中途满足条件即返回的写法:
if (dfs(root.left)) return ;
if (dfs(root.right)) return ;
搜索整个树写法:
left = fun(root.left);
right = fun(root.right);
return left与right的逻辑处理;
使用递归还是迭代? 有些问题使用迭代更能理解遍历和解答的过程,有些问题递归思路更好。

JAVA 相关数据结构的使用:

ArrayList :
List<Integer> list = new ArrayList<>();添加元素 add()
LinkedList:
添加元素 add() ,删除最后一个元素removeLast();
集合 Map:
Map<Integer,Integer> map=new HashMap();
栈 Stack:
Stack<TreeNode> s = new Stack<>(); pushpop
队列 Queue:
Queue<TreeNode> que = new LinkedList<>();offerpoll;
双端队列 Deque:
Deque<TreeNode> deque = new LinkedList<>(); offerfirst/offerlast/pollfirst/polllast

其他:

整型最小值: int max=Integer.MIN_VALUE;

总的来说做二叉树的题,首先想递归能不能做,函数是什么?参数是什么?递归终止条件是什么?单次递归逻辑如何处理?如何获取返回值,是立即返回,还是逻辑运算返回?是定义局部变量还是全局变量?迭代怎么做,有没有什么树的特性?存储结构队列和栈的优势。
如有错误,欢迎指正。秉持简单易懂的原则,如有疑问,欢迎提出。

本周完结,撒花,贴两张高木美照~

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

疯狂java杰尼龟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值