树的构建与序列化

  • 要学会用 continuous subarray 的眼光去看三序遍历数组,子树结构 = 子序列结构

  • 对于任意指定子树,在 inorder / preorder / postorder 中都是长度一样的连续 subarray,只是位置不同。


前序遍历和中序遍历树构造二叉树

这题挺常见的,很考察对于树的理解。

             [1] 
           /     \ 
         [5]      [4]
        /   \       \ 
      [2]   [3]      [6] 
                    /
                  [7]
  • Pre-order: 【1 (5,2,3) (4,6,7)】
  • In-order: 【(2,5,3) 1 (4,7,6)】
  • Post-order: 【(2,3,5) (7,6,4) 1】

In-order 和 pre-order 单独都只能提供一部分树的信息,只依靠一个无法建立出完全一样的树,因为有歧义。

递归建树的过程是

  • 建当前 root;
  • 建 左/右 子树;
  • 建 右/左 子树;

因此根据 inorder 和 preorder 的性质,我们用 preorder 的顺序决定“先建哪个为 root”,用 inorder 的相对位置决定 “左右子树是谁”。

在递归过程中,这句判断终止条件的if语句很重要。

if (inStart > inEnd || preorderIndex >= preorder.length) return null;

总思想是divide & conquer,先找到root,再找左右子树。然后divide成左右两部分。

public class Solution {
    /**
     *@param preorder : A list of integers that preorder traversal of a tree
     *@param inorder : A list of integers that inorder traversal of a tree
     *@return : Root of a tree
     */
    public TreeNode buildTree(int[] preorder, int[] inorder) {
        // write your code here
        if (preorder == null || inorder == null || preorder.length == 0 || inorder.length == 0) return null;
        return build(preorder, inorder, 0, 0, inorder.length - 1);
    }
    private TreeNode build(int[] preorder, int[] inorder, int preorderIndex,
                            int inStart, int inEnd) {
        if (inStart > inEnd || preorderIndex >= preorder.length) return null;

        int rootVal = preorder[preorderIndex];
        int pos = inStart;
        for (int i = inStart; i <= inEnd; ++i) {
            if (inorder[i] == rootVal) {
                pos = i;
                break;
            }
        }
        int leftLength = pos - inStart;

        TreeNode root = new TreeNode(rootVal);
        root.left = build(preorder, inorder, preorderIndex + 1, inStart, pos - 1);
        root.right = build(preorder, inorder, preorderIndex + leftLength + 1, pos + 1, inEnd);

        return root;
    }
}

中序遍历和后序遍历树构造二叉树

和上一题差不多,还是一样的配方,postorder就像是preorder倒过来,只是左右反着,这么想的话,把postIndex从末尾遍历到开头就可以了。还是很容易的。

public class Solution {
    /**
     *@param inorder : A list of integers that inorder traversal of a tree
     *@param postorder : A list of integers that postorder traversal of a tree
     *@return : Root of a tree
     */
    public TreeNode buildTree(int[] inorder, int[] postorder) {
        // write your code here
        return build(inorder, postorder, postorder.length - 1, 0, inorder.length - 1);
    }
    private TreeNode build(int[] inorder, int[] postorder, int postIndex,
                            int inStart, int inEnd) {
        if (inStart > inEnd || postIndex < 0) return null; 

        int rootVal = postorder[postIndex];
        int pos = inStart;
        for (int i = inStart; i <= inEnd; ++i) {
            if (rootVal == inorder[i]) {
                pos = i;
                break;
            }
        }
        int rightLength = inEnd - pos;

        TreeNode root = new TreeNode(rootVal);
        root.left = build(inorder, postorder, postIndex - rightLength - 1, inStart, pos - 1);
        root.right = build(inorder, postorder, postIndex - 1, pos + 1, inEnd);

        return root;
    }
}

297. Serialize and Deserialize Binary Tree

这也是个很有意思的问题,核心问题类似于 encode / decode strings,在于 “如何确定唯一的 binary tree ?” 对于 List of String 来讲,只需要正确做好单词的分隔还有分隔符的消除歧义;而 Tree 上可以有 level 的分隔,left / right subtree 的分隔。

相对来讲Tree的优势是,这个 TreeNode 里面只存数字,所以有很多额外的字母和符号可以利用,不用担心所用字符的歧义问题(不然就得定义 escape 符了)

最开始的想法是拿前序遍历和中序遍历去构建,就像前面的题一样,序列化就序列化成两个数组,但没有AC,不知道哪错了。

public class Codec {

    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        List<Integer> pre = preorder(root);
        List<Integer> in = inorder(root);

        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < pre.size(); ++i) {
            sb.append(pre.get(i));
        }
        for (int i = 0; i < in.size(); ++i) {
            sb.append(in.get(i));
        }
        return sb.toString();
    }

    private List<Integer> preorder(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        if (root == null) return result;

        result.add(root.val);
        result.addAll(preorder(root.left));
        result.addAll(preorder(root.right));

        return result;
    }

    private List<Integer> inorder(TreeNode root) {
        List<Integer> result = new ArrayList<>();
        if (root == null) return result;

        result.addAll(preorder(root.left));
        result.add(root.val);
        result.addAll(preorder(root.right));

        return result;
    }

    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        int len = data.length();
        int[] preorder = new int[len / 2];
        int[] inorder = new int[len / 2];
        for (int i = 0; i < len; ++i) {
            if (i < len / 2) {
                preorder[i] = data.charAt(i) - '0';
            } else {
                inorder[i - len / 2] = (data.charAt(i)) - '0';
            }
        }

        if (preorder == null || inorder == null || preorder.length == 0 || inorder.length == 0) return null;

        return build(preorder, inorder, 0, 0, inorder.length - 1);
    }
    private TreeNode build(int[] preorder, int[] inorder, int preIndex, int start, int end) {
        if (preIndex > preorder.length - 1 || start > end) return null;

        int leftlen = 0;
        int val = preorder[preIndex];
        for (int i = start; i < end; ++i) {
            if (inorder[i] == val) break;
            leftlen++;
        }
        TreeNode root = new TreeNode(val);
        TreeNode left = build(preorder, inorder, preIndex + 1, start, start + leftlen - 1);
        TreeNode right = build(preorder, inorder, preIndex + leftlen + 1, start + leftlen + 1, end);
        root.left = left;
        root.right = right;
        return root;
    }
}

解决这题的关键就是,自己补位,构建一个 full binary tree 出来。

自己第一遍 AC 的代码。思想就是做 preorder ,不过把左右子树分别用括号括起来。空节点会生成空括号。这样对于一棵子树的 substring,第一个 matching 括号就代表左子树,后面的就是右子树。

这种写法的好处一是和这章之前的思想有联系和继承,第二是不需要用逗号做分隔(但是每个叶节点会生成一对空括号)

递归的时候注意抹去括号的 index 细节,并且在一开始检查下是不是 "()".

另外一个经验是,处理字符串的时候尽量直接用内置函数,比如 str.indexOf('x') 这种,知道题的重点不是字符串就别每次都手写了。

比较简洁正规的 pre-order DFS 递归写法:

public class Codec {

    // Encodes a tree to a single string.
    public String serialize(TreeNode root) {
        StringBuilder sb = new StringBuilder();
        dfsSerialize(sb, root);
        return sb.toString().trim();
    }

    private void dfsSerialize(StringBuilder sb, TreeNode root) {
        if (root == null) {
            sb.append("#").append(" ");
            return;
        }
        sb.append(root.val).append(" ");
        dfsSerialize(sb, root.left);
        dfsSerialize(sb, root.right);
    }

    // Decodes your encoded data to tree.
    public TreeNode deserialize(String data) {
        if (data == null || data.length() == 0) return null;
        String[] strs = data.split(" ");
        int[] index = new int[1];
        return dfsDeserialize(strs, index);
    }

    private TreeNode dfsDeserialize(String[] strs, int[] index) {
        if (strs[index[0]].equals("#")) {
            index[0]++;
            return null;
        }
        TreeNode root = new TreeNode(Integer.parseInt(strs[index[0]]));
        index[0]++;
        root.left = dfsDeserialize(strs, index);
        root.right = dfsDeserialize(strs, index);
        return root;
    }
}

449. Serialize and Deserialize BST

多练一练,尤其是反序列化有点绕,用一个boolean值来确定是左子树还是右子树,真假值反复交替,这样把值赋到树上面。用index和list来确定是哪一个节点的孩子。序列化和反序列化有自己的性质,利用这样的性质我们才能用一种比较简洁的做法做出来,但性质也是人为规定的。

class Solution {
    /**
     * This method will be invoked first, you should design your own algorithm 
     * to serialize a binary tree which denote by a root node to a string which
     * can be easily deserialized by your own "deserialize" method later.
     */
    public String serialize(TreeNode root) {
        // write your code here
        if (root == null) return "{}";

        ArrayList<TreeNode> list = new ArrayList<>();
        list.add(root);
        for (int i = 0; i < list.size(); ++i) {
            TreeNode node = list.get(i);
            if (node == null) continue;
            list.add(node.left);
            list.add(node.right);
        }

        while (list.get(list.size() - 1) == null) {
            list.remove(list.size() - 1);
        }

        StringBuilder sb = new StringBuilder();
        sb.append("{");
        sb.append(list.get(0).val);
        for (int i = 1; i < list.size(); ++i) {
            if (list.get(i) == null) {
                sb.append(",#");
            } else {
                sb.append(",");
                sb.append(list.get(i).val);
            }
        }
        sb.append("}");
        return sb.toString();
    }

    /**
     * This method will be invoked second, the argument data is what exactly
     * you serialized at method "serialize", that means the data is not given by
     * system, it's given by your own serialize method. So the format of data is
     * designed by yourself, and deserialize it here as you serialize it in 
     * "serialize" method.
     */
    public TreeNode deserialize(String data) {
        // write your code here
        if (data.equals("{}")) return null;
        String[] set = data.substring(1, data.length() - 1).split(",");
        ArrayList<TreeNode> list = new ArrayList<TreeNode>();
        TreeNode root = new TreeNode(Integer.parseInt(set[0]));
        list.add(root);
        int index = 0;
        boolean isLeftChild = true;
        for (int i = 1; i < set.length; ++i) {
            if (!set[i].equals("#")) {
                TreeNode node = new TreeNode(Integer.parseInt(set[i]));
                if (isLeftChild) {
                    list.get(index).left = node;
                } else {
                    list.get(index).right = node;
                }
                list.add(node);
            }
            if (!isLeftChild) {
                index++;
            }
            isLeftChild = !isLeftChild;
        }
        return root;
    }
}

特别注意:

下面要用到的DFS方法,其serialize的规则和前文不同,这种不同的规则也给了DFS迭代方法以可能性。


在 Binary Tree 的各种遍历中,BFS 都是比较耗费空间的一种,所以一个显然的优化与 follow up 就是,能不能用 DFS ,迭代做。

值得注意的是,DFS的preorder和inorder做法都是可以的,前提是我们的对象是full binary tree,每个节点有0/2个子节点。

这种做法其实就是一种对 pre-order DFS 做法的 Stack 模拟。也是 Tree 类问题递归转迭代的常用手段。

  • 关键点1: 要存一个 boolean 记录下当前要填的点是不是左节点;

  • 关键点2:这个 boolean 的变化要看当前 boolean 值以及新填上的点是不是叶节点;

  • 关键点3:对于填充右节点的情况,链接完了就直接 pop(),有别于填充左子树时候用的 peek(). 因为 preorder 右子树结束之后 stack frame 就出栈了。


public String serialize(TreeNode root) {
        StringBuilder sb = new StringBuilder();
        dfsSerialize(root, sb);
        return sb.toString().trim();
    }

private void dfsSerialize(TreeNode root, StringBuilder sb){
        if(root == null){
            sb.append("#").append(",");
            return;
        }

        sb.append(root.val).append(",");
        dfsSerialize(root.left, sb);
        dfsSerialize(root.right, sb);
}

 public String serialize(TreeNode root) {
        Stack<TreeNode> stack = new Stack<>();
        StringBuilder sb = new StringBuilder();
        stack.push(root);

        while(!stack.isEmpty()){
            TreeNode node = stack.pop();
            if(node == null){
                sb.append("#").append(",");
                continue;
            }

            sb.append(node.val).append(",");
            stack.push(node.right);
            stack.push(node.left);
        }
        return sb.toString().trim();
}

对于deserialize而言:

public TreeNode deserialize(String data) {
        String[] strs = data.split(" ");
        int[] index = new int[1];

        return dfsDeserialize(strs, index);
    }

    private TreeNode dfsDeserialize(String[] strs, int[] index){
        if(strs[index[0]].equals("#")){
            index[0]++;
            return null;
        }

        TreeNode root = new TreeNode(Integer.parseInt(strs[index[0]]));
        index[0]++;
        root.left = dfsDeserialize(strs, index);
        root.right = dfsDeserialize(strs, index);

        return root;
    }

public TreeNode deserialize(String data) {
        Stack<TreeNode> stack = new Stack<>();
        String[] strs = data.split(" ");
        if(strs[0].equals("#")) return null;
        TreeNode root = new TreeNode(Integer.parseInt(strs[0]));
        stack.push(root);
        int index = 1;
        boolean doLeft = true;
        while(index < strs.length){
            String str = strs[index++];
            TreeNode node = (str.equals("#")) 
                            ? null
                            : new TreeNode(Integer.parseInt(str));
            if(doLeft){
                stack.peek().left = node;
                if(node == null) doLeft = false;
            } else {
                stack.pop().right = node;
                if(node != null) doLeft = true;
            }

            if(node != null) stack.push(node);
        }
        return root;
    }

536. Construct Binary Tree from String

606. Construct String from Binary Tree

class Solution {
    public String tree2str(TreeNode t) {
        if (t == null) return "";

        String result = t.val + "";

        String left = tree2str(t.left);
        String right = tree2str(t.right);

        if (left == "" && right == "") return result;
        if (left == "") return result + "()" + "(" + right + ")";
        if (right == "") return result + "(" + left + ")";
        return result + "(" + left  + ")" + "(" + right + ")";
    }
}

results matching ""

    No results matching ""