06、数据结构与算法---二叉树

递归的精髓其实在于关注好当前结点,尽可能少试图每次都将递归的过程在脑海里模拟一遍😮


一、树的理解

二叉树是最基本的树结构,先从此学起

这个结构像是生活中树的结构倒过来,根结点在上,叶子结点在最下


二、手动实现

这里实现了一些树性质上的操作,相对简单

java 复制代码
public class MyTree {

    //结点类
    static class TreeNode {

        public TreeNode left;
        public TreeNode right;
        public String val;

        public TreeNode() {
        }

        public TreeNode(String val) {
            this.val = val;
        }

    }

    //前序遍历
    private void preOrder(TreeNode root){
        if(root == null){
            return;
        }
        System.out.print(root.val+" ");
        preOrder(root.left);
        preOrder(root.right);
    }
    //中序遍历
    private void inOrder(TreeNode root){
        if(root == null){
            return;
        }
        inOrder(root.left);
        System.out.print(root.val+" ");
        inOrder(root.right);
    }
    //后序遍历
    private void postOrder(TreeNode root){
        if(root == null){
            return;
        }
        postOrder(root.left);
        postOrder(root.right);
        System.out.print(root.val+" ");
    }

    //计算总结点个数
    public int count = 0;
    //一、计数器写法
    public void size(TreeNode root) {
        if(root == null){
            return;
        }
        count++;
        size(root.left);
        size(root.right);
    }

    //二、节点数 = 左子树 + 右子树 + 根节点
    public int size2(TreeNode root) {
        if(root == null){
            return 0;
        }
        return size2(root.left) + size2(root.right) + 1;
    }

    //计算叶子结点个数
    public int count2;
    //一、计数器写法
    public void getLeafNodeCount(TreeNode root){
        if (root == null){
            return;
        }
        if(root.left == null && root.right == null){
            count2++;
        }
        getLeafNodeCount(root.left);
        getLeafNodeCount(root.right);
    }

    //二、叶子结点 = 左子树叶子 + 右子树叶子
    public int getLeafNodeCount2(TreeNode root){
        if (root == null){
            return 0;
        }
        if(root.left == null && root.right == null){
           return 1;
        }
        return getLeafNodeCount2(root.left) + getLeafNodeCount2(root.right);
    }

    //求某一层的结点个数
    //整棵树的第k层 = 左子树第k-1层 + 右子树第k-1层
    public int getLevelNodeCount(TreeNode root,int k){
        if(root == null){
            return 0;
        }
        if (k == 1){
            return 1;
        }
        return getLevelNodeCount(root.left,k-1) + getLevelNodeCount(root.right,k-1);
    }

    //二叉树的高度
    public int getMaxHeight(TreeNode root){
        if (root == null){
            return 0;
        }
        int leftHeight = getMaxHeight(root.left);
        int rightHeight = getMaxHeight(root.right);

        return Math.max(leftHeight,rightHeight)+1;
    }


    //寻找结点是否存在
    public TreeNode find(TreeNode root,String val){
        if (root == null){
            return null;
        }
        if(root.val.equals(val)){
            return root;
        }
        TreeNode treeNode = find(root.left,val);
        if(treeNode != null){
            return treeNode;
        }

        return find(root.right,val);
    }

}

测试类:

java 复制代码
  public static void main(String[] args) {

        TreeNode A = new TreeNode("A");
        TreeNode B = new TreeNode("B");
        TreeNode C = new TreeNode("C");
        TreeNode D = new TreeNode("D");
        TreeNode E = new TreeNode("E");
        TreeNode F = new TreeNode("F");
        TreeNode G = new TreeNode("G");
        TreeNode H = new TreeNode("H");

        //进行连接
        A.left = B;
        A.right = C;

        B.left = D;
        B.right = E;

        C.left = F;
        C.right = G;

        E.right = H;

        // 此时的结构为:
        //         A
        //       /   \
        //      B     C
        //     / \   / \
        //    D   E F   G
        //         \
        //          H

    }

🍉这里OJ题的前序遍历有返回值,可以对比一下上面void的写法:

接收返回值的前序遍历


三、树的练习题

题目由易到难,层层递进,狠狠递归🤨

1、相同的树

判断两个树是否相同,从判断结点上可以分为三种情况(即注释)

使用前序遍历,从根开始判断,再到左子树,最后到右子树

java 复制代码
  //相同的树
    public boolean isSameTree(TreeNode p, TreeNode q) {

        //一个为空另一个不为空
        if(p == null && q != null || p != null && q == null){
            return false;
        }
        //两个都是空
        if (p == null && q == null){
            return true;
        }
        //两个都不为空,可以想想为什么使用!=
        if(p.val != q.val){
            return false;
        }

        //两棵树左子树右子树都相同
        return isSameTree(p.left,q.left)&&isSameTree(p.right,q.right);
    }

2、另一棵树的子树

利用第一题的判断整棵树的方法,以一棵树作为根,让比较树与其'根'、左子树、右子树分别比较,这三者只要一个相同即可

java 复制代码
  //另一棵树的子树
    public boolean isSubtree(TreeNode root, TreeNode subRoot) {
        if(root == null){
            return false;
        }
        if(isSameTree(root,subRoot)){
            return true;
        }

        return isSubtree(root.left,subRoot) || isSubtree(root.right,subRoot);
    }

3、反转二叉树

该题有两种写法:

一、使用前序遍历的思路,由根开始交换,然后左子树右子树

二、后序遍历,左右子树交换完成直接把原来根结点的左右子树交换即可

java 复制代码
 //反转二叉树1
    //前序遍历写法
    public TreeNode flipTree(TreeNode root) {
        if(root == null){
            return null;
        }

        //1、访问当前节点:交换地址
        TreeNode tmp;
        tmp = root.left;
        root.left = root.right;
        root.right = tmp;

        //2、反转左子树
        flipTree(root.left);

        //3、反转右子树
        flipTree(root.right);
        return root;
    }

java 复制代码
//反转二叉树2
    //后序遍历写法
    public TreeNode flipTree2(TreeNode root) {
        if(root == null){
            return null;
        }

        //左子树遍历
        TreeNode left = flipTree2(root.left);
        //右子树遍历
        TreeNode right = flipTree2(root.right);

        //交换操作
        root.left = right;
        root.right = left;

        return root;
    }

4、平衡二叉树

时间复杂度为O(n^2)的写法:

java 复制代码
    //平衡二叉树
    public boolean isBalanced(TreeNode root) {
        if(root == null){
            return true;
        }
        int x1 =getMaxHeight2(root.left);
        int x2 =getMaxHeight2(root.right);
        
        return Math.abs(x1-x2)<=1 &&isBalanced(root.left)&&isBalanced(root.right);
        
    }
     //二叉树的高度
    public int getMaxHeight2(TreeNode root){
        if (root == null){
            return 0;
        }
        int leftHeight = getMaxHeight2(root.left);
        int rightHeight = getMaxHeight2(root.right);

        return Math.max(leftHeight,rightHeight)+1;
    }

嵌套调用递归方法,反复执行了一部分逻辑,时间复杂度较高


时间复杂度为O(n)的写法:

java 复制代码
  //平衡二叉树
    //此时时间复杂度是n的平方
    public boolean isBalanced(TreeNode root) {
        if(root == null){
            return true;
        }
//        int x1 = getMaxHeight2(root.left);
//        int x2 = getMaxHeight2(root.right);
//
//        if(x1 == -1 || x2 == -1 || Math.abs(x1-x2)>1){
//            return false;
//        }else {
//            return true;
//        }
        return getMaxHeight2(root) > 0;
    }
    //二叉树的高度
    public int getMaxHeight2(TreeNode root){
        if (root == null){
            return 0;
        }

        //最下方算式返回到上一级如果是负数就返回-1
        int leftHeight = getMaxHeight2(root.left);
        if(leftHeight<0){
            return -1;
        }
        int rightHeight = getMaxHeight2(root.right);
        if(rightHeight<0){
            return -1;
        }

        if(Math.abs(leftHeight-rightHeight)>1){
            return -1;
        }else {
            return Math.max(leftHeight,rightHeight)+1;
        }

    }

这里注释掉了自己比较冗杂的写法,因为事实上整体的递归逻辑都在getMaxHeight2这个方法里面了🤣


5、判断对称二叉树

判断是否对称本质就是, '左的左'与'右的右' 以及 '左的右'与'右的左' 比较

java 复制代码
    //对称二叉树
    public boolean checkSymmetricTree(TreeNode root) {
        if (root == null){
            return true;
        }
       return checkSymmetricTree2(root.left,root.right);
    }

    public boolean checkSymmetricTree2(TreeNode leftRoot,TreeNode rightRoot) {

        if(leftRoot == null && rightRoot != null || leftRoot != null && rightRoot == null){
            return false;
        }
        if (leftRoot == null && rightRoot == null){
            return true;
        }
        if(leftRoot.val != rightRoot.val){
            return false;
        }

        return checkSymmetricTree2(leftRoot.left,rightRoot.right) && checkSymmetricTree2(leftRoot.right,rightRoot.left);
    }

递归的精髓就在于关注好当前结点,尽量少试图每次都将递归的过程在脑海里模拟一遍


6、层序遍历

没有返回值的层序遍历:

java 复制代码
  //层序遍历
    public void levelOrder(TreeNode root) {

        if(root == null){
            return;
        }
        Queue<TreeNode> q = new LinkedList<>();
        q.offer(root);

        while (!q.isEmpty()){

           //记录当前结点
           TreeNode cur = q.poll();
           System.out.println(cur.val);

            if(cur.left != null){
                q.offer(cur.left);
            }

            if (cur.right != null){
                q.offer(cur.right);
            }

        }

    }

有返回值的层序遍历:

java 复制代码
//层序遍历
    public List<List<Integer>> levelOrder(TreeNode root) {

        List<List<Integer>> vessel = new LinkedList<>();

        if(root == null){
            return vessel;
        }
        Queue<TreeNode> q = new LinkedList<>();
        q.offer(root);

        while (!q.isEmpty()) {

            List<Integer> l = new LinkedList<>();
            //队列里的元素个数
            int size = q.size();

            while (size != 0) {
                //记录当前结点
                TreeNode cur = q.poll();
                l.add(cur.val);

                if (cur.left != null) {
                    q.offer(cur.left);
                }
                if (cur.right != null) {
                    q.offer(cur.right);
                }
                size--;
            }

            vessel.add(l);
        }

        return vessel;

    }

🍉将元素不断放入队列中,同一层的元素出队放入到当前层链表,处理完当前层数的所有元素后将这一层的链表放到大链表中,一直到队列为空


7、判断完全二叉树

层序遍历的一个小变种

🍉核心在于当cur变成null的时候,检查队列里是否有非空元素

java 复制代码
  //判断完全二叉树
    public boolean isCompleteTree(TreeNode root) {
        if(root == null){
            return true;
        }
        Queue<TreeNode> q = new LinkedList<>();
        q.offer(root);

        while (!q.isEmpty()){
            TreeNode cur = q.poll();
            //此时其它所有子树的左右节点都已经入队过了(包括null)
            if(cur == null){
                break;
            }
            q.offer(cur.left);
            q.offer(cur.right);
        }

        //检查队列中是否存在非空元素,存在则不是完全二叉树
       while (!q.isEmpty()){
          TreeNode top = q.peek();
           if(top != null){
               return false;
           }
           q.poll();
       }

        return true;
    }

8、二叉树的最近公共祖先

🍉递归的写法在于搞清楚p与q位置的三种情况

java 复制代码
//二叉树最近的公共祖先
    //递归写法:
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {

        if(root == null){
            return null;
        }
        if(root == p || root == q){
            return root;
        }

        TreeNode left = lowestCommonAncestor(root.left,p,q);
        TreeNode right = lowestCommonAncestor(root.right,p,q);

        //p与q分别在根节点两侧
        if(left != null && right != null){
            return root;
        }else if(left != null){    //p与q都在左侧
            return left;
        }else if(right != null){   //p与q都在右侧
            return right;
        }else {
            return null;
        }

    }

🍉迭代的写法有点类似之前的链表相交,关键在于分别找到p与q各自的路径,将路径存放到栈里面,先让两个栈长度相等,判断一起弹出元素,若相等则为最近公共祖先

java 复制代码
  //迭代写法
    public TreeNode lowestCommonAncestor2(TreeNode root, TreeNode p, TreeNode q){

        Stack<TreeNode> s1 = new Stack<>();
        Stack<TreeNode> s2 = new Stack<>();

        //要从根结点开始处理
        getPath(root,p,s1);
        getPath(root,q,s2);

        //弹出多余部分,size相同的时候找到pop出的相同结点
        int size1 = s1.size();
        int size2 = s2.size();
        int size = s1.size()-s2.size();

        if(size < 0){
            while (size2 != size1){
                s2.pop();
                size2--;
            }
        }else {
            while (size1 != size2){
                s1.pop();
                size1--;
            }
        }

        while (size1 != 0 ){
            TreeNode m = s1.pop();
            TreeNode n = s2.pop();
            if(m == n){
                return m;
            }
        }

        return null;
    }
    
    //核心方法
    //拿到路径的每一个元素
    public boolean getPath(TreeNode root,TreeNode node,Stack<TreeNode> s){
        if(root == null){
            return false;
        }
        //将根节点放入栈
        s.push(root);
        if(root == node){
            return true;
        }

        boolean flag1 = getPath(root.left,node,s);
        if (flag1 == true){
            return true;
        }

        boolean flag2 = getPath(root.right,node,s);
        if (flag2 == true){
            return true;
        }

        //弹出非路径结点
        //即当前结点以及左右子树都没有目标结点
        s.pop();
        return false;
    }

本章完

相关推荐
TE-茶叶蛋1 小时前
Maven install 的原理
java·maven
likerhood1 小时前
设计模式:原型模式(Prototype Pattern)java版本
java·设计模式·原型模式
wuxuanok1 小时前
Maven 编译报错:java.lang.NoSuchFieldError: JCImport 问题总结
java·开发语言·maven
薛定谔的猫19821 小时前
gradio学习代码部分
java·前端·javascript
Devin~Y1 小时前
大厂Java面试实战:Spring Boot + Redis + Kafka + Kubernetes + RAG 的三轮追问(附答案解析)
java·spring boot·redis·spring cloud·kafka·kubernetes·resilience4j
酉鬼女又兒1 小时前
Leetcode 26.删除有序数组中的重复项 双指针巧解有序数组去重:从快慢指针到原地修改算法的精髓
java·数据结构·算法·leetcode·职场和发展·蓝桥杯·排序算法
承渊政道1 小时前
【动态规划算法】(斐波那契数列模型详解)
数据结构·c++·学习·算法·leetcode·macos·动态规划
ch.ju1 小时前
Java程序设计(第3版)第二章——循环结构4
java
笨笨饿2 小时前
# 67_MCU的几大分区
数据结构·单片机·嵌入式硬件·算法·机器人·线性回归·个人开发