How to Serialize Binary Tree

41,796

Solution 1

All those articles talk mostly about the serialization part. The deserialization part is slightly tricky to do in one pass.

I have implemented an efficient solution for deserialization too.

Problem: Serialize and Deserialize a binary tree containing positive numbers.

Serialization part:

  1. Use 0 to represent null.
  2. Serialize to list of integers using preorder traversal.

Deserialization part:

  1. Takes in list of integers and uses recursive helper method for deserialization.
  2. Recursive deserializer returns a pair (BTNode node, int nextIndexToRead) where node is tree node constructed so far, and nextIndexToRead is position of next number to be read in the serialized list of numbers.

Below is the code in Java:

public final class BinaryTreeSerializer
{
    public static List<Integer> Serialize(BTNode root)
    {
        List<Integer> serializedNums = new ArrayList<Integer>();

        SerializeRecursively(root, serializedNums);

        return serializedNums;
    }

    private static void SerializeRecursively(BTNode node, List<Integer> nums)
    {
        if (node == null)
        {
            nums.add(0);
            return;
        }

        nums.add(node.data);
        SerializeRecursively(node.left, nums);
        SerializeRecursively(node.right, nums);
    }

    public static BTNode Deserialize(List<Integer> serializedNums)
    {
        Pair pair = DeserializeRecursively(serializedNums, 0);

        return pair.node;
    }

    private static Pair DeserializeRecursively(List<Integer> serializedNums, int start)
    {        
        int num = serializedNums.get(start);

        if (num == 0)
        {
            return new Pair(null, start + 1);
        }

        BTNode node = new BTNode(num);

        Pair p1 = DeserializeRecursively(serializedNums, start + 1);
        node.left = p1.node;

        Pair p2 = DeserializeRecursively(serializedNums, p1.startIndex);
        node.right = p2.node;

        return new Pair(node, p2.startIndex);
    }

    private static final class Pair
    {
        BTNode node;
        int startIndex;

        private Pair(BTNode node, int index)
        {
            this.node = node;
            this.startIndex = index;
        }
    }
}

public class BTNode 
{
    public int data;
    public BTNode left;
    public BTNode right;

    public BTNode(int data)
    {
        this.data = data;
    }
}

Solution 2

Using pre order traversal, serialize Binary tree. Use the same pre order traversal to deserialize tree. Be careful about the edge cases. Here null nodes are represented by "#"

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

    private static void serialize(TreeNode node, StringBuilder sb){
        if (node == null) {
            sb.append("# ");
        } else {
            sb.append(node.val + " ");
            serialize(node.left, sb);
            serialize(node.right, sb);
        }
    }

    public static TreeNode deserialize(String s){
        if (s == null || s.length() == 0) return null;
        StringTokenizer st = new StringTokenizer(s, " ");
        return deserialize(st);
    }

    private static TreeNode deserialize(StringTokenizer st){
        if (!st.hasMoreTokens())
            return null;
        String val = st.nextToken();
        if (val.equals("#"))
            return null;
        TreeNode root = new TreeNode(Integer.parseInt(val));
        root.left = deserialize(st);
        root.right = deserialize(st);
        return root;
    } 

Solution 3

Approach 1: Do both Inorder and Preorder traversal to searialize the tree data. On de-serialization use Pre-order and do BST on Inorder to properly form the tree.

You need both because A -> B -> C can be represented as pre-order even though the structure can be different.

Approach 2: Use # as a sentinel whereever the left or right child is null.....

Solution 4

I have been trying to get the gist of it. So here is my Java implementation. As mentioned, this is a binary tree not a BST. For serializing, a preorder traversal seems to be working easier (to a string with "NULL" for null nodes). Please check the code below with a complete example of recursion calls. For deserializing, the string is converted to a LinkedList where remove(0) gets the top element in an O(1) running time. Please also see a complete example in the comments of the code for deserializing. Hope that will help someone struggle less than I did :) The overall running time for each method (serialize and deserialize) is the same running time for binary tree traversal, i.e., O(n) where n is the number of nodes (entries) in the tree

import java.util.LinkedList;
import java.util.List;

public class SerDesBinTree<T> {

    public static class TreeEntry<T>{
        T element;
        TreeEntry<T> left;
        TreeEntry<T> right;
        public TreeEntry(T x){
            element = x;
            left = null;
            right = null;
        }
    }

    TreeEntry<T> root;
    int size;
    StringBuilder serSB = new StringBuilder();
    List<String> desList = new LinkedList<>();

    public SerDesBinTree(){
        root = null;
        size = 0;   
    }

    public void traverseInOrder(){
        traverseInOrder(this.root);
    }

    public void traverseInOrder(TreeEntry<T> node){
        if (node != null){
            traverseInOrder(node.left);
            System.out.println(node.element);
            traverseInOrder(node.right);
        }
    }

    public void serialize(){
        serialize(this.root);
    }


    /*
     *          1
     *         / \
     *        2   3
     *           /
     *          4 
     *        
     *        ser(1)                              
     *            serSB.append(1)                     serSB: 1
     *            ser(1.left)
     *            ser(1.right)
     *            |
     *            |
     *            ser(1.left=2)
     *                serSB.append(2)                 serSB: 1, 2
     *                ser(2.left)
     *                ser(2.right)
     *                |
     *                |
     *                ser(2.left=null)
     *                    serSB.append(NULL)          serSB: 1, 2, NULL
     *                    return
     *                |    
     *                ser(2.right=null)
     *                    serSB.append(NULL)          serSB: 1, 2, NULL, NULL
     *                    return
     *                    
     *             |
     *             ser(1.right=3)
     *                serSB.append(3)                 serSB: 1, 2, NULL, NULL, 3
     *                ser(3.left)
     *                ser(3.right)
     *                
     *                |
     *                ser(3.left=4)
     *                    serSB.append(4)             serSB: 1, 2, NULL, NULL, 3, 4
     *                    ser(4.left)
     *                    ser(4.right)
     *                    
     *                    |
     *                    ser(4.left=null)
     *                        serSB.append(NULL)      serSB: 1, 2, NULL, NULL, 3, 4, NULL
     *                        return
     *                        
     *                    ser(4.right=null)
     *                        serSB.append(NULL)      serSB: 1, 2, NULL, NULL, 3, 4, NULL, NULL
     *                        return
     *                        
     *                ser(3.right=null)
     *                    serSB.append(NULL)          serSB: 1, 2, NULL, NULL, 3, 4, NULL, NULL, NULL
     *                    return
     *        
     */
    public void serialize(TreeEntry<T> node){
        // preorder traversal to build the string
        // in addition: NULL will be added (to make deserialize easy)
        // using StringBuilder to append O(1) as opposed to
        // String which is immutable O(n)
        if (node == null){
            serSB.append("NULL,");
            return;
        }

        serSB.append(node.element + ",");
        serialize(node.left);
        serialize(node.right);
    }

    public TreeEntry<T> deserialize(TreeEntry<T> newRoot){
        // convert the StringBuilder into a list
        // so we can do list.remove() for the first element in O(1) time

        String[] desArr = serSB.toString().split(",");

        for (String s : desArr){
            desList.add(s);
        }


        return deserialize(newRoot, desList);
    }


    /*
     *          1
     *         / \
     *        2   3
     *           /
     *          4 
     * 
     *        deser(root, list)                              list: 1, 2, NULL, NULL, 3, 4, NULL, NULL, NULL
     *            root = new TreeEntry(1)                    list: 2, NULL, NULL, 3, 4, NULL, NULL, NULL
     *            root.left = deser(root.left, list)  // **
     *            root.right = deser(root.right, list) // *-*
     *            return root // ^*^
     *            
     *            
     *      so far subtree
     *          1
     *         / \
     *      null  null
     *            
     *            deser(root.left, list)
     *                 root.left = new TreeEntry(2)          list: NULL, NULL, 3, 4, NULL, NULL, NULL
     *                 root.left.left = deser(root.left.left, list) // ***
     *                 root.left.right = deser(root.left.right, list)  // ****
     *                 return root.left // eventually return new TreeEntry(2) to ** above after the two calls are done
     *                 
     *           so far subtree
     *                 2
     *                / \
     *            null   null 
     *                 
     *                 deser(root.left.left, list)      
     *                     // won't go further down as the next in list is NULL
     *                      return null    // to ***                    list: NULL, 3, 4, NULL, NULL, NULL
     *                      
     *           so far subtree (same, just replacing null)
     *                 2
     *                / \
     *            null   null 
     *            
     *                 deser(root.left.right, list)
     *                     // won't go further down as the next in list is NULL
     *                      return null    // to ****                 list: 3, 4, NULL, NULL, NULL
     *                      
     *           so far subtree (same, just replacing null)
     *                 2
     *                / \
     *            null   null 
     *            
     *      
     *      so far subtree // as node 2 completely returns to ** above
     *          1
     *         / \
     *        2  null
     *       / \
     *   null   null
     *      
     *      
     *            deser(root.right, list)
     *                 root.right = new TreeEntry(3)                list: 4, NULL, NULL, NULL
     *                 root.right.left = deser(root.right.left, list) // *&*
     *                 root.right.right = deser(root.right.right, list)  // *---*
     *                 return root.right // eventually return to *-* above after the previous two calls are done
     *                 
     *           so far subtree
     *                 3
     *                / \
     *            null   null 
     *            
     *            
     *                 deser(root.right.left, list)
     *                      root.right.left = new TreeEntry(4)       list: NULL, NULL, NULL
     *                      root.right.left.left = deser(root.right.left.left, list) // *(*
     *                      root.right.left.right = deser(root.right.left.right, list) // *)*
     *                      return root.right.left // to *&*
     *                      
     *                  so far subtree
     *                       4
     *                      / \
     *                  null   null 
     *                    
     *                       deser(root.right.left.left, list)
     *                             // won't go further down as the next in list is NULL
     *                             return null // to *(*         list: NULL, NULL
     *                             
     *                  so far subtree (same, just replacing null)
     *                       4
     *                      / \
     *                  null   null 
     *                  
     *                       deser(root.right.left.right, list)
     *                             // won't go further down as the next in list is NULL
     *                             return null // to *)*         list: NULL
     *                             
     *                             
     *                  so far subtree (same, just replacing null)
     *                       4
     *                      / \
     *                  null   null 
     *                  
     *                  
     *           so far subtree
     *                 3
     *                / \
     *               4   null   
     *              / \
     *           null  null
     *                
     *                
     *                deser(root.right.right, list)
     *                        // won't go further down as the next in list is NULL
     *                       return null // to *---*    list: empty
     *                       
     *           so far subtree (same, just replacing null of the 3 right)
     *                 3
     *                / \
     *               4   null   
     *              / \
     *           null  null   
     *           
     *           
     *           now returning the subtree rooted at 3 to root.right in *-*
     *           
     *          1
     *         / \
     *        /   \
     *       /     \
     *      2       3
     *     / \     / \
     * null  null /   null
     *           /
     *          4
     *         / \
     *      null  null 
     *      
     *      
     *      finally, return root (the tree rooted at 1) // see ^*^ above
     *    
     */
    public TreeEntry<T> deserialize(TreeEntry<T> node, List<String> desList){

        if (desList.size() == 0){
            return null;
        }

        String s = desList.remove(0); // efficient operation O(1)
        if (s.equals("NULL")){
            return null;
        }

        Integer sInt = Integer.parseInt(s);
        node = new TreeEntry<T>((T)sInt);

        node.left = deserialize(node.left, desList);
        node.right = deserialize(node.right, desList);

        return node;
    }


    public static void main(String[] args) {
        /*
         *          1
         *         / \
         *        2   3
         *           /
         *          4 
         *        
         */
        SerDesBinTree<Integer> tree = new SerDesBinTree<>();
        tree.root = new TreeEntry<Integer>(1);
        tree.root.left = new TreeEntry<Integer>(2);
        tree.root.right = new TreeEntry<Integer>(3);
        tree.root.right.left = new TreeEntry<Integer>(4);
        //tree.traverseInOrder();

        tree.serialize();
        //System.out.println(tree.serSB);

        tree.root = null;
        //tree.traverseInOrder();

        tree.root = tree.deserialize(tree.root);
        //tree.traverseInOrder();

        // deserialize into a new tree
        SerDesBinTree<Integer> newTree = new SerDesBinTree<>();
        newTree.root = tree.deserialize(newTree.root);
        newTree.traverseInOrder();


    }
}
Share:
41,796
worker1138
Author by

worker1138

Updated on November 16, 2021

Comments

  • worker1138
    worker1138 over 2 years

    I went to an interview today where I was asked to serialize a binary tree. I implemented an array-based approach where the children of node i (numbering in level-order traversal) were at the 2*i index for the left child and 2*i + 1 for the right child. The interviewer seemed more or less pleased, but I'm wondering what serialize means exactly? Does it specifically pertain to flattening the tree for writing to disk, or would serializing a tree also include just turning the tree into a linked list, say. Also, how would we go about flattening the tree into a (doubly) linked list, and then reconstructing it? Can you recreate the exact structure of the tree from the linked list?