Serialize and Deserialize Binary Tree
HardPrompt
Serialization is the process of converting a data structure or object into a sequence of bits so that it can be stored in a file or memory buffer, or transmitted across a network connection link to be reconstructed later in the same or another computer environment.
Design an algorithm to serialize and deserialize a binary tree. There is no restriction on how your serialization/deserialization algorithm should work. You just need to ensure that a binary tree can be serialized to a string and this string can be deserialized to the original tree structure.
Clarification: The input/output format is the same as how LeetCode serializes a binary tree. You do not necessarily need to follow this format, so please be creative and come up with different approaches yourself.
Example 1:
Input: root = [1,2,3,null,null,4,5]
Output: [1,2,3,null,null,4,5]Example 2:
Input: root = []
Output: []
Constraints:
- The number of nodes in the tree is in the range
[0, 104]. -1000 <= Node.val <= 1000
Approaches
2 approaches with complexity analysis and trade-offs.
This approach uses a Breadth-First Search (BFS), also known as a level-order traversal, to serialize the tree. We traverse the tree level by level, from left to right, and record the value of each node. A special marker (e.g., "N" or "#") is used for null nodes to ensure the tree's structure can be perfectly reconstructed.
Algorithm
-
Serialization (BFS)
- If the
rootisnull, return an empty string or a specific marker for an empty tree. - Initialize a queue (
java.util.Queue) and add therootnode to it. - Initialize a
StringBuilderto construct the serialized string. - Loop while the queue is not empty:
a. Dequeue a node
curr. b. Ifcurrisnull, append a null marker (e.g., "N") and a delimiter to theStringBuilder. c. Ifcurris notnull, append its value and a delimiter. Then, enqueue its left and right children. - Return the final string from the
StringBuilder.
- If the
-
Deserialization (BFS)
- If the input string is empty or represents a null tree, return
null. - Split the input string by the delimiter to get an array of value strings.
- Create the
rootnode from the first value in the array. - Initialize a queue and add the
rootto it. This queue will hold parent nodes awaiting their children. - Iterate through the rest of the value strings, starting from the second element (
i = 1). - In each iteration, dequeue a
parentnode. - Process the next value string (
nodes[i]) for the left child. If it's not the null marker, create a new node, attach it asparent.left, and enqueue the new node. - Process the subsequent value string (
nodes[i+1]) for the right child in the same manner. - Continue until all values are processed and the tree is reconstructed.
- Return the
root.
- If the input string is empty or represents a null tree, return
Walkthrough
Serialization
The serialization process involves a level-order traversal using a queue. We start with the root. As we visit each node, we append its value to a string. If a node is null, we append a special marker. Crucially, for any non-null node we visit, we add both its left and right children to the queue, even if they are null. This ensures that the structure of the tree is preserved. The process continues until the queue is empty.
public class Codec { private static final String NULL_SYMBOL = "N"; private static final String DELIMITER = ","; // Encodes a tree to a single string. public String serialize(TreeNode root) { if (root == null) { return ""; } StringBuilder sb = new StringBuilder(); Queue<TreeNode> queue = new LinkedList<>(); queue.offer(root); while (!queue.isEmpty()) { TreeNode node = queue.poll(); if (node == null) { sb.append(NULL_SYMBOL).append(DELIMITER); continue; } sb.append(node.val).append(DELIMITER); queue.offer(node.left); queue.offer(node.right); } return sb.toString(); }Deserialization
To deserialize, we first split the string by our delimiter to get an array of values. The first value corresponds to the root. We create the root node and add it to a queue. This queue will keep track of the parent nodes whose children we need to attach. We then iterate through the rest of the values in pairs. For each parent node we dequeue, the next two values in the array represent its left and right children. If a value is not our null marker, we create a new node, attach it to the parent, and add the new node to the queue to have its own children attached later. This reconstructs the tree in the same level-order it was serialized.
// Decodes your encoded data to tree. public TreeNode deserialize(String data) { if (data == null || data.isEmpty()) { return null; } String[] nodes = data.split(DELIMITER); TreeNode root = new TreeNode(Integer.parseInt(nodes[0])); Queue<TreeNode> queue = new LinkedList<>(); queue.offer(root); int i = 1; while (!queue.isEmpty() && i < nodes.length) { TreeNode parent = queue.poll(); // Left child if (!nodes[i].equals(NULL_SYMBOL)) { TreeNode left = new TreeNode(Integer.parseInt(nodes[i])); parent.left = left; queue.offer(left); } i++; // Right child if (i < nodes.length && !nodes[i].equals(NULL_SYMBOL)) { TreeNode right = new TreeNode(Integer.parseInt(nodes[i])); parent.right = right; queue.offer(right); } i++; } return root; }}Complexity
Time
O(N) - Both serialization and deserialization processes visit each node exactly once, where N is the total number of nodes in the tree.
Space
O(N) - In the worst-case scenario (a complete binary tree), the maximum number of nodes in the queue at any time is proportional to the number of nodes in the tree (W ≈ N/2), leading to O(N) space. The string itself also takes O(N) space.
Trade-offs
Pros
The logic is straightforward and follows the standard iterative BFS pattern.
It avoids deep recursion, which prevents potential
StackOverflowErrorfor very deep trees.
Cons
Can be less space-efficient than a DFS approach for balanced trees, as the queue's size is proportional to the tree's maximum width (O(N) in the worst case).
The serialized string might contain many trailing null markers for the last level, which can make the string longer than necessary (though this can be optimized).
This approach utilizes a Depth-First Search (DFS), specifically a pre-order traversal (Root-Left-Right), for both serialization and deserialization. The pre-order sequence is ideal for reconstruction because the root of any subtree is always processed before its children, making it easy to rebuild the tree recursively.
Algorithm
-
Serialization (Pre-order DFS)
- Define a recursive helper function,
buildString(node, stringBuilder). - Base Case: If the current
nodeisnull, append a null marker (e.g., "#") and a delimiter to thestringBuilder, then return. - Recursive Step:
a. Append the current
node.valand a delimiter. b. Recursively callbuildStringfor the left child:buildString(node.left, stringBuilder). c. Recursively callbuildStringfor the right child:buildString(node.right, stringBuilder). - The main
serializefunction initializes theStringBuilderand starts the recursion from theroot.
- Define a recursive helper function,
-
Deserialization (Pre-order DFS)
- Split the serialized string by the delimiter into a queue of values.
- Define a recursive helper function,
buildTree(nodesQueue). - Base Case: Dequeue a value. If it's the null marker, return
null. - Recursive Step:
a. Create a new
TreeNodewith the dequeued value. b. Reconstruct the left subtree by recursively callingnode.left = buildTree(nodesQueue). c. Reconstruct the right subtree by recursively callingnode.right = buildTree(nodesQueue). d. Return the creatednode. - The main
deserializefunction initializes the queue and starts the recursion.
Walkthrough
Serialization
Serialization is performed using a recursive pre-order traversal. The helper function traverses the tree and builds a string. When it encounters a non-null node, it appends the node's value followed by a delimiter. When it encounters a null child pointer, it appends a special null marker (e.g., "#") and a delimiter. This marking of nulls is essential to distinguish between different tree structures that might otherwise have the same pre-order traversal of values.
public class Codec { private static final String NULL_SYMBOL = "#"; private static final String DELIMITER = ","; // Encodes a tree to a single string. public String serialize(TreeNode root) { StringBuilder sb = new StringBuilder(); buildString(root, sb); return sb.toString(); } private void buildString(TreeNode node, StringBuilder sb) { if (node == null) { sb.append(NULL_SYMBOL).append(DELIMITER); } else { sb.append(node.val).append(DELIMITER); buildString(node.left, sb); buildString(node.right, sb); } }Deserialization
For deserialization, we first split the string into a list or queue of values. We then use a recursive helper function that consumes values from this queue to build the tree. The function reads the next value: if it's the null marker, it returns null. Otherwise, it creates a new node with the value. Then, it makes a recursive call to build the left subtree and another to build the right subtree. Because the data is in pre-order, the recursive calls naturally consume the correct values from the queue to form the left and right subtrees in the correct order.
// Decodes your encoded data to tree. public TreeNode deserialize(String data) { if (data == null || data.isEmpty()) { return null; } Queue<String> nodes = new LinkedList<>(Arrays.asList(data.split(DELIMITER))); return buildTree(nodes); } private TreeNode buildTree(Queue<String> nodes) { String val = nodes.poll(); if (val.equals(NULL_SYMBOL)) { return null; } else { TreeNode node = new TreeNode(Integer.parseInt(val)); node.left = buildTree(nodes); node.right = buildTree(nodes); return node; } }}Complexity
Time
O(N) - Both serialization and deserialization require visiting each node and null marker once. Therefore, the time complexity is linear with respect to the number of nodes.
Space
O(N) - The space is dominated by the recursion call stack and the storage for the serialized string. The recursion depth is equal to the height of the tree, H. In the worst case of a skewed tree, H can be N, leading to O(N) space. For a balanced tree, it's O(log N). The string itself also requires O(N) space.
Trade-offs
Pros
Elegant and concise recursive implementation that naturally maps to the tree structure.
Generally more space-efficient for average and balanced trees, as the space complexity of the call stack is O(H), where H is the tree height (O(log N) for a balanced tree).
Cons
For very deep and skewed trees, the recursion depth can be large, potentially leading to a
StackOverflowError.The recursive implementation might be slightly less intuitive for beginners compared to the iterative BFS approach.
Solutions
Solution
/** * Definition for a binary tree node. * public class TreeNode { * public int val; * public TreeNode left; * public TreeNode right; * public TreeNode(int x) { val = x; } * } */ public class Codec { // Encodes a tree to a single string. public string serialize ( TreeNode root ) { if ( root == null ) { return null ; } List < string > ans = new List < string >(); Queue < TreeNode > q = new Queue < TreeNode >(); q . Enqueue ( root ); while ( q . Count > 0 ) { TreeNode node = q . Dequeue (); if ( node != null ) { ans . Add ( node . val . ToString ()); q . Enqueue ( node . left ); q . Enqueue ( node . right ); } else { ans . Add ( "#" ); } } return string . Join ( "," , ans ); } // Decodes your encoded data to tree. public TreeNode deserialize ( string data ) { if ( data == null ) { return null ; } string [] vals = data . Split ( ',' ); int i = 0 ; TreeNode root = new TreeNode ( int . Parse ( vals [ i ++])); Queue < TreeNode > q = new Queue < TreeNode >(); q . Enqueue ( root ); while ( q . Count > 0 ) { TreeNode node = q . Dequeue (); if ( vals [ i ] != "#" ) { node . left = new TreeNode ( int . Parse ( vals [ i ])); q . Enqueue ( node . left ); } i ++; if ( vals [ i ] != "#" ) { node . right = new TreeNode ( int . Parse ( vals [ i ])); q . Enqueue ( node . right ); } i ++; } return root ; } } // Your Codec object will be instantiated and called as such: // Codec codec = new Codec(); // codec.deserialize(codec.serialize(root));/** * Definition for a binary tree node. * public class TreeNode { * int val; * TreeNode left; * TreeNode right; * TreeNode(int x) { val = x; } * } */ public class Codec { private static final String NULL = "#" ; private static final String SEP = "," ; // Encodes a tree to a single string. public String serialize ( TreeNode root ) { if ( root == null ) { return "" ; } StringBuilder sb = new StringBuilder (); preorder ( root , sb ); return sb . toString (); } private void preorder ( TreeNode root , StringBuilder sb ) { if ( root == null ) { sb . append ( NULL + SEP ); return ; } sb . append ( root . val + SEP ); preorder ( root . left , sb ); preorder ( root . right , sb ); } // Decodes your encoded data to tree. public TreeNode deserialize ( String data ) { if ( data == null || "" . equals ( data )) { return null ; } List < String > vals = new LinkedList <>(); for ( String x : data . split ( SEP )) { vals . add ( x ); } return deserialize ( vals ); } private TreeNode deserialize ( List < String > vals ) { String first = vals . remove ( 0 ); if ( NULL . equals ( first )) { return null ; } TreeNode root = new TreeNode ( Integer . parseInt ( first )); root . left = deserialize ( vals ); root . right = deserialize ( vals ); return root ; } } // Your Codec object will be instantiated and called as such: // Codec ser = new Codec(); // Codec deser = new Codec(); // TreeNode ans = deser.deserialize(ser.serialize(root));/** * Definition for a binary tree node. * function TreeNode(val) { * this.val = val; * this.left = this.right = null; * } */ /** * Encodes a tree to a single string. * * @param {TreeNode} root * @return {string} */ var serialize = function (root) { return rserialize(root, ""); };/** * Decodes your encoded data to tree. * * @param {string} data * @return {TreeNode} */ var deserialize = function (data) { const dataArray = data.split(" , "); return rdeserialize(dataArray); };const rserialize = (root, str) => { if (root === null) { str += " #, "; } else { str += root.val + "" + " , "; str = rserialize(root.left, str); str = rserialize(root.right, str); } return str;};const rdeserialize = (dataList) => { if (dataList[0] === " # ") { dataList.shift(); return null; } const root = new TreeNode(parseInt(dataList[0])); dataList.shift(); root.left = rdeserialize(dataList); root.right = rdeserialize(dataList); return root;}; /** * Your functions will be called as such: * deserialize(serialize(root)); *//** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode(int x) : val(x), left(NULL), right(NULL) {} * }; */ class Codec { public: // Encodes a tree to a single string. string serialize ( TreeNode * root ) { if ( ! root ) return "" ; string s = "" ; preorder ( root , s ); return s ; } void preorder ( TreeNode * root , string & s ) { if ( ! root ) s += "# " ; else { s += to_string ( root -> val ) + " " ; preorder ( root -> left , s ); preorder ( root -> right , s ); } } // Decodes your encoded data to tree. TreeNode * deserialize ( string data ) { if ( data == "" ) return nullptr ; stringstream ss ( data ); return deserialize ( ss ); } TreeNode * deserialize ( stringstream & ss ) { string first ; ss >> first ; if ( first == "#" ) return nullptr ; TreeNode * root = new TreeNode ( stoi ( first )); root -> left = deserialize ( ss ); root -> right = deserialize ( ss ); return root ; } }; // Your Codec object will be instantiated and called as such: // Codec ser, deser; // TreeNode* ans = deser.deserialize(ser.serialize(root));# Definition for a binary tree node. # class TreeNode(object): # def __init__(self, x): # self.val = x # self.left = None # self.right = None class Codec : def serialize ( self , root ): """Encodes a tree to a single string. :type root: TreeNode :rtype: str """ if root is None : return '' res = [] def preorder ( root ): if root is None : res . append ( "#," ) return res . append ( str ( root . val ) + "," ) preorder ( root . left ) preorder ( root . right ) preorder ( root ) return '' . join ( res ) def deserialize ( self , data ): """Decodes your encoded data to tree. :type data: str :rtype: TreeNode """ if not data : return None vals = data . split ( ',' ) def inner (): first = vals . pop ( 0 ) if first == '#' : return None return TreeNode ( int ( first ), inner (), inner ()) # seems using a constructor __init__(val, left, right) return inner () # Your Codec object will be instantiated and called as such: # ser = Codec() # deser = Codec() # ans = deser.deserialize(ser.serialize(root)) ############ from collections import deque # bfs, each level based class Codec : def serialize ( self , root ): """Encodes a tree to a single string. :type root: TreeNode :rtype: str """ ret = [] queue = deque ([ root ]) while queue : top = queue . popleft () if not top : ret . append ( "None" ) continue else : ret . append ( str ( top . val )) queue . append ( top . left ) queue . append ( top . right ) return "," . join ( ret ) def deserialize ( self , data ): """Decodes your encoded data to tree. :type data: str :rtype: TreeNode """ data = data . split ( "," ) if data [ 0 ] == "None" : return None root = TreeNode ( int ( data [ 0 ])) queue = deque ([ root ]) i = 0 while queue and i < len ( data ): top = queue . popleft () i += 1 left = right = None if i < len ( data ) and data [ i ] != "None" : left = TreeNode ( int ( data [ i ])) queue . append ( left ) i += 1 if i < len ( data ) and data [ i ] != "None" : right = TreeNode ( int ( data [ i ])) queue . append ( right ) top . left = left top . right = right return root # Your Codec object will be instantiated and called as such: # codec = Codec() # codec.deserialize(codec.serialize(root))Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.