Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

428. Serialize and Deserialize N-ary Tree - Leetcode Solution

Code Implementation

"""
# Definition for a Node.
class Node:
    def __init__(self, val=None, children=None):
        self.val = val
        self.children = children if children is not None else []

class Codec:
    def serialize(self, root: 'Node') -> str:
        res = []
        def dfs(node):
            if not node:
                return
            res.append(str(node.val))
            res.append(str(len(node.children)))
            for child in node.children:
                dfs(child)
        dfs(root)
        return ' '.join(res)
    
    def deserialize(self, data: str) -> 'Node':
        if not data:
            return None
        tokens = iter(data.split())
        def dfs():
            val = int(next(tokens))
            size = int(next(tokens))
            node = Node(val)
            for _ in range(size):
                node.children.append(dfs())
            return node
        return dfs()
      
/*
// Definition for a Node.
class Node {
public:
    int val;
    vector<Node*> children;

    Node() {}

    Node(int _val) {
        val = _val;
    }

    Node(int _val, vector<Node*> _children) {
        val = _val;
        children = _children;
    }
};
*/

class Codec {
public:
    // Encodes a tree to a single string.
    string serialize(Node* root) {
        string res;
        dfs(root, res);
        return res;
    }
    
    void dfs(Node* node, string& res) {
        if (!node) return;
        res += to_string(node->val) + " ";
        res += to_string(node->children.size()) + " ";
        for (Node* child : node->children) {
            dfs(child, res);
        }
    }

    // Decodes your encoded data to tree.
    Node* deserialize(string data) {
        if (data.empty()) return nullptr;
        istringstream iss(data);
        return dfs2(iss);
    }
    
    Node* dfs2(istringstream& iss) {
        int val, size;
        iss >> val >> size;
        Node* node = new Node(val);
        for (int i = 0; i < size; ++i) {
            node->children.push_back(dfs2(iss));
        }
        return node;
    }
};
      
/*
// Definition for a Node.
class Node {
    public int val;
    public List<Node> children;

    public Node() {}

    public Node(int _val) {
        val = _val;
    }

    public Node(int _val, List<Node> _children) {
        val = _val;
        children = _children;
    }
};
*/

class Codec {
    // Encodes a tree to a single string.
    public String serialize(Node root) {
        StringBuilder sb = new StringBuilder();
        dfs(root, sb);
        return sb.toString();
    }
    
    private void dfs(Node node, StringBuilder sb) {
        if (node == null) return;
        sb.append(node.val).append(" ");
        sb.append(node.children.size()).append(" ");
        for (Node child : node.children) {
            dfs(child, sb);
        }
    }

    // Decodes your encoded data to tree.
    public Node deserialize(String data) {
        if (data.isEmpty()) return null;
        String[] tokens = data.split(" ");
        int[] idx = new int[]{0};
        return dfs2(tokens, idx);
    }
    
    private Node dfs2(String[] tokens, int[] idx) {
        int val = Integer.parseInt(tokens[idx[0]++]);
        int size = Integer.parseInt(tokens[idx[0]++]);
        Node node = new Node(val, new ArrayList<>());
        for (int i = 0; i < size; ++i) {
            node.children.add(dfs2(tokens, idx));
        }
        return node;
    }
}
      
/**
 * // Definition for a Node.
 * function Node(val, children) {
 *    this.val = val;
 *    this.children = children || [];
 * };
 */

var Codec = function() {};

Codec.prototype.serialize = function(root) {
    let res = [];
    function dfs(node) {
        if (!node) return;
        res.push(node.val);
        res.push(node.children.length);
        for (let child of node.children) {
            dfs(child);
        }
    }
    dfs(root);
    return res.join(' ');
};

Codec.prototype.deserialize = function(data) {
    if (!data) return null;
    let tokens = data.split(' ');
    let idx = 0;
    function dfs() {
        let val = parseInt(tokens[idx++]);
        let size = parseInt(tokens[idx++]);
        let node = new Node(val, []);
        for (let i = 0; i < size; ++i) {
            node.children.push(dfs());
        }
        return node;
    }
    return dfs();
};
      

Problem Description

The problem asks you to design an algorithm to serialize and deserialize an N-ary tree. An N-ary tree is a rooted tree in which each node can have up to N children (not just two, as in a binary tree).

You must implement two functions:

  • serialize(root): Converts the tree to a string representation.
  • deserialize(data): Converts the string representation back to the original tree structure.
Your serialization and deserialization must be lossless: after serializing and then deserializing, the tree should remain identical. You may assume the tree contains only integer values.

Constraints:

  • Each node has a val and a list of children.
  • There is one valid answer for each input.
  • Do not reuse elements or nodes.

Thought Process

At first glance, the problem is similar to serializing and deserializing a binary tree, but with the added complexity that each node can have any number of children.

For binary trees, we often use pre-order or level-order traversal, using markers (like # or null) for missing children. For N-ary trees, we need a way to know how many children each node has, since there could be zero or many.

The brute-force way would be to represent the tree in a nested format (like JSON), but that's verbose and less efficient. Instead, we can use a compact pre-order traversal where, for each node, we record:

  • The node's value
  • The number of its children
  • Then recursively process all its children
This way, during deserialization, we always know how many nodes to expect as children for any given node.

The main challenge is ensuring that the serialization format contains enough information to reconstruct the tree exactly, even when nodes have zero children.

Solution Approach

Let's break down the algorithm step-by-step:

  1. Serialization:
    • We use a pre-order traversal (visit node, then children).
    • For each node, append its val and the number of children to the result.
    • Then recursively serialize each child.
    • We join the results as a space-separated string for compactness.
  2. Deserialization:
    • We split the input string into tokens (numbers).
    • Using an iterator or index, we read the next two tokens: the node's val and its number of children.
    • We create a node with that value.
    • Then, for the given number of children, recursively build each child and add it to the node's children list.
    • Continue until all tokens are consumed.
  3. Design Choices:
    • We use pre-order because it allows us to process the tree recursively and always know where children start/end.
    • Including the number of children as metadata allows us to reconstruct the tree exactly, regardless of how many children each node has.

This approach is efficient, simple, and works for any N-ary tree structure.

Example Walkthrough

Let's walk through a concrete example:

Suppose we have the following N-ary tree:

        1
      / | \
     3  2  4
    / \
   5   6
    

  • Serialization:
    • Start at root (1): value = 1, children = 3
    • Process child 3: value = 3, children = 2
    • Process child 5: value = 5, children = 0
    • Process child 6: value = 6, children = 0
    • Process child 2: value = 2, children = 0
    • Process child 4: value = 4, children = 0

    The serialized string becomes:
    1 3 3 2 5 0 6 0 2 0 4 0

  • Deserialization:
    • Read 1 (root), 3 (children count)
    • Child 1: 3, 2 (so two children)
    • Child 1.1: 5, 0 (no children)
    • Child 1.2: 6, 0 (no children)
    • Child 2: 2, 0
    • Child 3: 4, 0

    As we process, we reconstruct the original tree structure exactly.

Time and Space Complexity

Brute-force Approach:

  • If we used a nested or verbose string representation (like JSON), both time and space complexity would be O(N), but with a much larger constant factor due to extra symbols and formatting.
Optimized Approach:
  • Time Complexity: O(N), where N is the number of nodes. We visit each node exactly once during both serialization and deserialization.
  • Space Complexity: O(N), for the output string (which stores two numbers per node) and the call stack during recursion (which in the worst case is O(H), where H is the tree height, but in the average case is much less).

The solution is optimal in both time and space for this problem.

Summary

To serialize and deserialize an N-ary tree, we use a pre-order traversal, recording each node's value and the number of its children. This compact representation allows us to reconstruct the tree exactly, regardless of its shape or the number of children per node. The approach is simple, efficient, and generalizes well to any N-ary tree. The key insight is to include the number of children in the serialization, enabling correct parsing during deserialization.