LeetCode刷题总结之二叉树的构建算法-一道题13种解法

前言

最近开始刷到一些二叉树的构建的算法题,挺有意思的,打算总结一下。这里总结的都是确定二叉树的构造算法题,可能有多个构造结果的算法题就没考虑。

从构造目标上来看,这里讨论的算法题可以分为两种:

  1. 二叉树的构造
  2. 二叉搜索树(BST)的构造

从构造条件上来看,这里讨论的算法题也可以分为两种:

  1. 不含重复数值节点的二叉树的构造
  2. 含重复数值节点的二叉树的构造

1.从前序与中序遍历以及中序和后序遍历构造二叉树

这2个题目分别为:

  1. LeetCode.105 从前序与中序遍历序列构造二叉树,中等难度
  2. LeetCode.106 从中序与后序遍历序列构造二叉树,中等难度

1.1 解题方法

首先,按之前我们给分类条件给这两种题目一个定性:它们都是一个不含重复节点的二叉树构造算法题。这2个题目的思路和做法都是一样的:

  1. 首先从先序/后序序列中找到根节点,根据根节点将中序序列分为左子树和右子树。
  2. 递归地对左子树和右子树进行二叉树的构造。

思路其实是比较好想的,如果你在面试中遇到了这2个题目,那其实考察的编码的基本功了。虽然比较好想,但是一次把代码写出来且保证AC还是有一定难度的。

1.2 复杂度分析

时间复杂度: O(n),由于每次递归我们的inorder和preorder的总数都会减1,因此我们要递归n次。 空间复杂度: O(n),递归n次,系统调用栈的深度为n。

1.3 Show the code

public class Q105BuildTree {
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        int len = preorder.length;
        if (len == 0) return null;

        return buildTreeNode(preorder, 0, len - 1, inorder, 0, len - 1);
    }

    private TreeNode buildTreeNode(int[] preorder, int start1, int end1, int[] inorder, int start2, int end2) {
        int rootVal = preorder[start1];
        TreeNode root = new TreeNode(rootVal);

        if (start1 < end1) {
            int idx = findRootIdxInOrder(inorder, start2, end2, rootVal);
            int leftLen = idx - start2;
            int rightLen = end2 - idx;

            if (leftLen > 0) {
                root.left = buildTreeNode(preorder, start1 + 1, start1 + leftLen, inorder, start2, start2 + leftLen - 1);
            }

            if (rightLen > 0) {
                root.right = buildTreeNode(preorder, start1 + 1 + leftLen, end1, inorder, idx + 1, end2);
            }
        }

        return root;
    }

    private int findRootIdxInOrder(int[] array, int start, int end, int val) {
        for (int i = start; i <= end; i++) {
            if (array[i] == val) {
                return i;
            }
        }
        throw new UnsupportedOperationException("Unreachable logic!");
    }
}
复制代码
public class Q106BuildTree {
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        if (inorder.length == 0) return null;
        return buildTreeTrace(inorder, 0, inorder.length - 1, postorder, 0, postorder.length - 1);
    }

    private TreeNode buildTreeTrace(int[] inorder, int inLeft, int inRight, int[] postorder, int postLeft, int postRight) {
        int rootVal = postorder[postRight];
        TreeNode root = new TreeNode(rootVal);
        if (postLeft == postRight) {
            return root;
        }

        int inOrderRootIdx = findRootIdxInOrder(inorder, inLeft, inRight, rootVal);
        int leftTreeLen = inOrderRootIdx - inLeft;
        if (leftTreeLen > 0) {
            root.left = buildTreeTrace(inorder, inLeft, inLeft + leftTreeLen - 1, postorder, postLeft, postLeft + leftTreeLen - 1);
        }
        int rightTreeLen = inRight - inOrderRootIdx;
        if (rightTreeLen > 0) {
            root.right = buildTreeTrace(inorder, inOrderRootIdx + 1, inRight, postorder, postLeft + leftTreeLen, postRight - 1);
        }
        return root;
    }

    private int findRootIdxInOrder(int[] inorder, int inLeft, int inRight, int rootVal) {
        for (int i = inLeft; i <= inRight; i++) {
            if (inorder[i] == rootVal) {
                return i;
            }
        }
        throw new UnsupportedOperationException("Unreachable logic!");
    }
}
复制代码

2. 从前序遍历构造BST以及序列化与反序列化BST

这2个题目分别为:

  1. LeetCode.449 序列化和反序列化二叉搜索树,中等难度
  2. LeetCode.1008 先序遍历构造二叉树,中等难度

同样地,按之前我们给分类条件给这两种题目一个定性:它们都是一个不含重复节点的二叉搜索树构造算法题。其中Q449的题干描述里面并没有给出“不含重复节点”的条件,但是它的测试用例里面都是“不含重复节点”的用例。这里,我们就暂且给它加上“不含重复节点”的条件。

很显然,仅仅是将这2个题目放在一起,我们就发现可以通过Q1008的解法去搞定Q449。于是我们这里先分析Q1008: 从前序遍历构造BST,随后再分析Q449: 序列化与反序列化BST

2.0 解题思考

问题1:为什么可以从前序遍历还原一个唯一的节点不重复的BST?

  1. 前序遍历是:根-左子树-右子树,那么对于一个前序遍历,我们便可以获取到BST的根节点。
  2. 拿到根节点后,我们便可以找到左子树的所有节点和右子树的所有节点。同样地,可以获取左子树和右子树的根节点。
  3. 由于左子树和右子树也是以根-左-右的方式进行遍历的,那么也适用于上述的构造方式。

问题2:可不可以通过后序遍历还原一个唯一的节点不重复的BST?

答案同理是可以的。正因为如此,下面的给出的5种解法,都对应一种思路类似的从后序遍历构造BST的解法。所以,对于Q449: 序列化与反序列化BST,我们可以将其序列化成前序或者后序遍历,再从对应的遍历构造出BST,这样通过前序或者后序遍历反序列化BST这类解法就一共有10种了。至于,从后序遍历构造BST的5种方法,我这里就不贴了,有兴趣的朋友可以自己写一下或者参考下我的githubQ1008_1BSTFromPostorder。

2.1 解题方法1: 先序遍历和中序遍历构造二叉树

2.1.1 解题思路

首先将先序遍历排序得到中序遍历,随后使用分治的方法从先序遍历和中序遍历构造出二叉搜索树,即前面的方法。

2.1.2 复杂度分析

时间复杂度: O(nlogn),排序。 空间复杂度: O(n),需要存储中序遍历结果。

2.1.3 Show the code

参考前面的代码,就不重复贴了。

2.2 解题方法2:二分查找插入点

2.2.1 解题思路

参考二分插入排序,思路大致如下: 考虑前n-1个点都构造好了,对于第n个点,我们根据BST树的性质二分找到对应的插入点,然后插入第n个点。

2.2.2 复杂度分析

时间复杂度:O(nlogn),插入过程耗时T

空间复杂度:O(1)

image

2.2.3 Show the code

    public TreeNode bstFromPreorder(int[] preorder) {
        TreeNode root = new TreeNode(preorder[0]);
        for (int i = 1; i < preorder.length; i++) {
            int val = preorder[i];
            TreeNode node = new TreeNode(val);
            putNode(root, node);
        }
        return root;
    }

    private void putNode(TreeNode root, TreeNode node) {
        TreeNode last = null;
        TreeNode iter = root;
        while (iter != null) {
            last = iter;
            if (iter.val > node.val) {
                iter = iter.left;
            } else {
                iter = iter.right;
            }
        }
        if (last.val > node.val) {
            last.left = node;
        } else {
            last.right = node;
        }
    }
复制代码

2.3 解题方法3:递归

2.3.1 解题思路

第一个元素为root节点,其后的节点比root大的属于root的右子树, 比root小的是属于其左子树,递归构造左右子树。遍历找到左右子树的分界位置。

2.3.2 复杂度分析

时间复杂度:O(n^2),考虑最坏情况,所有节点都在左子树,这种情况递归n次,每次内部迭代1+2+…n-1。

空间复杂度:O(n),递归n次,系统调用栈的深度为n。

2.3.3 Show the code

    public TreeNode bstFromPreorder2(int[] preorder) {
        return bstFromPreorder(preorder, 0, preorder.length - 1);
    }

    private TreeNode bstFromPreorder(int[] preorder, int start, int end) {
        if (start > end) {
            return null;
        }
        TreeNode root = new TreeNode(preorder[start]);
        int idx = start + 1;
        while (idx <= end && preorder[idx] < preorder[start]) {
            idx++;
        }
        root.left = bstFromPreorder(preorder, start + 1, idx - 1);
        root.right = bstFromPreorder(preorder, idx, end);
        return root;
    }
复制代码

2.4 解题方法4:(lower, upper) + 递归

这是LeetCode的官方解法,我花了一会才能理解,感觉有点难想啊。

2.4.1 解题思路

  1. 将 lower 和 upper 的初始值分别设置为负无穷和正无穷,因为根节点的值可以为任意值。
  2. 从先序遍历的第一个元素 idx = 0 开始构造二叉树,构造使用的函数名为 helper(lower, upper): 如果 idx = n,即先序遍历中的所有元素已经被添加到二叉树中,那么此时构造已经完成; 如果当前 idx 对应的先序遍历中的元素 val = preorder[idx] 的值不在 [lower, upper] 范围内,则进行回溯; 如果 idx 对应的先序遍历中的元素 val = preorder[idx] 的值在 [lower, upper] 范围内,则新建一个节点 root,并对其左孩子递归处理 helper(lower, val),对其右孩子递归处理 helper(val, upper)。

2.4.2 复杂度分析

时间复杂度:O(n),仅扫描前序遍历一次 空间复杂度:O(n),考虑最坏情况,所有节点都在左子树,这种情况递归n次,系统栈深度n

2.4.3 Show the code

    int idx = 0;
    int[] preorder;
    int n;

    public TreeNode helper(int lower, int upper) {
        // if all elements from preorder are used
        // then the tree is constructed
        if (idx == n) return null;

        int val = preorder[idx];
        // if the current element
        // couldn't be placed here to meet BST requirements
        if (val < lower || val > upper) return null;

        // place the current element
        // and recursively construct subtrees
        TreeNode root = new TreeNode(val);
        idx++;
        root.left = helper(lower, val);
        root.right = helper(val, upper);
        return root;
    }

    public TreeNode bstFromPreorder3(int[] preorder) {
        this.preorder = preorder;
        n = preorder.length;
        return helper(Integer.MIN_VALUE, Integer.MAX_VALUE);
    }
复制代码

2.5 解题方法5:迭代

这也是LeetCode的官方解法,我第一次解题的思路和这个类似,不过当时处理逻辑没想清楚。

2.5.1 解题思路

  1. 将先序遍历中的第一个元素作为二叉树的根节点,即 root = new TreeNode(preorder[0]),并将其放入栈中。
  2. 使用 for 循环迭代先序遍历中剩下的所有元素:
  • 将栈顶的元素作为父节点,当前先序遍历中的元素作为子节点。如果栈顶的元素值小于子节点的元素值,则将栈顶的元素弹出并作为新的父节点,直到栈空或栈顶的元素值大于子节点的元素值。注意,这里作为父节点的是最后一个被弹出栈的元素,而不是此时栈顶的元素;
  • 如果父节点的元素值小于子节点的元素值,则子节点为右孩子,否则为左孩子;
  • 将子节点放入栈中。

2.5.2 复杂度分析

时间复杂度:O(n),仅扫描前序遍历一次 空间复杂度:O(n),考虑最坏情况,所有节点都在左子树,队列长度为n

2.5.3 Show the code

    public TreeNode bstFromPreorder4(int[] preorder) {
        int n = preorder.length;
        if (n == 0) return null;

        TreeNode root = new TreeNode(preorder[0]);
        Deque deque = new ArrayDeque();
        deque.push(root);

        for (int i = 1; i < n; i++) {
            // take the last element of the deque as a parent
            // and create a child from the next preorder element
            TreeNode node = deque.peek();
            TreeNode child = new TreeNode(preorder[i]);
            // adjust the parent 
            while (!deque.isEmpty() && deque.peek().val < child.val)
                node = deque.pop();

            // follow BST logic to create a parent-child link
            if (node.val < child.val) node.right = child;
            else node.left = child;
            // add the child into deque
            deque.push(child);
        }
        return root;
    }
复制代码

3. 序列化与反序列化二叉树

这题目为:

LeetCode.297 二叉树的序列化与反序列化,困难难度

同样地,按之前我们给分类条件给这题目一个定性:它是一个含重复节点的二叉树构造算法题。这个题目明显比上述的题目都困难,因为它的条件最宽泛。

3.0 解题思考

问题:下面我们给的第一种解法就是通过带null节点的前序遍历还原二叉树,那么可以通过带null节点中序或者后序遍历来还原吗?

  1. 这里就我自己的思考,我认为带null节点的前序或者后序遍历是可以还原二叉树的,而中序遍历则不行(可能是我还没写出来解法)。
  2. 一个比较关键的点就是这2种都可以明晰的知道根节点,这样就能根据根节点递归还原,而中序遍历却无法确定根节点。
  3. 这里通过后序遍历还原的代码与前序比较类似,我就不贴了,有兴趣的朋友可以自己写一下或者参考下我的githubQ297SerializeAndDeserializeBinaryTree。

3.1 解题方法1:带null节点的前序遍历(DFS)

3.1.1 解题思路

  1. 树序列化的时候将叶子节点的左右null节点孩子也保存到先序遍历结果中。
  2. 反序列化的时候可以根据null节点的信息将还原二叉树。

3.1.2 复杂度分析

序列化: 时间复杂度:O(n),二叉树的前序遍历。 空间复杂度: O(n),递归需要系统栈和非递归需要手动构造的辅助栈。 反序列化: 时间复杂度:O(n),每一个节点处理一次。 空间复杂度: O(n),存储队列。

3.1.3 Show the code

    public String serialize(TreeNode root) {
        StringBuilder res = preOrderNonRecur(root, new StringBuilder());
        return res.toString();
    }

    /**
     * 前序遍历(DFS),根-左-右
     *     1
     *    / \
     *   2   3
     *      / \
     *     4   5
     * 1,2,null,null,3,4,null,null,5,null,null
     */
    StringBuilder preOrderRecur(TreeNode root, StringBuilder sb) {
        if (root == null) {
            sb.append("null,");
            return sb;
        } else {
            sb.append(root.val);
            sb.append(",");

            preOrderRecur(root.left, sb);
            preOrderRecur(root.right, sb);
        }
        return sb;
    }

    StringBuilder preOrderNonRecur(TreeNode root, StringBuilder sb) {
        Stack stack = new Stack<>();
        stack.add(root);

        while (!stack.isEmpty()) {
            TreeNode pop = stack.pop();
            sb.append(pop == null ? "null" : pop.val).append(",");
            if (pop == null) continue;
            stack.add(pop.right);
            stack.add(pop.left);
        }

        return sb;
    }

    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        // 将序列化的结果转为字符串数组
        String[] temp = data.split(",");
        // 字符串数组转为集合类便于操作
        LinkedList list = new LinkedList<>(Arrays.asList(temp));
        return preOrderDeser(list);
    }

    /**
     * 反前序遍历(DFS)的序列化
     */
    public TreeNode preOrderDeser(LinkedList list) {
        TreeNode root;
        if (list.peekFirst().equals("null")) {
            // 删除第一个元素 则第二个元素成为新的首部 便于递归
            list.pollFirst();
            return null;
        } else {
            root = new TreeNode(Integer.parseInt(list.peekFirst()));
            list.pollFirst();
            root.left = preOrderDeser(list);
            root.right = preOrderDeser(list);
        }
        return root;
    }
复制代码

3.2 解题方法2:带null节点的层次遍历(BFS)

3.2.1 解题思路

  1. 树序列化的时候将叶子节点的左右null节点孩子也保存到层次遍历结果中。
  2. 反序列化的时候可以根据null节点的信息将还原二叉树。

3.2.2 复杂度分析

序列化: 时间复杂度:O(n),二叉树的层次遍历。 空间复杂度: O(n),辅助队列。 反序列化: 时间复杂度:O(n),每一个节点处理一次。 空间复杂度: O(n),存储队列。

3.2.3 Show the code

    /**
     * 层次遍历(BFS)
     */
    public String serialize2(TreeNode root) {
        if (root == null) {
            return "";
        }

        StringBuilder sb = new StringBuilder();
        LinkedList queue = new LinkedList<>();
        queue.add(root);
        while (!queue.isEmpty()) {
            TreeNode pop = queue.removeFirst();
            sb.append(pop == null ? "null," : (pop.val + ","));
            if (pop != null) {
                queue.add(pop.left);
                queue.add(pop.right);
            }
        }
        return sb.toString();
    }

    /**
     * 反层次遍历(BFS)的序列化
     */
    public TreeNode deserialize2(String data) {
        if (data.isEmpty()) return null;
        String[] strs = data.split(",");
        Integer[] layerNode = new Integer[strs.length];

        for (int i = 0; i < strs.length; i++) {
            layerNode[i] = strs[i].equals("null") ? null : Integer.parseInt(strs[i]);
        }

        Queue queue = new ArrayDeque<>();
        TreeNode root = new TreeNode(layerNode[0]);
        queue.add(root);
        int cur = 1;
        while (!queue.isEmpty()) {
            TreeNode pop = queue.poll();
            if (layerNode[cur] != null) {
                pop.left = new TreeNode(layerNode[cur]);
                queue.add(pop.left);
            }
            cur++;
            if (layerNode[cur] != null) {
                pop.right = new TreeNode(layerNode[cur]);
                queue.add(pop.right);
            }
            cur++;
        }

        return root;
    }
复制代码

4. 总结

可以发现,序列化和反序列化二叉树作为条件最宽泛的方法是实用于其他条件更强的算法题的。如果也用这个方法去解Q449: 序列化与反序列化BST,我们一共有13种解法,是不是有点夸张~

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