【Java 数据结构】二叉树的经典面试题 (图解)

点进来你就是我的人了
博主主页:戳一戳,欢迎大佬指点!

人生格言:当你的才华撑不起你的野心的时候,你就应该静下心来学习!

欢迎志同道合的朋友一起加油喔
目标梦想:进大厂,立志成为一个牛掰的Java程序猿,虽然现在还是一个嘿嘿
谢谢你这么帅气美丽还给我点赞!比个心


目录

1. 相同的树

2. 另一颗树的子树

3.翻转二叉树

4. 平衡二叉树

5.对称二叉树

6.层序遍历

7. 二叉树的构建及遍历

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

 (1)方法一的代码

    (2) 方法二的代码

9. 从前序遍历与中序遍历序列构造二叉树

10.  从中序遍历与后序遍历序列构造二叉树

11. 根据二叉树创建字符串



1. 相同的树

●题目展示【Java 数据结构】二叉树的经典面试题 (图解)_第1张图片

●写题链接:  100. 相同的树 - 力扣(LeetCode)

●解题思路:两棵树同时递归, 每遍历到一个结点就判断
   1️⃣当 两个结点p, q, 一个为空一个不为空时, 返回 false
   2️⃣当 两个结点p, q, 的val值不同时, 返回 false
   3️⃣当 两个结点p, q, 同时遍历到空结点时, 说明 p, q 的所有祖先点相同, 返回 true

⚠️⚠️⚠️注意:        
不能 p, q, 同时不为空且值相同时就返回 true, 因为有可能这个结点暂时相同, 但子树的结点还不能相同, 一定是当 p, q, 同时遍历到空结点时, 才说明这一路的结点没有返回 false, 那么此时 p, q 的所有祖先点相同, 才能返回true

        ●图解:【Java 数据结构】二叉树的经典面试题 (图解)_第2张图片

 ●实现代码:

时间复杂度:O(min⁡(m,n)),其中 m 和 n 分别是两个二叉树的节点数。对两个二叉树同时进行深度优先搜索,只有当两个二叉树中的对应节点都不为空时才会访问到该节点,因此被访问到的节点数不会超过较小的二叉树的节点数。

空间复杂度:O(min⁡(m,n)),其中 m 和 n 分别是两个二叉树的节点数。空间复杂度取决于递归调用的层数,递归调用的层数不会超过较小的二叉树的最大高度,最坏情况下,二叉树的高度等于节点数。

    public boolean isSameTree(TreeNode p, TreeNode q) {
    	// p, q, 有其中一个不为空
        if ( (p == null && q != null ) || (p != null && q == null) ) {
            return false;
        }
        // val不相同的情况
        if (p.val != q.val) {
            return false;
        }
        // p, q, 都为空的情况
        if (p == null && q == null) {
            return true;
        }
        // 左右子树都要判断
        return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
    }

        ●递归过程演示:        【Java 数据结构】二叉树的经典面试题 (图解)_第3张图片

2. 另一颗树的子树

●题目展示【Java 数据结构】二叉树的经典面试题 (图解)_第4张图片

●写题链接:  100. 相同的树 - 力扣(LeetCode)

●解题思路:

上一题就是 判断两棵二叉树是否相同, 判断一棵树是否是另一颗的子树, 只需要遍历以 root 为根的树的每一个结点, 和以 subRoot 为根的树, 只要是相同的树则说明 root 这棵树中有 subRoot 这棵树, 所以上一题的代码可以直接拿来用

题目描述中说 tree 也可以看做它自身的一棵子树 , 说明: 如果给定的 root 和 subRoot 本身就是相同的树, 也符合题目要求, 所以首先要对给定的 root 和 subRoot 判断是否为同一棵树
然后利用子问题思想: 让 root 的左右子树递归判断是否存在一个结点和 subRoot 是相同的树

⚠️⚠️⚠️注意:
虽然题目给定两个树的结点范围是[1,2000], 说明两棵树都不为空, 但是不能省略代码中对 root 和 sunRoot 的判空, 因为:
1️⃣在上一题分析过, 当 p, q, 同时遍历到空结点时, 说明 p, q 的所有祖先点相同, 返回 true, 所以 root 和subRoot 都需要遍历到空结点
2️⃣还有一种情况是 root 这棵树的所有结点都遍历完了也不满足和 subRoot 是同一棵树, 所以要返回 false

●图解:【Java 数据结构】二叉树的经典面试题 (图解)_第5张图片

 ●实现代码:时间复杂度O(s * t),左边这棵树的每一个结点都要与右边树的每一个结点比较

    public boolean isSubtree(TreeNode root, TreeNode subRoot) {
		// 防止空指针异常
        if(root == null || subRoot == null) {
            return false;
        }
        if(isSameTree(root,subRoot)) {
            return true;
        }
        // 遍历,左右子树有一边满足条件即可
        return isSubtree(root.left,subRoot) || isSubtree(root.right,subRoot);
    }
        public boolean isSameTree(TreeNode p, TreeNode q) {
    	// p, q, 有其中一个不为空
        if ( (p == null && q != null ) || (p != null && q == null) ) {
            return false;
        }
        // val不相同的情况
        if (p.val != q.val) {
            return false;
        }
        // p, q, 都为空的情况
        if (p == null && q == null) {
            return true;
        }
        // 左右子树都要判断
        return isSameTree(p.left, q.left) && isSameTree(p.right, q.right);
    }

3.翻转二叉树

●题目展示【Java 数据结构】二叉树的经典面试题 (图解)_第6张图片

●写题链接:力扣

●解题思路: 

交换一下左右节点,然后再递归的交换左节点,右节点 根据动画图我们可以总结出递归的两个条件如下:

终止条件:当前节点为 null 时返回
交换当前节点的左右节点,再递归的交换当前节点的左节点,递归的交换当前节点的右节点

●图解:   

【Java 数据结构】二叉树的经典面试题 (图解)_第7张图片

  ●实现代码:

时间复杂度:每个元素都必须访问一次,所以是 O(n)

空间复杂度:最坏的情况下,需要存放 O(h)个函数调用(h是树的高度),所以是 O(h)

class Solution {
	public TreeNode invertTree(TreeNode root) {
		//递归函数的终止条件,节点为空时返回
		if(root==null) {
			return null;
		}
		//下面三句是将当前节点的左右子树交换
		TreeNode tmp = root.right;
		root.right = root.left;
		root.left = tmp;
		//递归交换当前节点的 左子树
		invertTree(root.left);
		//递归交换当前节点的 右子树
		invertTree(root.right);
		//函数返回时就表示当前这个节点,以及它的左右子树
		//都已经交换完了
		return root;
	}
}

4. 平衡二叉树

●题目展示【Java 数据结构】二叉树的经典面试题 (图解)_第8张图片

●写题链接:力扣

●解题思路: 这个题需要借助求高度的代码,求平衡二叉树其实思路很简单,只要计算根结点的这棵树的左子树的高度和右子树的高度,然后相减判断是否 <= 1 ,如果满足,则继续往下判断,以根的左右孩子作为根,继续重复操作。当然这不是最好的方法,时间复杂度太高了。

  ●图解:   【Java 数据结构】二叉树的经典面试题 (图解)_第9张图片

 ●实现代码:时间复杂度为O(n^2)

    //求高度的代码 
    public int height(TreeNode root) {
        if(root == null) {
            return 0;
        }
        if(root.left == null && root.right == null) {
            return 1;
        }
        int leftH = height(root.left);
        int leftR = height(root.right);
        return leftH > leftR ? leftH+1 : leftR+1;
    }
    public boolean isBalanced(TreeNode root) {
        if(root == null) return true;
        int leftH = height(root.left);//左树的高度
        int rightH = height(root.right);//右树的高度
        //求出左右子树的高度后,判断差值是否 <= 1 ,满足就继续往下递归,看看左右子树是否也是平衡树
        return (Math.abs(leftH - rightH) <= 1) && (isBalanced(root.left) && isBalanced(root.right));
    }

思路二:如果按照第一种写法,在算最初的根节点那棵树时,就已经分别计算下面的左子树,或者右子树的左右子树的高度,如果按照那种方式继续递归下去,就会重复计算子树的高度,效率非常慢;于是第二种方法,就是改造求高度的函数,我们在递归的过程中就直接判断左右子树高度的差值是否 <= 1,满足我就返回左右子树高度中较大的一个加一,不满足就返回-1,因为高度不可能是负数,这样将省略很多不必要的计算;还要注意的一点就是,判断条件还要满足求出的左右子树高度均大于0,否则负数也将在递归的过程中参与计算了。

●图解:   【Java 数据结构】二叉树的经典面试题 (图解)_第10张图片

 ●实现代码:时间复杂度为O(n),大大提高了效率 

    public int height(TreeNode root) {
        if(root == null) return 0;
        //如果左树或者右树在计算的过程中拿到了负数说明不是平衡数直接返回负数
        int leftH = height(root.left);
        if( leftH < 0 ) {
            return -1;
        }   
        int rightH = height(root.right);
        if( rightH < 0 ) {
            return -1;
        }
        //我在求高度的时候就检查下面的树是否平衡
        if(Math.abs(leftH - rightH) <= 1) {
            return Math.max(leftH,rightH) + 1;
        } else {
            return -1;
        }
    }
    public boolean isBalanced(TreeNode root) {
        if(root == null) return true;
        return height(root) >= 0;
    }

5.对称二叉树

●题目展示【Java 数据结构】二叉树的经典面试题 (图解)_第11张图片

●写题链接:力扣

●解题思路: 

根据题目的描述,镜像对称,就是左右两边相等,也就是左子树和右子树是相当的。
注意这句话,左子树和右子相等,也就是说要递归的比较左子树和右子树。
我们将根节点的左子树记做 left,右子树记做 right。比较 left 是否等于 right,不等的话直接返回就可以了,如果相当,比较 left 的左节点和 right 的右节点,再比较 left 的右节点和 right 的左节点.

●图解: 

【Java 数据结构】二叉树的经典面试题 (图解)_第12张图片

●实现代码:

算法的时间复杂度是 O(n),因为要遍历 n 个节点.
空间复杂度是 O(n),空间复杂度是递归的深度,也就是跟树高度有关,最坏情况下树变成一个链表结构,高度是n.

class Solution {
	public boolean isSymmetric(TreeNode root) {
		if(root==null) {
			return true;
		}
		//调用递归函数,比较左节点,右节点
		return dfs(root.left,root.right);
	}
	
	boolean dfs(TreeNode left, TreeNode right) {
		//递归的终止条件是两个节点都为空
		//或者两个节点中有一个为空
		//或者两个节点的值不相等
		if(left==null && right==null) {
			return true;
		}
		if(left == null && right != null || left != null && right == null) {
			return false;
		}
		if(left.val!=right.val) {
			return false;
		}
		//再递归的比较 左节点的左孩子 和 右节点的右孩子
		//以及比较  左节点的右孩子 和 右节点的左孩子
		return dfs(left.left,right.right) && dfs(left.right,right.left);
	}
}

6.层序遍历

●题目展示

●写题链接:力扣

●解题思路: 

此题跟前面讲过的二叉树层序遍历类似,不同之处在于此题是将遍历结果分层存在了一个list里面,以一个二维列表的形式返回,还是跟之前一样,我们需要一个队列,先将根节点入队,检查队列是否为空并计算队列的个数,队列不为空则弹出一个队头元素存到一个小list里面,同时入队该节点的左右子树节点,重新上述操作,每一层节点存完都添加到大的list里面,当队列为空时循环结束;

⚠️⚠️⚠️注意:
1,循环之前让根节点 root 入队,循环的大前提条件是队列不为空,如果队列为空说明所有的结点全部出队,退出循环返回 list
2,内部循环控制每一层的循环次数,也就是这一层的节点个数, 每层循环开始前 new 一个新的 ArrayList,目的是为了存当前层的节点

●图解:  ●实现代码:

class Solution {
    public List> levelOrder(TreeNode root) {
        // 创建一个空的结果列表,用于存储每一层节点的值
        List> ret = new ArrayList<>();
        
        // 如果根节点为空,直接返回空结果列表
        if (root == null) return ret;
 
        // 创建一个队列,用于存储待处理的节点
        Queue queue = new LinkedList<>();
 
        // 将根节点加入队列
        queue.offer(root);
 
        // 当队列不为空时,说明还有节点需要处理
        while(!queue.isEmpty()) {
            // 获取当前层的节点数
            int size = queue.size();
            
            // 创建一个列表,用于存储当前层节点的值
            List row = new ArrayList<>();
            
            // 遍历当前层的节点
            while(size > 0) {
                // 弹出队列中的一个节点,并更新当前层节点数
                TreeNode cur = queue.poll();
                size--;
                
                // 将当前节点的值添加到当前层节点值列表中
                row.add(cur.val);
                
                // 如果当前节点有左孩子,将其加入队列
                if (cur.left != null) {
                    queue.offer(cur.left);
                }
                
                // 如果当前节点有右孩子,将其加入队列
                if (cur.right != null) {
                    queue.offer(cur.right);
                }  
            }
            
            // 将当前层节点值列表添加到结果列表中
            ret.add(row);
        }
        
        // 返回最终的结果列表
        return ret;
    }
}

7. 二叉树的构建及遍历

●题目展示【Java 数据结构】二叉树的经典面试题 (图解)_第13张图片

 ●写题链接:二叉树遍历_牛客题霸_牛客网

●解题思路: 

1.这道题的关键代码就是创建二叉树这个函数,题目给的是一串先序遍历的字符串,所以我们创建二叉树的时候,也是用先序遍历来创建的,这里可能会有人问:只知道先序遍历也能创建一棵二叉树吗?平时,我们必须要知道中序遍历和前后序遍历的其中一种,就能创建二叉树,但是这里不一样,它给的字符串,虽然我们只知道要按前序遍历来创建,但是它规定了空树在哪,所以只有一种情况,就可以创建二叉树。

2.得到这棵树之后,我们就可以从字符串入手去创建这棵树了,我们需要拿到字符串的每一个字符,换做以前,我们肯定要弄一个循环,但是二叉树的真正创建方式是用递归来创建的,所以不能用循环,我们需要一个变量 i ,通过 charAt()方法不断获取字符串的每一个字符,如果 i 放在函数里面的话,每次递归,i 都会重新变为 0 ,所以我们需要将 i 定义在方法的外面,接下来,就是实例化一个根节点,然后不断以左右子树作为根节点,递归下去。

●图解: 

  ●实现代码:

import java.util.*;
 
class TreeNode {
    char val;
    TreeNode left;
    TreeNode right;
    public TreeNode(char val) {
        this.val = val;
    }
}
 
public class Main {
    //主要代码
    public static int i = 0;
    //字符串是先序遍历的,所以创建这棵树就也是按照先序遍历来创建
    public static TreeNode createTree(String str) {
        TreeNode root = null;
        if(str.charAt(i) != '#') {
            root = new TreeNode(str.charAt(i));//创建根
            i++;
            root.left = createTree(str);//创建左树
            root.right = createTree(str);//创建右数
        } else {
            i++;
        }
        return root;
    }
    //中序遍历
    public static void inorder(TreeNode root) {
        if(root == null) return;
        inorder(root.left);
        System.out.print(root.val + " ");
        inorder(root.right);
    }
    
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        while(scan.hasNextLine()) {
            String str = scan.nextLine();
            TreeNode root = createTree(str);
            inorder(root);
        }
    }
}

 递归过程演示:【Java 数据结构】二叉树的经典面试题 (图解)_第14张图片

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

●题目展示

●写题链接:力扣

●解题思路: 

(1)方法一,看根结点是不是,递归左右子树【Java 数据结构】二叉树的经典面试题 (图解)_第15张图片

(2)方法二,使用两个栈来找公共祖先 【Java 数据结构】二叉树的经典面试题 (图解)_第16张图片

 实现代码:

 (1)方法一的代码

    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if(root == null) {
            return null;
        }
        //其中一个为root,则返回root
        if(root == p || root == q) {
            return root;
        }
 
        //找左右子树,是从下往上找的
        TreeNode leftRet = lowestCommonAncestor(root.left, p, q);
        TreeNode rightRet = lowestCommonAncestor(root.right, p, q);
 
        //如果都不为空,那么说明p,q在两侧
        if(leftRet != null && rightRet != null) {
            return root;
        } else if(leftRet != null) {
            //如果左不为空,那么右为空,说明p,q都在左侧,且leftRet为root
            return leftRet;
        } else if(rightRet != null){
            //如果右不为空,那么左为空,说明p,q都在右侧,且rightRet为root
            return rightRet;
        }
        return null;
    }

    (2) 方法二的代码

    private boolean getPath(TreeNode root, TreeNode node, Stack stack) {
        if(root == null || node == null) {
            return false;
        }
        stack.push(root);
        //递归过程中,如果根等于 p 或 q , 说明路径找完了,依次返回
        if(root == node) {
            return true;
        }
 
        //往左递归找 p 或 q
        boolean leftRet = getPath(root.left, node, stack);
        if(leftRet == true) {
            return true;
        }
 
        //往右递归找 p 或 q
        boolean rightRet = getPath(root.right, node, stack);
        if(rightRet == true) {
            return true;
        }
        //因为getPath方法,一进来就入栈,如果没有返回true,说明入栈的就不是路劲上的结点,直接弹出,然后返回false
        stack.pop();
        return false;
    }
 
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        //1.分别获取 p,q 的路径,依次添加到栈中
        Stack stack1 = new Stack<>();
        getPath(root, p, stack1);
 
        Stack stack2 = new Stack<>();
        getPath(root, q, stack2);
 
        //2.求出两个栈的大小,让大的先走差值步
        int size1 = stack1.size();
        int size2 = stack2.size();
 
        if(size1 > size2) {
            int size = size1 - size2;
            while(size != 0) {
                stack1.pop();
                size--;
            }
        } else {
            int size = size2 - size1;
            while(size != 0) {
                stack2.pop();
                size--;
            }
        }
 
        //再一起走,直到栈顶元素相等
        while(!stack1.empty() && ! stack2.empty()) {
            if(stack1.peek() == stack2.peek()) {
                return stack1.pop();
            } else {
                stack1.pop();
                stack2.pop();
            }
        }
        return null;
    }

9. 从前序遍历与中序遍历序列构造二叉树

●题目展示

●写题链接:力扣

●解题思路: 

1.定义 preIndex 遍历前序遍这个数组

2.在中序遍历的 inbegin - inend之间,找到 前序遍历元素 的位置

3.此时 preIndex 左边的就是左树,preIndex 右边的就是右树

4.递归创建左树和递归创建右树

5.当 inLeft > inRight 的时候,说明没有了左树或者右树

【Java 数据结构】二叉树的经典面试题 (图解)_第17张图片

  ●实现代码:

class Solution {
    public int preIndex = 0;
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        return buildTreeChild(preorder,inorder,0,inorder.length-1);
    }
    private TreeNode buildTreeChild(int[] preorder, int[] inorder,int inbegin,int inend){
        //没有了左树或者没有了右树
        if(inbegin > inend) {
            return null;
        }
        TreeNode root = new TreeNode(preorder[preIndex]);
        //找到当前根节点 在中序遍历中的位置
        int rootIndex = findInorder(inorder,preorder[preIndex],inbegin,inend);
        preIndex++;
        root.left = buildTreeChild(preorder, inorder,inbegin,rootIndex-1);
        root.right = buildTreeChild(preorder, inorder,rootIndex+1,inend);
        return root;
    }
 
    private int findInorder(int[] inorder,int val,int inbegin,int inend){
        for(int i = inbegin; i <= inend; i++) {
            if(inorder[i] == val) {
                return i;
            }
        }
        return -1;
    }
}

10.  从中序遍历与后序遍历序列构造二叉树

●题目展示

●写题链接:力扣

●解题思路: 这道题和前面那道题思路一样,唯一不一样的地方就是这里的 postIndex(后续遍历序列的下标)要从序列末尾开始,这样就形成了 根、右、左 的顺序了。

【Java 数据结构】二叉树的经典面试题 (图解)_第18张图片

  ●实现代码:

class Solution {
    public int postIndex =0;
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        postIndex = postorder.length-1;
         return buildTreeChild(postorder,inorder,0,inorder.length-1);
    }
    private TreeNode buildTreeChild(int[] postorder, int[] inorder,int inbegin,int inend){
        //没有了左树或者没有了右树
        if(inbegin > inend) {
            return null;
        }
        TreeNode root = new TreeNode(postorder[postIndex]);
        //找到当前根节点 在中序遍历中的位置
        int rootIndex = findInorder(inorder,postorder[postIndex],inbegin,inend);
        postIndex--;
        root.right = buildTreeChild(postorder, inorder,rootIndex+1,inend);
        root.left = buildTreeChild(postorder, inorder,inbegin,rootIndex-1);
        return root;
    }
 
    private int findInorder(int[] inorder,int val,int inbegin,int inend){
        for(int i = inbegin; i <= inend; i++) {
            if(inorder[i] == val) {
                return i;
            }
        }
        return -1;
    }
}

11. 根据二叉树创建字符串

 ●题目展示

●写题链接:力扣

●解题思路: 采用前序遍历

  1. 如果一个节点有左子树,那么在遍历该节点之后,需要在字符串中添加一个左括号 "("接下来递归遍历左子树,在遍历完左子树后,添加一个右括号 )。这对括号表示该节点的左子树。

  2. 如果一个节点没有左子树,但有右子树,那么在遍历该节点后,需要在字符串中添加一对空括号 "()"这对空括号表示该节点没有左子树。接下来,递归遍历右子树,并在遍历完右子树后,添加一个右括号 )。这对括号表示该节点的右子树。

  3. 如果一个节点有右子树,无论是否有左子树,都需要在递归遍历右子树前,在字符串中添加一个左括号 "("。接下来递归遍历右子树,在遍历完右子树后,添加一个右括号 ")"这对括号表示该节点的右子树

  ●实现代码:

class Solution {
    // 主方法,将二叉树转换成字符串表示
    public String tree2str(TreeNode root) {
        // 创建一个 StringBuilder 对象,用于存储结果字符串
        StringBuilder sb = new StringBuilder();
        
        // 调用辅助方法进行递归遍历
        tree2strChild(root, sb);
        
        // 返回结果字符串
        return sb.toString();
    }

    // 辅助方法,用于递归遍历二叉树
    private void tree2strChild(TreeNode t, StringBuilder sb) {
        // 如果当前节点为空,则直接返回
        if (t == null) return;

        // 将当前节点的值添加到结果字符串中
        sb.append(t.val);

        // 如果当前节点有左孩子
        if (t.left != null) {
            // 添加左括号
            sb.append("(");
            // 递归遍历左子树
            tree2strChild(t.left, sb);
            // 添加右括号
            sb.append(")");
        } else {
            // 如果当前节点没有左孩子,但有右孩子
            if (t.right != null) {
                // 添加一对空括号
                sb.append("()");
            } else {
                // 如果当前节点既没有左孩子,也没有右孩子,直接返回
                return;
            }
        }

        // 如果当前节点有右孩子
        if (t.right != null) {
            // 添加左括号
            sb.append("(");
            // 递归遍历右子树
            tree2strChild(t.right, sb);
            // 添加右括号
            sb.append(")");
        }
        // 如果当前节点没有右孩子,直接返回
        else {
            return;
        }
    }
}

你可能感兴趣的:(java,算法,数据结构)