二叉树

43 阅读3分钟

递归三要素

递归算法的三个要素。每次写递归,都按照这三要素来写,可以保证大家写出正确的递归算法!

  1. 确定递归函数的参数和返回值:  确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
  2. 确定终止条件:  写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
  3. 确定单层递归的逻辑:  确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。

前中后序遍历——递归法

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> res = new LinkedList<>();
        preorder(root, res);
        return res;
    }

    public void preorder(TreeNode cur, List<Integer> list) {
        if (cur == null) return;
        list.add(cur.val);
        preorder(cur.left, list);
        preorder(cur.right, list);
    }
}

前中后序遍历——迭代法

前序遍历,访问节点(遍历节点入栈)和处理节点(将元素放进result数组中)顺序一致,可以同步处理。

中序遍历,遍历节点和处理节点的顺序不一致故不能同步处理,不需要先将root入栈,而是直接走while循环,while内除了栈空判断还需要借助cur指针空判断,cur先一直向左走(同时入栈),走到头了(cur == null)就弹出一个处理,然后取右节点继续走。

后序遍历,可以在前序代码基础上修改左右元素入栈顺序,最后反转整个列表得到结果。

  • 前序
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> res = new LinkedList<>();
        if (root == null) return res;
        Deque<TreeNode> stack = new LinkedList<>();
        TreeNode cur = root;
        stack.push(cur);
        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);     // 左(左后入,先出)
        }
        return res;
    }
  • 中序
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> res = new LinkedList<>();
        if (root == null) return res;
        TreeNode cur = root;
        Deque<TreeNode> stack = new LinkedList();
        // 因为中序,遍历和处理节点的顺序不一致。所以while循环内还需要借助cur指针。
        while (cur != null || !stack.isEmpty()) {
            if (cur != null) {
                // 先一直往左走
                stack.push(cur);
                cur = cur.left;
            }else {
                // 走到头了,弹出一个处理,然后取右节点
                cur = stack.pop();
                res.add(cur.val);
                cur = cur.right;
            }
        }
        return res;
    }
  • 后序
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> res = new LinkedList<>();
        if (root == null) return res;
        Deque<TreeNode> stack = new LinkedList<>();
        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);
        return res;
    }

层序遍历

队列先进先出,符合一层一层遍历的逻辑。while循环,一个循环内遍历一层。注意每层先定义size记录该层节点数,然后再用while遍历本层的size个节点。

    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> res = new LinkedList<>();
        Deque<TreeNode> queue = new ArrayDeque<>();
        if (root == null) return res;
        queue.offer(root);
        while (!queue.isEmpty()) {
            int size = queue.size();
            List<Integer> list = new ArrayList<>();
            while (size-- != 0) {
                TreeNode cur = queue.poll();
                list.add(cur.val);
                if (cur.left != null) queue.offer(cur.left);
                if (cur.right != null)  queue.offer(cur.right);
            }
            res.add(list);
        }
        return res;
    }

反转二叉树

递归的中序遍历是不行的,因为使用递归的中序遍历,某些节点的左右孩子会翻转两次。

如果非要使用递归中序的方式写,也可以,如下代码就可以避免节点左右孩子翻转两次的情况:

class Solution {
public:
    TreeNode* invertTree(TreeNode* root) {
        if (root == NULL) return root;
        invertTree(root->left);         // 左
        swap(root->left, root->right);  // 中
        invertTree(root->left);         // 注意 这里依然要遍历左孩子,因为中间节点已经翻转了
        return root;
    }
};

代码虽然可以,但这毕竟不是真正的递归中序遍历了。但使用迭代方式统一写法的中序是可以的。

对称二叉树

首先想清楚,判断对称二叉树要比较的是什么,并不是左右节点!

对于二叉树是否对称,要比较的是根节点的左子树与右子树是不是相互翻转的,理解这一点就知道了其实我们要比较的是两个树(这两个树是根节点的左右子树) ,所以在递归遍历的过程中,也是要同时遍历两棵树。

递归法,三部曲。

class Solution {
    public boolean isSymmetric(TreeNode root) {
        return compare(root.left, root.right);
    }
    boolean compare(TreeNode left, TreeNode right) {
        if (left == null && right == null) return true;
        if (left == null || right == null || left.val != right.val) return false;
        return compare(left.left, right.right) && compare(left.right, right.left);
    }
}

迭代法,可以用队列、栈、双端队列模拟,只要控制好放入和取出比较的顺序。

class Solution {
    public boolean isSymmetric(TreeNode root) {
        Deque<TreeNode> que = new LinkedList<>();
        if (root == null) return true;
        if (root.left != null) que.offer(root.left);
        if (root.right != null) que.offer(root.right);
        while (!que.isEmpty()) {
            TreeNode t1 = que.poll();
            TreeNode t2 = que.poll();
            if (t1 == null && t2 == null) continue;
            if (t1 == null || t2 == null || t1.val != t2.val) return false;
            que.offer(t1.left);
            que.offer(t2.right);
            que.offer(t1.right);
            que.offer(t2.left); 
        }
        return true;
    }
}

二叉树最大深度

根节点的高度就是二叉树的最大深度

  • 层次遍历(简单
  • 递归,后序遍历。
    public int maxDepth(TreeNode root) {
        if (root == null) return 0;
        int leftDepth = maxDepth(root.left);
        int rightDepth = maxDepth(root.right);
        return Math.max(leftDepth, rightDepth) + 1;
    }

二叉树最小深度

求二叉树的最小深度和求二叉树的最大深度的差别主要在于处理左右孩子不为空的逻辑。

image.png
class Solution {
    public int minDepth(TreeNode root) {
        if (root == null) return 0;
        int left = minDepth(root.left);
        int right = minDepth(root.right);
        if (root.left == null && root.right == null) return 1;
        if (root.left == null) return 1 + right;
        if (root.right == null) return 1 + left;
        return 1 + Math.min(left, right);
    }
}

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

就是按照手工模拟的过程,构造。注意分割数组是用左右边界下标确定的。

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 dfs(0, inorder.length, 0, postorder.length, inorder, postorder);
    }
    // 由后序确定根节点;
    // 根据根节点切中序,得到左子树中序,右子树中序;
    // 根据左子树长度切后序,得到左右子树的后序;
    // 有了左右子树的中后序,可以递归造树。
    // 左闭右开
    TreeNode dfs(int inBegin, int inEnd, int postBegin, int postEnd, int[] inorder, int[] postorder) {
        if (inBegin >= inEnd || postBegin >= postEnd) return null;
        int val = postorder[postEnd - 1];
        TreeNode root = new TreeNode(val);
        int rootIndex = map.get(val);
        int len = rootIndex - inBegin;
        root.left = dfs(inBegin, rootIndex, postBegin, postBegin + len, inorder, postorder);
        root.right = dfs(rootIndex + 1, inEnd, postBegin + len, postEnd - 1, inorder, postorder);
        return root;
    }
}