大厂面试算法-二叉树专题

105 阅读14分钟

@TOC 不要纠结,干就完事了,熟练度很重要!!!多练习,多总结!!!

纲领篇

在这里插入图片描述 二叉树的所有问题,就是让你在前中后序位置注入巧妙的代码逻辑,去达到自己的目的,你只需要单独思考每一个节点应该做什么,其他的不用你管,抛给二叉树遍历框架,递归会在所有节点上做相同的操作。

二叉树题目的递归解法可以分两类思路,第一类是遍历一遍二叉树得出答案,第二类是通过分解问题计算出答案,这两类思路分别对应着 回溯算法核心框架 和 动态规划核心框架。

前序位置是刚刚进入节点的时刻,后序位置是即将离开节点的时刻。意味着前序位置的代码只能从函数参数中获取父节点传递来的数据,而后序位置的代码不仅可以获取参数数据,还可以获取到子树通过函数返回值传递回来的数据。

后序位置的特点,只有后序位置才能通过返回值获取子树的信息。一旦你发现题目和子树有关,那大概率要给函数设置合理的定义和返回值,在后序位置写代码了。

LeetCode 104. 二叉树的最大深度

在这里插入图片描述

解题思路

法一:很容易发现一棵二叉树的最大深度可以通过子树的最大高度推导出来,这就是分解问题计算答案的思路。 法二:显然遍历一遍二叉树,用一个外部变量记录每个节点所在的深度,取最大值就可以得到最大深度,这就是遍历二叉树计算答案的思路。

代码实现

法一

class Solution {
    public int maxDepth(TreeNode root) {
        if(root == null){
            return 0;
        }
        int left = maxDepth(root.left);
        int right = maxDepth(root.right);
        return Math.max(left, right)+1;
    }
}

法二

// 记录最大深度
int res = 0;
// 记录遍历到的节点的深度
int depth = 0;

// 主函数
int maxDepth(TreeNode root) {
    traverse(root);
    return res;
}

// 二叉树遍历框架
void traverse(TreeNode root) {
    if (root == null) {
        // 到达叶子节点,更新最大深度
        res = Math.max(res, depth);
        return;
    }
    // 前序位置
    depth++;
    traverse(root.left);
    traverse(root.right);
    // 后序位置
    depth--;
}

遇到一道二叉树的题目时的通用思考过程是:

  1. 是否可以通过遍历一遍二叉树得到答案?如果可以,用一个traverse函数配合外部变量来实现。
  2. 是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值。
  3. 无论使用哪一种思维模式,你都要明白二叉树的每一个节点需要做什么,需要在什么时候(前中后序)做。

LeetCode 543. 二叉树的直径

在这里插入图片描述

解题思路

每一条二叉树的「直径」长度,就是一个节点的左右子树的最大深度之和。 现在让我求整棵树中的最长「直径」,那直截了当的思路就是遍历整棵树中的每个节点,然后通过每个节点的左右子树的最大深度算出每个节点的「直径」,最后把所有「直径」求个最大值即可。

代码实现

class Solution {
    int maxDia = 0;
    public int diameterOfBinaryTree(TreeNode root) {
        maxDep(root);
        return maxDia;
    }

    public int maxDep(TreeNode root){
        if(root == null){
            return 0;
        }
        int left = maxDep(root.left);
        int right = maxDep(root.right);
        maxDia = Math.max(maxDia, left+right);
        return 1+Math.max(left, right);

    }
}

LeetCode 124. 二叉树中的最大路径和

在这里插入图片描述

解题思路

参考LeetCode 543. 二叉树的直径

代码实现

class Solution {
    int pathMax = Integer.MIN_VALUE;
    public int maxPathSum(TreeNode root) {
        pathNum(root);
        return pathMax;
    }

    public int pathNum(TreeNode root){
        if(root == null){
            return 0;
        }
        int left = Math.max(pathNum(root.left), 0);
        int right = Math.max(pathNum(root.right), 0);
        pathMax = Math.max(left+right+root.val, pathMax);
        return Math.max(left, right)+root.val;

    }
}

LeetCode 144. 二叉树的前序遍历

在这里插入图片描述

代码实现

class Solution {
    List<Integer> res = new ArrayList<>();
    public List<Integer> preorderTraversal(TreeNode root) {
        traverse(root);
        return res;
    }

    public void traverse(TreeNode root){
        if(root == null){
            return ;
        }

        res.add(root.val);
        traverse(root.left);
        traverse(root.right);
    }

}

二叉树层序遍历

// 输入一棵二叉树的根节点,层序遍历这棵二叉树
void levelTraverse(TreeNode root) {
    if (root == null) return;
    Queue<TreeNode> q = new LinkedList<>();
    q.offer(root);

    // 从上到下遍历二叉树的每一层
    while (!q.isEmpty()) {
        int sz = q.size();
        // 从左到右遍历每一层的每个节点
        for (int i = 0; i < sz; i++) {
            TreeNode cur = q.poll();
            // 将下一层节点放入队列
            if (cur.left != null) {
                q.offer(cur.left);
            }
            if (cur.right != null) {
                q.offer(cur.right);
            }
        }
    }
}

思维篇

LeetCode 226. 翻转二叉树

在这里插入图片描述

解题思路

法一:这题能不能用「遍历」的思维模式解决? 可以,我写一个traverse函数遍历每个节点,让每个节点的左右子节点颠倒过来就行了。 单独抽出一个节点,需要让它做什么?让它把自己的左右子节点交换一下。 需要在什么时候做?好像前中后序位置都可以。

法二:这题能不能用「分解问题」的思维模式解决? 我们尝试给invertTree函数赋予一个定义:

// 定义:将以 root 为根的这棵二叉树翻转,返回翻转后的二叉树的根节点
TreeNode invertTree(TreeNode root);

然后思考,对于某一个二叉树节点x执行invertTree(x),你能利用这个递归函数的定义做点啥? 我可以用invertTree(x.left)先把x的左子树翻转,再用invertTree(x.right)把x的右子树翻转,最后把x的左右子树交换,这恰好完成了以x为根的整棵二叉树的翻转,即完成了invertTree(x)的定义。

代码实现

法一:

class Solution {
    public TreeNode invertTree(TreeNode root) {
        traverse(root);
        return root;

    }

    public TreeNode traverse(TreeNode root){
        if(root == null){
            return null;
        }

        traverse(root.left);
        traverse(root.right);
        TreeNode tmp = root.left;
        root.left = root.right;
        root.right = tmp;
        return root;
    }
}

法二:

// 定义:将以 root 为根的这棵二叉树翻转,返回翻转后的二叉树的根节点
TreeNode invertTree(TreeNode root) {
    if (root == null) {
        return null;
    }
    // 利用函数定义,先翻转左右子树
    TreeNode left = invertTree(root.left);
    TreeNode right = invertTree(root.right);

    // 然后交换左右子节点
    root.left = right;
    root.right = left;

    // 和定义逻辑自恰:以 root 为根的这棵二叉树已经被翻转,返回 root
    return root;
}

LeetCode 116. 填充每个节点的下一个右侧节点指针

在这里插入图片描述

解题思路

这题能不能用「遍历」的思维模式解决? 很显然,一定可以。 每个节点要做的事也很简单,把自己的next指针指向右侧节点就行了。

传统的traverse函数是遍历二叉树的所有节点,但现在我们想遍历的其实是两个相邻节点之间的「空隙」。 可以在二叉树的基础上进行抽象,你把图中的每一个方框看做一个节点: 在这里插入图片描述 一棵二叉树被抽象成了一棵三叉树,三叉树上的每个节点就是原先二叉树的两个相邻节点。 现在,我们只要实现一个traverse函数来遍历这棵三叉树,每个「三叉树节点」需要做的事就是把自己内部的两个二叉树节点穿起来:

代码实现

class Solution {
    public Node connect(Node root) {
        if(root == null){
            return null;
        }
        traverse(root.left, root.right);
        return root;
    }


    public void traverse(Node node1, Node node2){
        if(node1==null || node2== null){
            return ;
        }
        node1.next=node2;
        traverse(node1.left, node1.right);
        traverse(node2.left, node2.right);
        traverse(node1.right, node2.left);
        return ;
    }
}

LeetCode 114. 二叉树展开为链表

在这里插入图片描述

解题思路

这题能不能用「分解问题」的思维模式解决?尝试给出flatten函数的定义:

// 定义:输入节点 root,然后 root 为根的二叉树就会被拉平为一条链表
void flatten(TreeNode root);

有了这个函数定义,如何按题目要求把一棵树拉平成一条链表? 对于一个节点x,可以执行以下流程:

  1. 先利用flatten(x.left)和flatten(x.right)将x的左右子树拉平。
  2. 将x的右子树接到左子树下方,然后将整个左子树作为右子树。

代码实现

class Solution {
    public void flatten(TreeNode root) {
        if(root == null){
            return ;
        }

        flatten(root.left);
        flatten(root.right);
        TreeNode left = root.left;
        TreeNode right = root.right;

        root.left=null;
        root.right=left;
        TreeNode p = root;
        while(p.right != null){
            p = p.right;
        }
        p.right = right;

    }
}

构造篇

二叉树的构造问题一般都是使用「分解问题」的思路:构造整棵树 = 根节点 + 构造左子树 + 构造右子树。

LeetCode 654. 最大二叉树

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

解题思路

每个二叉树节点都可以认为是一棵子树的根节点,对于根节点,首先要做的当然是把想办法把自己先构造出来,然后想办法构造自己的左右子树。

所以,我们要遍历数组把找到最大值maxVal,从而把根节点root做出来,然后对maxVal左边的数组和右边的数组进行递归构建,作为root的左右子树。

当前nums中的最大值就是根节点,然后根据索引递归调用左右数组构造左右子树即可。

代码实现

class Solution {
    public TreeNode constructMaximumBinaryTree(int[] nums) {
        return build(nums, 0, nums.length-1);
    }


    public TreeNode build(int[] nums, int i, int j){
        if(i > j){
            return null;
        }
        int idx = -1, maxVal = Integer.MIN_VALUE;
        for(int k = i; k <= j;k++){
            if(nums[k] > maxVal){
                maxVal = nums[k];
                idx = k;
            }
        }

        TreeNode root = new TreeNode(maxVal);
        root.left = build(nums, i, idx-1);
        root.right = build(nums, idx+1, j);
        return root;
    }
}

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

在这里插入图片描述

解题思路

root.left = build(preorder, ?, ?,
                  inorder, inStart, index - 1);

root.right = build(preorder, ?, ?,
                   inorder, index + 1, inEnd);

在这里插入图片描述

代码实现

class Solution {
    Map<Integer, Integer> map = new HashMap<>();
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        for(int i = 0;i < inorder.length;i++){
            map.put(inorder[i], i);
        }
        return build(preorder, 0, preorder.length-1, inorder, 0, inorder.length-1);
    }

    public TreeNode build(int[] pre, int i, int j,int[] in, int m, int n){
        if(i > j){
            return null;
        }
        
        TreeNode root = new TreeNode(pre[i]);
        int idx = map.get(pre[i]);
        int size = idx-m;
        root.left = build(pre, i+1, i+size, in, m, idx-1);
        root.right = build(pre, i+size+1, j, in, idx+1, n);
        return root;
    }
}

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

在这里插入图片描述

解题思路

在这里插入图片描述

代码实现

class Solution {
    Map<Integer, Integer> map = new HashMap<>();
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        for(int i = 0;i < inorder.length;i++){
            map.put(inorder[i], i);
        }
        return buildTree(inorder, 0, inorder.length-1, postorder, 0, postorder.length-1);

    }

    public TreeNode buildTree(int[] in, int i, int j, int[] post, int m, int n){
        if(i > j){
            return null;
        }
        int rootVal = post[n];
        int index = map.get(rootVal);
        TreeNode root = new TreeNode(rootVal);
        int size = index - i;
        root.left = buildTree(in, i, index-1, post, m, m+size-1);
        root.right = buildTree(in, index+1, j, post, m+size, n-1);
        return root;
    }
}

LeetCode 889. 根据前序和后序遍历构造二叉树

在这里插入图片描述

解题思路

通过前序中序,或者后序中序遍历结果可以确定一棵原始二叉树,但是通过前序后序遍历结果无法确定原始二叉树。 题目也说了,如果有多种可能的还原结果,你可以返回任意一种。

用后序遍历和前序遍历结果还原二叉树,解法逻辑上和前两道题差别不大,也是通过控制左右子树的索引来构建: **1. 首先把前序遍历结果的第一个元素或者后序遍历结果的最后一个元素确定为根节点的值。

  1. 然后把前序遍历结果的第二个元素作为左子树的根节点的值。
  2. 在后序遍历结果中寻找左子树根节点的值,从而确定了左子树的索引边界,进而确定右子树的索引边界,递归构造左右子树即可。**

在这里插入图片描述

代码实现

class Solution {
    Map<Integer, Integer> map = new HashMap<>();
    public TreeNode constructFromPrePost(int[] preorder, int[] postorder) {
        for(int i = 0; i < postorder.length;i++){
            map.put(postorder[i], i);
        }
        return build(preorder, 0, preorder.length-1, postorder, 0, postorder.length-1);
    }

    public TreeNode build(int[] pre,int i, int j, int[] post, int m, int n){
        if(i > j){
            return null;
        }
        if(i == j){
            return new TreeNode(pre[i]);
        }

        TreeNode root = new TreeNode(pre[i]);
        int leftVal = pre[i+1];
        int idx = map.get(leftVal);
        int size = idx-m+1;
        root.left = build(pre, i+1, i+size, post, m, idx);
        root.right = build(pre, i+size+1, j, post, idx+1, n-1);
        return root;
    }
}

序列化篇

LeetCode 297. 二叉树的序列化与反序列化

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

解题思路

法一:前序遍历 在这里插入图片描述 法二:后序遍历 在这里插入图片描述 法三:层序遍历 在这里插入图片描述

代码实现

法一:前序遍历

public class Codec {

    private String SEP = ";";
    private String NULL = "#";
    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        StringBuilder sb = new StringBuilder();
        serialize(root, sb);
        return sb.toString();
    }

    public void serialize(TreeNode root, StringBuilder sb){
        if(root == null){
            sb.append(NULL).append(SEP);
            return;
        }

        sb.append(root.val).append(SEP);
        serialize(root.left, sb);
        serialize(root.right, sb);
        return;
    }

    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        String[] arr = data.split(";");
        LinkedList<String> list = new LinkedList<>();
        for(String s:arr){
            list.addLast(s);
        }
        return deserialize(list);        

    }

    public TreeNode deserialize(LinkedList<String> list){
        if(list.isEmpty()){
            return null;
        }

        String rootVal = list.removeFirst();
        if(rootVal.equals(NULL)){
            return null;
        }
        TreeNode root = new TreeNode(Integer.parseInt(rootVal));
        root.left = deserialize(list);
        root.right = deserialize(list);
        return root;
    }
}

法二:后序遍历

public class Codec {

    private String SEP = ";";
    private String NULL = "#";
    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        StringBuilder sb = new StringBuilder();
        serialize(root, sb);
        return sb.toString();
    }

    public void serialize(TreeNode root, StringBuilder sb){
        if(root == null){
            sb.append(NULL).append(SEP);
            return;
        }

        serialize(root.left, sb);
        serialize(root.right, sb);
        sb.append(root.val).append(SEP);
        return;
    }

    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        String[] arr = data.split(";");
        LinkedList<String> list = new LinkedList<>();
        for(String s:arr){
            list.addLast(s);
        }
        return deserialize(list);        

    }

    public TreeNode deserialize(LinkedList<String> list){
        if(list.isEmpty()){
            return null;
        }

        String rootVal = list.removeLast();
        if(rootVal.equals(NULL)){
            return null;
        }
        TreeNode root = new TreeNode(Integer.parseInt(rootVal));
        root.right = deserialize(list);
        root.left = deserialize(list);
        return root;
    }
}

法三:层序遍历

public class Codec {

    String SEP = ",";
    String NULL = "#";

    /* 将二叉树序列化为字符串 */
    public String serialize(TreeNode root) {
        if (root == null) return "";
        StringBuilder sb = new StringBuilder();
        // 初始化队列,将 root 加入队列
        Queue<TreeNode> q = new LinkedList<>();
        q.offer(root);

        while (!q.isEmpty()) {
            TreeNode cur = q.poll();

            /* 层级遍历代码位置 */
            if (cur == null) {
                sb.append(NULL).append(SEP);
                continue;
            }
            sb.append(cur.val).append(SEP);
            /*****************/

            q.offer(cur.left);
            q.offer(cur.right);
        }

        return sb.toString();
    }

    public TreeNode deserialize(String data) {
        if (data.isEmpty()) return null;
        String[] nodes = data.split(SEP);
        // 第一个元素就是 root 的值
        TreeNode root = new TreeNode(Integer.parseInt(nodes[0]));

        // 队列 q 记录父节点,将 root 加入队列
        Queue<TreeNode> q = new LinkedList<>();
        q.offer(root);

        for (int i = 1; i < nodes.length; ) {
            // 队列中存的都是父节点
            TreeNode parent = q.poll();
            // 父节点对应的左侧子节点的值
            String left = nodes[i++];
            if (!left.equals(NULL)) {
                parent.left = new TreeNode(Integer.parseInt(left));
                q.offer(parent.left);
            } else {
                parent.left = null;
            }
            // 父节点对应的右侧子节点的值
            String right = nodes[i++];
            if (!right.equals(NULL)) {
                parent.right = new TreeNode(Integer.parseInt(right));
                q.offer(parent.right);
            } else {
                parent.right = null;
            }
        }
        return root;
    }
}

后序篇

LeetCode 652. 寻找重复的子树

在这里插入图片描述

解题思路

你需要知道以下两点:

  1. 以我为根的这棵二叉树(子树)长啥样?
  2. 以其他节点为根的子树都长啥样?

代码实现

class Solution {
     Map<String, Integer> map = new HashMap<>();
    List<TreeNode> res = new ArrayList<>();
    public List<TreeNode> findDuplicateSubtrees(TreeNode root) {
        traverse(root);
        return res;
    }

    public String traverse(TreeNode root){
        if(root == null){
            return "#";
        }
        String left = traverse(root.left);
        String right = traverse(root.right);
        String str = left+","+right+","+root.val;
        int count = map.getOrDefault(str, 0);
        if(count == 1){
            res.add(root);
        }
        map.put(str, count+1);
        return str;
    }
}

归并排序

LeetCode 912. 排序数组

在这里插入图片描述

解题思路

归并排序的过程可以在逻辑上抽象成一棵二叉树,树上的每个节点的值可以认为是nums[lo..hi],叶子节点的值就是数组中的单个元素: 在这里插入图片描述 然后,在每个节点的后序位置(左右子节点已经被排好序)的时候执行merge函数,合并两个子节点上的子数组: 在这里插入图片描述 这个merge操作会在二叉树的每个节点上都执行一遍,执行顺序是二叉树后序遍历的顺序

代码实现

class Solution {
    int[] temp;
    public int[] sortArray(int[] nums) {
        temp = new int[nums.length];
        sort(nums, 0, nums.length-1);
        return nums;
    }

    public void sort(int[] nums, int low, int high){
        if(low >= high){
            return;
        }
        int mid = low + (high-low)/2;
        sort(nums, low, mid);
        sort(nums, mid+1, high);
        merge(nums, low, mid, high);

    }

    public void merge(int[] nums, int low, int mid, int high){
        for(int i = low; i <= high ;i++){
            temp[i] = nums[i];
        }

        int i = low, j = mid+1;
        for(int k = low;k <= high;k++){
            if(i == mid + 1){
                nums[k] = temp[j++];
            }else if(j == high+1){
                nums[k] = temp[i++];
            }else if(temp[i] > temp[j]){
                nums[k] = temp[j++];
            }else {
                nums[k] = temp[i++];
            }
        }
    }
}

LeetCode 315. 计算右侧小于当前元素的个数

在这里插入图片描述

解题思路

在这里插入图片描述 在对nuns[lo..hi]合并的过程中,每当执行nums[p] = temp[i]时,就可以确定temp[i]这个元素后面比它小的元素个数为j - mid - 1。

当然,nums[lo..hi]本身也只是一个子数组,这个子数组之后还会被执行merge,其中元素的位置还是会改变。但这是其他递归节点需要考虑的问题,我们只要在merge函数中做一些手脚,就可以让每个递归节点叠加每次merge时记录的结果。

代码实现

class Solution {
    Pair[] tmp;
    int[] count;
    public List<Integer> countSmaller(int[] nums) {
        tmp = new Pair[nums.length];
        count = new int[nums.length];
        Pair[] arr = new Pair[nums.length];
        for(int i = 0;i < nums.length;i++){
            arr[i] = new Pair(nums[i], i);
        }
        sort(arr, 0, nums.length-1);
        List<Integer> list = new ArrayList<>();
        for(int c : count){
            list.add(c);
        }
        return list;

    }

    public void sort(Pair[] arr, int low, int high){
        if(low >= high){
            return;
        }
        int mid = low+(high-low)/2;
        sort(arr, low, mid);
        sort(arr, mid+1, high);
        merge(arr, low, mid, high);

    }

    public void merge(Pair[] arr, int low, int mid, int high){
        for(int i = low; i <= high;i++){
            tmp[i] = arr[i];
        }

        int i = low, j = mid+1;
        for(int k = low;k<= high;k++){
            if(i == mid+1){
                arr[k] = tmp[j++];
            }else if(j == high+1){
                arr[k] = tmp[i++];
                count[arr[k].idx]+=j-mid-1;
            }else if(tmp[i].val > tmp[j].val){
                arr[k] = tmp[j++];
            }else {
                arr[k] = tmp[i++];
                count[arr[k].idx]+=j-mid-1;
            }

        }

    }


}

class Pair{
    int val;
    int idx;
    Pair(int val, int idx){
        this.val = val;
        this.idx = idx;
    }
}

总结

本题来源于Leetcode中 归属于二叉树类型题目。 同许多在算法道路上不断前行的人一样,不断练习,修炼自己! 如有博客中存在的疑问或者建议,可以在下方留言一起交流,感谢各位!

觉得本博客有用的客官,可以给个点赞+收藏哦! 嘿嘿

喜欢本系列博客的可以关注下,以后除了会继续更新面试手撕代码文章外,还会出其他系列的文章!