LeeCode刷题指南 | 二叉树全整理

138 阅读9分钟

1 二叉树遍历

二叉树遍历有前中后序遍历三种方式,每种方式的递归和迭代遍历都是必须掌握的。

1.1 递归

二叉树的递归遍历比较统一,前中后序的遍历代码基本相同,只是交换了前后顺序。

//前序递归
//输入输出
List<Integer> res = new ArrayList<>();
preOrder(res, root);
return res;

//递归代码
private void preOrder(TreeNode node, List<> res) {
     if(node == null) return;
     //该处体现前序,中间节点先加入结果集,就是前序
     //中序和后序就是将该处代码顺序交换
     res.add(node.val);
     preOrder(node.left);
     preOrder(node.right);
}

1.2 迭代

迭代法的迭代过程是借助栈实现的。 迭代法的前序遍历和后序遍历基本类似,中序遍历则有所不同。

  1. 前序遍历与后序遍历。值得注意的是,每次从栈中弹出,需要用一个TreeNode来接住弹出的节点。
//1.前序迭代
public List<Integer> preOrdre(TreeNode root) {
    List<Integer> res = new ArrayList<>();
    if(root == null) return null;
    //对于栈,需要注意的地方是,不能将null加进去
    Stack<TreeNode> stack = new Stack<>();
    stack.push(root);
    while(!stack.isEmpty()) {
        //前序迭代,首先处理中间的元素
        TreeNode node = stack.pop();
        res.add(node.val);
        //对于左右的元素,应该是先处理最左的元素,然后处理最左元素对于的右元素
        //所以入栈顺序应该是先入右边,再如左边,弹出就是上面的顺序了
        if(node.right != null) stack.push(node.right);
        if(node.left != null) stack.push(node.left);
        
    }
}
//2.后序迭代
//后序迭代的思路是,以中-右-左的方式迭代,反转成左-右-中的形式
public List<Integer> postOrder(TreeNode root) {
    List<Integer> res = new ArrayList<>();
    if(root == null) return null;
    Stack<TreeNode> stack = new Stack<>();
    stack.push(root);
    while(!stack.isEmpty()) {
        TreeNode node = stack.pop();
        res.add(node.val);
        if(node.left != null) stack.push(node.left);
        if(node.right != null) stack.push(node.right);
    }
    Collections.reverse(res);
}

2.中序遍历:中序遍历为什么和前面两者不同?原因在于中序遍历的访问顺序与处理顺序是不同的,而前面两者中是相同的。所以中序遍历要借助指针来访问节点,而栈来处理元素。

  • 遍历过程中,每个节点都会做为小二叉树的中节点和左右节点。当作为中节点时,是在遍历过程中的,不加入结果集;作为左右节点时,加入结果集。
//3.中序
public List<Integer> postOrder(TreeNode root) {
    List<Integer> res = new ArrayList<>();
    if(root == null) return null;
    Stack<TreeNode> stack = new Stack<>();
    //这里与前面相反的地方,是因为root并不是第一个弹出处理的节点
    TreeNode cur = root;
    //对于该判断条件,有以下4种情况。有三种是成立的,所以选择与
    //1.cur != null stack.Empty 这是刚进入迭代,还未向栈中添加元素
    //2.cur != null !stack.Empty 这是迭代过程中
    //3.cur == null !stack.Empty 这是迭代到叶子节点
    //4.cur == null stack.Empty 迭代到叶子节点,而且栈中处理完了,表明遍历完这棵树了
    while(cur != null || !stack.isEmpty()) {
        //遍历过程中,cur != null 只有两种身份,左子节点或者是右子节点
        //cur != null 的处理就是入栈,遍历
        if(cur != null) {
            stack.push(cur);
            cur = cur.left;
        } 
        //cur == null 的情况就是遍历完一条路径,将会从左子节点或者右子节点跳回到父节点,即中间的点;而在此时,是作为上一个小二叉树的左子节点
        else {
            cur = stack.pop();
            res.add(cur);
            cur = cur.right;
        }
    }
}

1.3 统一迭代法

风格统一的前中后序迭代法。主要思路是空指针标记法,用空指针标记要加入结果集的元素,即在小二叉树中作为左右节点时的元素。

//1.中序遍历统一迭代法
public List<Integer> postOrder(TreeNode root) {
    List<Integer> res = new ArrayList<>();
    if(root == null) return null;
    Stack<TreeNode> stack = new Stack<>();
    stack.push(root);
    while(!stack.Empty()) {
        TreeNode node = stack.peek();
        //栈顶元素 != null  表明是作为遍历过程节点,那么要加入其左右子节点
        if(node != null) {
            stack.pop();
            if(node.right != null) stack.push(node.right);
            stack.push(node);
            stack.push(null);//遍历使命介绍,下次就加入结果集
            if(node.left != null) stack.push(node.left);
        }
        else {
            stack.pop();
            node = stack.pop();
            res.add(node.val);
        }
    }
    return res;
}

相比于中序遍历,前序和后序遍历的区别就在于:遍历过程中入栈的顺序,其差异如下:

//2.前序遍历
if(node != null) {
    stack.pop();
    if(node.right != null) stack.push(node.right);
    if(node.left != null) stack.push(node.left);
    stack.push(node);
    stack.push(null);
}

1.4 层序遍历

前面的所有遍历方式本质上都是深度优先遍历,即DFS。而层序遍历是BFS,广度优先遍历,这里会使用到的数据结构是队列。

class Solution {
    public List<List<Integer>> resList = new ArrayList<List<Integer>>();

    public List<List<Integer>> levelOrder(TreeNode root) {
        //checkFun01(root,0);
        checkFun02(root);

        return resList;
    }
    
    //1.递归实现层序遍历
    //传入参数:deep
    private void checkFun01(TreeNode node, Integer deep) {
        //终止条件
        if(node == null) return;
        deep += 1;//从上往下调用,每次传入的都是上一层的deep
        if(res.size() < deep) {
            res.add(new ArrayList<>());
        }
        res.get(deep-1).add(node.val);
        //传递给左右子节点
        checkFun01(node.left, deep);
        checkFun01(node.right, deep);
    }
    
    //2.迭代实现层序遍历
    //队列实现,每层从左到右依次进队,处理每个节点时,每个节点的左右子节点也依次进队
    private void checkFunc02(TreeNode node) {
        if(node == null) return;
        Queue<TreeNode> que = new LinkedList<TreeNode>();
        que.offer(node);
        while(!que.isEmpty()) {
            List<Integer> itemList = new ArrayList<>();
            int len = que.size();//获得当前层的长度
            while(len > 0) {
                TreeNode tmp = que.poll();
                itemList.add(tmp.val);
                if(tmp.left != null) que.offer(tmp.left);
                if(tmp.right != null) que.offer(tmp.right);
                len -= 1;
            }
            res.add(itemList);
        }
    }

2 构造二叉树

2.1 根据前中后序构造二叉树

2.1.1 前中后序数组分析

无论是何种顺序的数组,将最上层的节点去除后,左右子树在数组中也是左右分离的。这是分析这类题目的基础。

  1. 中序:1)最上层节点在数组中间,该节点的左右分别为左右子树。2)每个左右子树的序列的最后一位都为最右节点。
  2. 后序:1)每个独立二叉树序列中,最后一位为顶层节点(遍历时的当前节点)。
  3. 前序:与后序相反。

2.1.2 前序和中序构造二叉树

//前序与中序构造二叉树
//用Map实现查找操作
//采用左闭右开
Map<Integer, Integer> inMap;
    public TreeNode buildTree(int[] preorder, int[] inorder) { 
        if(preorder == null) return null;
        inMap = new HashMap<>();
        int n = inorder.length;
        for(int i = 0; i < n; i++) {
            inMap.put(inorder[i], i);
        }
        return findNode(preorder, inorder, 0, n, 0,n);
    }
    private TreeNode findNode(int[] preorder, int[] inorder, int inBegin, int inEnd, int preBegin, int preEnd) {
        if(inBegin >= inEnd && preBegin >= preEnd) return null;
        int index = inMap.get(preorder[preBegin]);
        int len = index - inBegin;
        //len表示左右子树的长度
        //指针加上len就是右开的右边界的值
        TreeNode node = new TreeNode(preorder[preBegin]);
        node.left = findNode(preorder, inorder, inBegin, index, preBegin + 1, preBegin + len + 1);
        node.right = findNode(preorder, inorder, index + 1, inEnd, preBegin + len + 1, preEnd);

        return node;
    }
    

2.2 根据层序遍历构造二叉树

2.2.1 层序遍历数组分析

此处考虑有null值的数组,整体满足层序的顺序。同时缺少的左右子节点也通过null值标识。

image.png

上图所示数组为[1,2,3,null,null,4,5]。可以得到:

  1. 除了根节点外,其他节点都是左右成对出现,遍历时可以成对遍历。
  2. 没有子节点的情况,通过不加入队列实现。

2.2.2 构造二叉树

//层序遍历构造二叉树
public TreeNode deserialize(Integer[] data) {
    Queue<TreeNode> queue = new LinkedList<>();
    //层序遍历使用队列来实现:
    //队列中只进不为null的值,入队以下层元素的身份入队
    //出队以上层节点的身份出队
    TreeNode root = new TreeNode(data[0]);
    queue.add(root);
    int i = 1;
    //指针 i:
    //1.每次出队,指针都指向的是出队节点的左子节点
    //2.每个节点,指针都要向后移两位
    while(!queue.isEmpty()) {
        TreeNode node = queue.poll();
        if(val[i] != null) {
            node.left = new TreeNode(val[i]);
            queue.add(node.left);
        }
        node += 1;
        if(val[i] != null) {
            node.right = new TreeNode(val[i]);
            queue.add(node.right);
        }
    }
    return root;
}

3 二叉树混合其他题型

3.1 二叉树 + 前缀和 + 哈希表

LeeCode原题链接:leetcode.cn/problems/pa…

3.1.1 题目要求及解析

  1. 题目要求:求出二叉树中节点之和为targetSum的路径数目。路径不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的
  2. 解析:遍历二叉树的过程中构造前缀和表。该表中的前后任意两值相减可以得到任意路径的和。
preSum[end]preSum[Begin]=targetSumpreSum[end]targetSum=preSum[Begin]preSum[end] - preSum[Begin] = targetSum \\ preSum[end]-targetSum = preSum[Begin]

根据以上公式可得对于遍历到的节点,只需要寻找前序前缀和中是否有preSum - targetSum的值

3.1.2 代码实现

//路径总和:前缀和 + 哈希表
class Solution {
    private int ans;
    public int pathSum(TreeNode root, int targetSum) {
        Map<Long, Integer> preSum = new HashMap<>();//哈希表
        preSum.put(0L, 1);
        long s = 0;//前缀和
        dfs(root, targetSum, s, preSum);
        return ans;
    }
    private void dfs(TreeNode root, int targetSum, long s, Map<Long, Integer> preSum) {
    //前序DFS遍历,每遍历一个节点更新前缀和
        if(root == null) return;
        s += root.val;//更新前缀和
        ans += preSum.getOrDefault(s - targetSum, 0);//更新结果,getOrDefault:有就返回,没有就返回默认值
        preSum.merge(s, 1, Integer::sum);//更新前缀和表,merge:修改哈希表中元素,有就sum,没有就设为1。
        dfs(root.left, targetSum, s, preSum);
        dfs(root.right, targetSum, s, preSum);
        preSum.merge(s, -1, Integer::sum);//回溯,恢复现场
    }
}

3.2 二叉树 + 动归

LeeCode链接:leetcode.cn/problems/un…

3.2.1 题目要求

  1. 题目要求:求出从1到n的n个节点组成的二叉树一共有多少种。
  2. 解析:n个节点的二叉树的种类可由n个节点分别做根节点组成。
count[n]=fori(count[i])count[i]=forj(count[ij]count[j])count[n] = fori(count[i])\\ count[i] = forj(count[i-j] * count[j])\\

3.2.2 代码实现

//二叉树种类:动规
public int numTrees(int n) {
        int[] dp = new int[n+1];
        dp[0] = 1;
        dp[1] = 1;

        for(int i = 2; i <= n; i++) {
            for(int j = 1; j <= i; j++) {
                dp[i] += dp[j-1] * dp[i-j];
                //上述代码实现
            }
        }

        return dp[n];
    }

Hot100 二叉树题型整理

分类题号题目解法
遍历类型(1)236二叉树公共祖先DFS,后序递归遍历,找左右子树是否有节点
遍历类型(1)124二叉树最大路径和中序递归DFS,每个节点判断其子树有无最大路径,然后将更长的一半子树返给上层
遍历类型(1)543二叉树直径与124类似
遍历类型(1)538把二叉树转换为累加树DFS递归,右中左
遍历类型(1)114把二叉树转换为链表(先序遍历顺序相同)DFS递归,将每个节点的右边接为左边、左边的末尾接到右边
遍历类型(1)617合并二叉树DFS前序
遍历类型(1)104二叉树的最大深度DFS后序,从底层一个个加上来
遍历类型(1)101对称二叉树DFS对称遍历,(左左、右右)
遍历类型(1)98验证二叉搜索树中序遍历,按顺序比较max
构造类型(2)297二叉树序列化与反序列化编码:层序遍历,存储。解码:层序遍历数组构造二叉树。均使用队列
构造类型(2)105前序与中序遍历序列构造二叉树未提及具体解法
混合类型(3)96不同的二叉搜索树动态规划
混合类型(3)437路径总和前缀和 + 哈希表,前序递归遍历,遍历从根节点开始的所有路径。后面的前缀和 - 目标和 == 前面的前缀和,就找到了一个可行路径