Before attempting this problem, you should be comfortable with:
N-ary Trees - Understanding tree nodes with a variable number of children stored in a list
Tree Traversal (DFS) - Recursively visiting all nodes in preorder fashion
Tree Traversal (BFS) - Level-order traversal using a queue to process nodes level by level
Hash Maps - Mapping node identities to their parent-child relationships during reconstruction
Recursion - Serializing and deserializing tree structures requires recursive processing
String Encoding - Converting tree structure to string format and parsing it back
1. Parent Child relationships
Intuition
One way to serialize an n-ary tree is to record each node along with a unique identifier and its parent's identifier. During serialization, we assign each node an ID, store its value, and record its parent's ID. During deserialization, we first create all nodes, then link children to their parents using the stored parent IDs. This approach explicitly encodes the tree structure through parent references.
Algorithm
Serialization:
Traverse the tree using DFS, assigning each node a unique identity starting from 1.
For each node, append three values: its identity, its value, and its parent's identity (or 'N' for the root).
Return the concatenated string.
Deserialization:
Parse the string in chunks of 3 characters to extract identity, value, and parent ID.
Create all nodes and store them in a map keyed by identity.
For each non-root node, find its parent using the parent ID and add the node to the parent's children list.
classCodec{public:
string serialize(Node* root){
string result;int identity =1;serializeHelper(root, result, identity,-1);return result;}voidserializeHelper(Node* root, string& result,int& identity,int parentId){if(!root)return;
result +=(char)(identity +'0');
result +=(char)(root->val +'0');
result +=(parentId ==-1)?'N':(char)(parentId +'0');int currentId = identity;for(Node* child : root->children){
identity++;serializeHelper(child, result, identity, currentId);}}
Node*deserialize(string data){if(data.empty())returnnullptr;
unordered_map<int, Node*> nodes;for(int i =0; i < data.size(); i +=3){int identity = data[i]-'0';int orgValue = data[i +1]-'0';
nodes[identity]=newNode(orgValue,vector<Node*>());}for(int i =3; i < data.size(); i +=3){int identity = data[i]-'0';int parentId = data[i +2]-'0';
nodes[parentId]->children.push_back(nodes[identity]);}return nodes[data[0]-'0'];}};
classCodec{constructor(){}serialize(root){const serializedList =[];let identity ={value:1};this._serializeHelper(root, serializedList, identity,null);return serializedList.join('');}_serializeHelper(root, serializedList, identity, parentId){if(!root)return;
serializedList.push(String.fromCharCode(identity.value +48));
serializedList.push(String.fromCharCode(root.val +48));
serializedList.push(parentId !==null? String.fromCharCode(parentId +48):'N');const currentId = identity.value;for(const child of root.children){
identity.value++;this._serializeHelper(child, serializedList, identity, currentId);}}deserialize(data){if(!data)returnnull;const nodes =newMap();for(let i =0; i < data.length; i +=3){const identity = data.charCodeAt(i)-48;const orgValue = data.charCodeAt(i +1)-48;
nodes.set(identity,new_Node(orgValue,[]));}for(let i =3; i < data.length; i +=3){const identity = data.charCodeAt(i)-48;const parentId = data.charCodeAt(i +2)-48;
nodes.get(parentId).children.push(nodes.get(identity));}return nodes.get(data.charCodeAt(0)-48);}}
publicclassCodec{privateint identity;publicstringSerialize(Node root){var sb =newStringBuilder();
identity =1;SerializeHelper(root, sb,null);return sb.ToString();}privatevoidSerializeHelper(Node root,StringBuilder sb,int? parentId){if(root ==null)return;
sb.Append((char)(identity +'0'));
sb.Append((char)(root.val +'0'));
sb.Append(parentId.HasValue ?(char)(parentId.Value +'0'):'N');int currentId = identity;foreach(var child in root.children){
identity++;SerializeHelper(child, sb, currentId);}}publicNodeDeserialize(string data){if(string.IsNullOrEmpty(data))returnnull;var nodes =newDictionary<int, Node>();for(int i =0; i < data.Length; i +=3){int id = data[i]-'0';int val = data[i +1]-'0';
nodes[id]=newNode(val,newList<Node>());}for(int i =3; i < data.Length; i +=3){int id = data[i]-'0';int parentId = data[i +2]-'0';
nodes[parentId].children.Add(nodes[id]);}return nodes[data[0]-'0'];}}
type Codec struct{}funcConstructor()*Codec {return&Codec{}}func(this *Codec)serialize(root *Node)string{if root ==nil{return""}var result []byte
identity :=1var helper func(node *Node, parentId int)
helper =func(node *Node, parentId int){if node ==nil{return}
result =append(result,byte(identity+'0'))
result =append(result,byte(node.Val+'0'))if parentId ==-1{
result =append(result,'N')}else{
result =append(result,byte(parentId+'0'))}
currentId := identity
for_, child :=range node.Children {
identity++helper(child, currentId)}}helper(root,-1)returnstring(result)}func(this *Codec)deserialize(data string)*Node {iflen(data)==0{returnnil}
nodes :=make(map[int]*Node)for i :=0; i <len(data); i +=3{
id :=int(data[i]-'0')
val :=int(data[i+1]-'0')
nodes[id]=&Node{Val: val, Children:[]*Node{}}}for i :=3; i <len(data); i +=3{
id :=int(data[i]-'0')
parentId :=int(data[i+2]-'0')
nodes[parentId].Children =append(nodes[parentId].Children, nodes[id])}return nodes[int(data[0]-'0')]}
class Codec {privatevar identity =1funserialize(root: Node?): String {val sb =StringBuilder()
identity =1serializeHelper(root, sb,null)return sb.toString()}privatefunserializeHelper(root: Node?, sb: StringBuilder, parentId: Int?){if(root ==null)return
sb.append((identity +'0'.code).toChar())
sb.append((root.`val` +'0'.code).toChar())
sb.append(if(parentId !=null)(parentId +'0'.code).toChar()else'N')val currentId = identity
for(child in root.children){
identity++serializeHelper(child, sb, currentId)}}fundeserialize(data: String): Node?{if(data.isEmpty())returnnullval nodes = mutableMapOf<Int, Node>()for(i indata.indices step 3){val id =data[i]-'0'val value =data[i +1]-'0'
nodes[id]=Node(value).apply{ children =mutableListOf()}}for(i in3 until data.length step 3){val id =data[i]-'0'val parentId =data[i +2]-'0'
nodes[parentId]!!.children.add(nodes[id]!!)}return nodes[data[0]-'0']}}
classCodec{privatevar identity =1funcserialize(_ root:Node?)->String{var result =[Character]()
identity =1serializeHelper(root,&result,nil)returnString(result)}privatefuncserializeHelper(_ root:Node?,_ result:inout[Character],_ parentId:Int?){guardlet root = root else{return}
result.append(Character(UnicodeScalar(identity +48)!))
result.append(Character(UnicodeScalar(root.val +48)!))iflet parentId = parentId {
result.append(Character(UnicodeScalar(parentId +48)!))}else{
result.append("N")}let currentId = identity
for child in root.children {
identity +=1serializeHelper(child,&result, currentId)}}funcdeserialize(_ data:String)->Node?{if data.isEmpty {returnnil}var nodes =[Int:Node]()let chars =Array(data)for i instride(from:0, to: chars.count, by:3){let id =Int(chars[i].asciiValue!-48)let val =Int(chars[i +1].asciiValue!-48)
nodes[id]=Node(val)
nodes[id]!.children =[]}for i instride(from:3, to: chars.count, by:3){let id =Int(chars[i].asciiValue!-48)let parentId =Int(chars[i +2].asciiValue!-48)
nodes[parentId]!.children.append(nodes[id]!)}return nodes[Int(chars[0].asciiValue!-48)]}}
Time & Space Complexity
Time complexity:
Serialization : O(N). For every node, we add 3 different values to the final string and every node is processed exactly once.
Deserialization : O(N). Technically, it is 3N for the first for loop and N for the second one. However, constants are ignored in asymptotic complexity analysis. So, the overall time complexity for deserialization is O(N).
Space complexity:
Serialization : O(N). The space occupied by the serialization helper function is through recursion stack and the final string that is produced. Usually, we don't take into consideration the space of the output. However, in this case, the output is something which is not fixed. For all we know, someone might be able to generate a string of size N/2. We don't know! So, the size of the final string is a part of the space complexity here. Overall, the space is 4N=O(N).
Deserialization : O(N). The space occupied by the deserialization helper function is through the hash map. For each entry, we have 3 values. Thus, we can say the space is 3N. But again, the constants don't really matter in asymptotic complexity. So, the overall space is O(N).
Where N is the number of nodes in the tree.
2. Depth First Search with Children Sizes
Intuition
Instead of storing parent IDs, we can store each node's value followed by its number of children. This gives us enough information to reconstruct the tree during DFS deserialization. When we encounter a node, we know exactly how many children to expect, so we can recursively build each subtree. This approach is more space-efficient since we only need 2 characters per node.
Algorithm
Serialization:
For each node, append its value followed by the count of its children.
Recursively serialize each child.
Return the concatenated string.
Deserialization:
Maintain an index pointer into the string.
Read the value at the current index and create a node.
Read the next character to get the number of children.
classWrappableInt:def__init__(self, x):
self.value = x
defgetValue(self):return self.value
defincrement(self):
self.value +=1classCodec:defserialize(self, root:'Node')->str:
serializedList =[]
self._serializeHelper(root, serializedList)return"".join(serializedList)def_serializeHelper(self, root, serializedList):ifnot root:return# Actual value
serializedList.append(chr(root.val +48))# Number of children
serializedList.append(chr(len(root.children)+48))for child in root.children:
self._serializeHelper(child, serializedList)defdeserialize(self, data:str)->'Node':ifnot data:returnNonereturn self._deserializeHelper(data, WrappableInt(0))def_deserializeHelper(self, data, index):if index.getValue()==len(data):returnNone# The invariant here is that the "index" always# points to a node and the value next to it # represents the number of children it has.
node = Node(ord(data[index.getValue()])-48,[])
index.increment()
numChildren =ord(data[index.getValue()])-48for _ inrange(numChildren):
index.increment()
node.children.append(self._deserializeHelper(data, index))return node
classCodec{classWrappableInt{privateint value;publicWrappableInt(int x){this.value = x;}publicintgetValue(){returnthis.value;}publicvoidincrement(){this.value++;}}publicStringserialize(Node root){StringBuilder sb =newStringBuilder();this._serializeHelper(root, sb);return sb.toString();}privatevoid_serializeHelper(Node root,StringBuilder sb){if(root ==null){return;}// Add the value of the node
sb.append((char)(root.val +'0'));// Add the number of children
sb.append((char)(root.children.size()+'0'));// Recurse on the subtrees and build the // string accordinglyfor(Node child : root.children){this._serializeHelper(child, sb);}}publicNodedeserialize(String data){if(data.isEmpty())returnnull;returnthis._deserializeHelper(data,newWrappableInt(0));}privateNode_deserializeHelper(String data,WrappableInt index){if(index.getValue()== data.length()){returnnull;}// The invariant here is that the "index" always// points to a node and the value next to it // represents the number of children it has.Node node =newNode(data.charAt(index.getValue())-'0',newArrayList<Node>());
index.increment();int numChildren = data.charAt(index.getValue())-'0';for(int i =0; i < numChildren; i++){
index.increment();
node.children.add(this._deserializeHelper(data, index));}return node;}}
classCodec{public:
string serialize(Node* root){
string result;serializeHelper(root, result);return result;}voidserializeHelper(Node* root, string& result){if(!root)return;
result +=(char)(root->val +'0');
result +=(char)(root->children.size()+'0');for(Node* child : root->children){serializeHelper(child, result);}}
Node*deserialize(string data){if(data.empty())returnnullptr;int index =0;returndeserializeHelper(data, index);}
Node*deserializeHelper(string& data,int& index){if(index == data.size())returnnullptr;
Node* node =newNode(data[index]-'0',vector<Node*>());
index++;int numChildren = data[index]-'0';for(int i =0; i < numChildren; i++){
index++;
node->children.push_back(deserializeHelper(data, index));}return node;}};
classCodec{constructor(){}/**
* @param {_Node|null} root
* @return {string}
*/serialize=function(root){const serializedList =[];this._serializeHelper(root, serializedList);return serializedList.join('');}_serializeHelper=function(root, serializedList){if(!root){return;}// Actual value
serializedList.push(String.fromCharCode(root.val +48));// Number of children
serializedList.push(String.fromCharCode(root.children.length +48));for(const child of root.children){this._serializeHelper(child, serializedList);}}/**
* @param {string} data
* @return {_Node|null}
*/deserialize=function(data){if(!data){returnnull;}const index ={value:0};returnthis._deserializeHelper(data, index);}_deserializeHelper=function(data, index){if(index.value === data.length){returnnull;}// The invariant here is that the "index" always// points to a node and the value next to it// represents the number of children it has.const node =new_Node(data.charCodeAt(index.value)-48,[]);
index.value++;const numChildren = data.charCodeAt(index.value)-48;for(let i =0; i < numChildren; i++){
index.value++;
node.children.push(this._deserializeHelper(data, index));}return node;}}
type Codec struct{}funcConstructor()*Codec {return&Codec{}}func(this *Codec)serialize(root *Node)string{var result []bytevar helper func(node *Node)
helper =func(node *Node){if node ==nil{return}
result =append(result,byte(node.Val+'0'))
result =append(result,byte(len(node.Children)+'0'))for_, child :=range node.Children {helper(child)}}helper(root)returnstring(result)}func(this *Codec)deserialize(data string)*Node {iflen(data)==0{returnnil}
index :=0var helper func()*Node
helper =func()*Node {if index ==len(data){returnnil}
node :=&Node{Val:int(data[index]-'0'), Children:[]*Node{}}
index++
numChildren :=int(data[index]-'0')for i :=0; i < numChildren; i++{
index++
node.Children =append(node.Children,helper())}return node
}returnhelper()}
class Codec {funserialize(root: Node?): String {val sb =StringBuilder()serializeHelper(root, sb)return sb.toString()}privatefunserializeHelper(root: Node?, sb: StringBuilder){if(root ==null)return
sb.append((root.`val` +'0'.code).toChar())
sb.append((root.children.size +'0'.code).toChar())for(child in root.children){serializeHelper(child, sb)}}fundeserialize(data: String): Node?{if(data.isEmpty())returnnullval index =intArrayOf(0)returndeserializeHelper(data, index)}privatefundeserializeHelper(data: String, index: IntArray): Node?{if(index[0]==data.length)returnnullval node =Node(data[index[0]]-'0').apply{ children =mutableListOf()}
index[0]++val numChildren =data[index[0]]-'0'for(i in0 until numChildren){
index[0]++
node.children.add(deserializeHelper(data, index)!!)}return node
}}
classCodec{funcserialize(_ root:Node?)->String{var result =[Character]()serializeHelper(root,&result)returnString(result)}privatefuncserializeHelper(_ root:Node?,_ result:inout[Character]){guardlet root = root else{return}
result.append(Character(UnicodeScalar(root.val +48)!))
result.append(Character(UnicodeScalar(root.children.count +48)!))for child in root.children {serializeHelper(child,&result)}}funcdeserialize(_ data:String)->Node?{if data.isEmpty {returnnil}var index =0let chars =Array(data)returndeserializeHelper(chars,&index)}privatefuncdeserializeHelper(_ data:[Character],_ index:inoutInt)->Node?{if index == data.count {returnnil}let node =Node(Int(data[index].asciiValue!-48))
node.children =[]
index +=1let numChildren =Int(data[index].asciiValue!-48)for_in0..<numChildren {
index +=1
node.children.append(deserializeHelper(data,&index)!)}return node
}}
Time & Space Complexity
Time complexity:
Serialization : O(N). For every node, we add 2 different values to the final string and every node is processed exactly once.
Deserialization : O(N). For deserialization, we process the entire string, one character at a time and also construct the tree along the way. So, the overall time complexity for deserialization is 2N=O(N).
Space complexity:
Serialization : O(N). The space occupied by the serialization helper function is through recursion stack and the final string that is produced. We know the size of the final string to be 2N. So, that is one part of the space complexity. The other part is the one occupied by the recursion stack which is O(N). Overall, the space is O(N).
Deserialization : O(N). For deserialization, the space occupied is by the recursion stack only. We don't use any other intermediate data structures like we did in the previous approach and simply rely on the information in the string and recursion to work it's magic. So, the space complexity would be O(N) since this is not a balanced tree of any sort. It's not even binary.
Where N is the number of nodes in the tree.
3. Depth First Search with a Sentinel
Intuition
Another approach uses a sentinel character (like '#') to mark the end of a node's children. After serializing a node's value and all its children recursively, we append the sentinel. During deserialization, we read nodes and recursively build children until we hit the sentinel, which signals that all children for the current node have been processed.
Algorithm
Serialization:
For each node, append its value.
Recursively serialize each child.
Append the sentinel '#' to mark the end of this node's subtree.
Return the concatenated string.
Deserialization:
Maintain an index pointer into the string.
Read the value at the current index and create a node.
Increment the index and repeatedly deserialize children until the sentinel '#' is encountered.
Skip the sentinel and return the constructed node.
classWrappableInt:def__init__(self, x):
self.value = x
defgetValue(self):return self.value
defincrement(self):
self.value +=1classCodec:defserialize(self, root:'Node')->str:
serializedList =[]
self._serializeHelper(root, serializedList)return"".join(serializedList)def_serializeHelper(self, root, serializedList):ifnot root:return# Actual value
serializedList.append(chr(root.val +48))for child in root.children:
self._serializeHelper(child, serializedList)# Add the sentinel to indicate that all the children# for the current node have been processed
serializedList.append('#')defdeserialize(self, data:str)->'Node':ifnot data:returnNonereturn self._deserializeHelper(data, WrappableInt(0))def_deserializeHelper(self, data, index):if index.getValue()==len(data):returnNone
node = Node(ord(data[index.getValue()])-48,[])
index.increment()while(data[index.getValue()]!='#'):
node.children.append(self._deserializeHelper(data, index))# Discard the sentinel. Note that this also moves us# forward in the input string. So, we don't have the index# progressing inside the above while loop!
index.increment()return node
classCodec{classWrappableInt{privateint value;publicWrappableInt(int x){this.value = x;}publicintgetValue(){returnthis.value;}publicvoidincrement(){this.value++;}}// Encodes a tree to a single string.publicStringserialize(Node root){StringBuilder sb =newStringBuilder();this._serializeHelper(root, sb);return sb.toString();}privatevoid_serializeHelper(Node root,StringBuilder sb){if(root ==null){return;}// Add the value of the node
sb.append((char)(root.val +'0'));// Recurse on the subtrees and build the // string accordinglyfor(Node child : root.children){this._serializeHelper(child, sb);}// Add the sentinel to indicate that all the children// for the current node have been processed
sb.append('#');}// Decodes your encoded data to tree.publicNodedeserialize(String data){if(data.isEmpty())returnnull;returnthis._deserializeHelper(data,newWrappableInt(0));}privateNode_deserializeHelper(String data,WrappableInt index){if(index.getValue()== data.length()){returnnull;}Node node =newNode(data.charAt(index.getValue())-'0',newArrayList<Node>());
index.increment();while(data.charAt(index.getValue())!='#'){
node.children.add(this._deserializeHelper(data, index));}// Discard the sentinel. Note that this also moves us// forward in the input string. So, we don't have the index// progressing inside the above while loop!
index.increment();return node;}}
type Codec struct{}funcConstructor()*Codec {return&Codec{}}func(this *Codec)serialize(root *Node)string{var result []bytevar helper func(node *Node)
helper =func(node *Node){if node ==nil{return}
result =append(result,byte(node.Val+'0'))for_, child :=range node.Children {helper(child)}
result =append(result,'#')}helper(root)returnstring(result)}func(this *Codec)deserialize(data string)*Node {iflen(data)==0{returnnil}
index :=0var helper func()*Node
helper =func()*Node {if index ==len(data){returnnil}
node :=&Node{Val:int(data[index]-'0'), Children:[]*Node{}}
index++for data[index]!='#'{
node.Children =append(node.Children,helper())}
index++return node
}returnhelper()}
class Codec {funserialize(root: Node?): String {val sb =StringBuilder()serializeHelper(root, sb)return sb.toString()}privatefunserializeHelper(root: Node?, sb: StringBuilder){if(root ==null)return
sb.append((root.`val` +'0'.code).toChar())for(child in root.children){serializeHelper(child, sb)}
sb.append('#')}fundeserialize(data: String): Node?{if(data.isEmpty())returnnullval index =intArrayOf(0)returndeserializeHelper(data, index)}privatefundeserializeHelper(data: String, index: IntArray): Node?{if(index[0]==data.length)returnnullval node =Node(data[index[0]]-'0').apply{ children =mutableListOf()}
index[0]++while(data[index[0]]!='#'){
node.children.add(deserializeHelper(data, index)!!)}
index[0]++return node
}}
classCodec{funcserialize(_ root:Node?)->String{var result =[Character]()serializeHelper(root,&result)returnString(result)}privatefuncserializeHelper(_ root:Node?,_ result:inout[Character]){guardlet root = root else{return}
result.append(Character(UnicodeScalar(root.val +48)!))for child in root.children {serializeHelper(child,&result)}
result.append("#")}funcdeserialize(_ data:String)->Node?{if data.isEmpty {returnnil}var index =0let chars =Array(data)returndeserializeHelper(chars,&index)}privatefuncdeserializeHelper(_ data:[Character],_ index:inoutInt)->Node?{if index == data.count {returnnil}let node =Node(Int(data[index].asciiValue!-48))
node.children =[]
index +=1while data[index]!="#"{
node.children.append(deserializeHelper(data,&index)!)}
index +=1return node
}}
Time & Space Complexity
Time complexity:
Serialization : O(N). For every node, we add 2 different values to the final string and every node is processed exactly once.
Deserialization : O(N). For deserialization, we process the entire string, one character at a time and also construct the tree along the way. So, the overall time complexity for deserialization is 2N=O(N).
Space complexity:
Serialization : O(N). The space occupied by the serialization helper function is through recursion stack and the final string that is produced. We know the size of the final string to be 2N. So, that is one part of the space complexity. The other part is the one occupied by the recursion stack which is O(N). Overall, the space is O(N).
Deserialization : O(N). For deserialization, the space occupied is by the recursion stack only. We don't use any other intermediate data structures like we did in the previous approach and simply rely on the information in the string and recursion to work it's magic. So, the overall space complexity would be O(N).
Where N is the number of nodes in the tree.
4. Level order traversal
Intuition
We can serialize the tree level by level using BFS. We use two sentinel markers: '#' to indicate the end of a level, and '$' to indicate switching to a different parent's children on the same level. During deserialization, we track nodes at the current and previous levels, using the sentinels to know when to move to the next level or switch parents.
Algorithm
Serialization:
Use a queue initialized with the root and a level-end marker.
For each node, append its value and enqueue all its children.
After processing a node (if not the last on its level), add a child-switch marker.
When encountering the level-end marker, append '#' and add a new level-end marker if the queue is not empty.
Return the concatenated string.
Deserialization:
Create the root node from the first character.
Maintain current and previous level lists.
Process each character:
'#': Move to the next level by swapping lists and getting the next parent.
'$': Switch to the next parent on the same level.
Otherwise: Create a child node and attach it to the current parent.
classCodec:def_serializeHelper(self, root, serializedList):
queue = collections.deque()
queue.append(root)
queue.append(None)while queue:# Pop a node
node = queue.popleft()# If this is an "endNode", we need to add another one# to mark the end of the current level unless this# was the last level.if(node ==None):# We add a sentinal value of "#" here
serializedList.append("#")if queue:
queue.append(None)elif node =="C":# Add a sentinal value of "$" here to mark the switch to a# different parent.
serializedList.append("$")else:# Add value of the current node and add all of it's# children nodes to the queue. Note how we convert# the integers to their corresponding ASCII counterparts.
serializedList.append(chr(node.val +48))for child in node.children:
queue.append(child)# If this not is NOT the last one on the current level, # add a childNode as well since we move on to processing# the next node.if queue[0]!=None:
queue.append("C")defserialize(self, root:'Node')->str:ifnot root:return""
serializedList =[]
self._serializeHelper(root, serializedList)return"".join(serializedList)def_deserializeHelper(self, data, rootNode):# We move one level at a time and at every level, we need access# to the nodes on the previous level as well so that we can form# the children arrays properly. Hence two arrays.
prevLevel, currentLevel = collections.deque(), collections.deque()
currentLevel.append(rootNode)
parentNode = rootNode
# Process the characters in the string one at a time.for i inrange(1,len(data)):if data[i]=="#":# Special processing for end of level. We need to swap the# array lists. Here, we simply re-initialize the "currentLevel"# arraylist rather than clearing it.
prevLevel = currentLevel
currentLevel = collections.deque()# Since we move one level down, we take the parent as the first# node on the current level.
parentNode = prevLevel.popleft()if prevLevel elseNoneelse:if data[i]=="$":# Special handling for change in parent on the same level
parentNode = prevLevel.popleft()if prevLevel elseNoneelse:
childNode = Node(ord(data[i])-48,[])
currentLevel.append(childNode)
parentNode.children.append(childNode)defdeserialize(self, data:str)->'Node':ifnot data:returnNone
rootNode = Node(ord(data[0])-48,[])
self._deserializeHelper(data, rootNode)return rootNode
classCodec{publicStringserialize(Node root){if(root ==null){return"";}StringBuilder sb =newStringBuilder();this._serializeHelper(root, sb);return sb.toString();}privatevoid_serializeHelper(Node root,StringBuilder sb){// Queue to perform a level order traversal of the treeQueue<Node> q =newLinkedList<Node>();// Two dummy nodes that will help us in serialization string formation.// We insert the "endNode" whenever a level ends and the "childNode"// whenever a node's children are added to the queue and we are about// to switch over to the next node.Node endNode =newNode();Node childNode =newNode();
q.add(root);
q.add(endNode);while(!q.isEmpty()){// Pop a nodeNode node = q.poll();// If this is an "endNode", we need to add another one// to mark the end of the current level unless this// was the last level.if(node == endNode){// We add a sentinal value of "#" here
sb.append('#');if(!q.isEmpty()){
q.add(endNode);}}elseif(node == childNode){// Add a sentinal value of "$" here to mark the switch to a// different parent.
sb.append('$');}else{// Add value of the current node and add all of it's// children nodes to the queue. Note how we convert// the integers to their corresponding ASCII counterparts.
sb.append((char)(node.val +'0'));for(Node child : node.children){
q.add(child);}// If this not is NOT the last one on the current level, // add a childNode as well since we move on to processing// the next node.if(q.peek()!= endNode){
q.add(childNode);}}}}publicNodedeserialize(String data){if(data.isEmpty()){returnnull;}Node rootNode =newNode(data.charAt(0)-'0',newArrayList<Node>());this._deserializeHelper(data, rootNode);return rootNode;}privatevoid_deserializeHelper(String data,Node rootNode){// We move one level at a time and at every level, we need access// to the nodes on the previous level as well so that we can form// the children arrays properly. Hence two arrays.LinkedList<Node> currentLevel =newLinkedList<Node>();LinkedList<Node> prevLevel =newLinkedList<Node>();
currentLevel.add(rootNode);Node parentNode = rootNode;// Process the characters in the string one at a time.for(int i =1; i < data.length(); i++){char d = data.charAt(i);if(d =='#'){// Special processing for end of level. We need to swap the// array lists. Here, we simply re-initialize the "currentLevel"// arraylist rather than clearing it.
prevLevel = currentLevel;
currentLevel =newLinkedList<Node>();// Since we move one level down, we take the parent as the first// node on the current level.
parentNode = prevLevel.poll();}else{if(d =='$'){// Special handling for change in parent on the same level
parentNode = prevLevel.poll();}else{Node childNode =newNode(d -'0',newArrayList<Node>());
currentLevel.add(childNode);
parentNode.children.add(childNode);}}}}}
classCodec{private:void_serializeHelper(Node* root, string& serializedList){
deque<variant<Node*,char>> queue;
queue.push_back(root);
queue.push_back(nullptr);while(!queue.empty()){// Pop a nodeauto front = queue.front();
queue.pop_front();// If this is an "endNode", we need to add another one// to mark the end of the current level unless this// was the last level.if(holds_alternative<Node*>(front)&&get<Node*>(front)==nullptr){// We add a sentinel value of "#" here
serializedList +="#,";if(!queue.empty()){
queue.push_back(nullptr);}}// Add a sentinel value of "$" here to mark the switch to a// different parent.elseif(holds_alternative<char>(front)&&get<char>(front)=='C'){
serializedList +="$,";}else{// Add value of the current node and add all of its// children nodes to the queue.
Node* node =get<Node*>(front);
serializedList +=to_string(node->val)+",";for(Node* child : node->children){
queue.push_back(child);}// If this node is NOT the last one on the current level,// add a childNode as well since we move on to processing// the next node.if(!queue.empty()&&!(holds_alternative<Node*>(queue.front())&&get<Node*>(queue.front())==nullptr)){
queue.push_back('C');}}}}void_deserializeHelper(const string& data, Node* rootNode){// We move one level at a time and at every level, we need access// to the nodes on the previous level as well so that we can form// the children arrays properly. Hence two arrays.
deque<Node*> prevLevel, currentLevel;
currentLevel.push_back(rootNode);
Node* parentNode = rootNode;int i =0;// Skip past first value (already parsed for rootNode)while(i < data.length()&& data[i]!=',') i++;
i++;// skip comma// Process the characters in the string one at a time.while(i < data.length()){// Special processing for end of level. We need to swap the// array lists. Here, we simply re-initialize the "currentLevel"// arraylist rather than clearing it.if(data[i]=='#'){
prevLevel = currentLevel;
currentLevel =deque<Node*>();// Since we move one level down, we take the parent as the first// node on the current level.if(!prevLevel.empty()){
parentNode = prevLevel.front();
prevLevel.pop_front();}else{
parentNode =nullptr;}
i +=2;// skip "#,"}// Special handling for change in parent on the same levelelseif(data[i]=='$'){if(!prevLevel.empty()){
parentNode = prevLevel.front();
prevLevel.pop_front();}else{
parentNode =nullptr;}
i +=2;// skip "$,"}else{// Parse number (handles multi-digit values)int val =0;while(i < data.length()&& data[i]!=','){
val = val *10+(data[i]-'0');
i++;}
i++;// skip comma
Node* childNode =newNode(val,vector<Node*>());
currentLevel.push_back(childNode);
parentNode->children.push_back(childNode);}}}public:
string serialize(Node* root){if(root ==nullptr){return"";}
string serializedList;_serializeHelper(root, serializedList);return serializedList;}
Node*deserialize(string data){if(data.empty()){returnnullptr;}// Parse first value (handles multi-digit values)int val =0;int i =0;while(i < data.length()&& data[i]!=','){
val = val *10+(data[i]-'0');
i++;}
Node* rootNode =newNode(val,vector<Node*>());_deserializeHelper(data, rootNode);return rootNode;}};
classCodec{_serializeHelper(root, serializedList){const queue =newDeque();
queue.pushBack(root);
queue.pushBack(null);while(queue.size()>0){// Pop a nodeconst node = queue.popFront();// If this is an "endNode", we need to add another one// to mark the end of the current level unless this// was the last level.if(node ===null){// We add a sentinel value of "#" here
serializedList.push("#");if(queue.size()>0){
queue.pushBack(null);}}elseif(node ==="C"){// Add a sentinel value of "$" here to mark the switch to a// different parent.
serializedList.push("$");}else{// Add value of the current node and add all of its// children nodes to the queue. Note how we convert// the integers to their corresponding ASCII counterparts.
serializedList.push(String.fromCharCode(node.val +48));for(const child of node.children){
queue.pushBack(child);}// If this node is NOT the last one on the current level,// add a childNode as well since we move on to processing// the next node.if(queue.front()!==null){
queue.pushBack("C");}}}}serialize(root){if(!root){return"";}const serializedList =[];this._serializeHelper(root, serializedList);return serializedList.join("");}_deserializeHelper(data, rootNode){// We move one level at a time and at every level, we need access// to the nodes on the previous level as well so that we can form// the children arrays properly. Hence two arrays.let prevLevel =newDeque();let currentLevel =newDeque();
currentLevel.pushBack(rootNode);let parentNode = rootNode;// Process the characters in the string one at a time.for(let i =1; i < data.length; i++){if(data[i]==="#"){// Special processing for end of level. We need to swap the// array lists. Here, we simply re-initialize the "currentLevel"// arraylist rather than clearing it.
prevLevel = currentLevel;
currentLevel =newDeque();// Since we move one level down, we take the parent as the first// node on the current level.
parentNode = prevLevel.size()>0? prevLevel.popFront():null;}else{if(data[i]==="$"){// Special handling for change in parent on the same level
parentNode = prevLevel.size()>0? prevLevel.popFront():null;}else{const childNode =new_Node(data.charCodeAt(i)-48,[]);
currentLevel.pushBack(childNode);
parentNode.children.push(childNode);}}}}deserialize(data){if(!data){returnnull;}const rootNode =new_Node(data.charCodeAt(0)-48,[]);this._deserializeHelper(data, rootNode);return rootNode;}}
Serialization : O(N). For every node, we add 2 different values to the final string and every node is processed exactly once. We add the value of the node itself and we also add the child switch sentinel. Also, for the nodes that end a particular level, we add the level end sentinel.
Deserialization : O(N). For deserialization, we process the entire string, one character at a time and also construct the tree along the way. So, the overall time complexity for deserialization is 2N=O(N).
Space complexity:
Serialization : O(N). The space occupied by the serialization helper function is through the queue and the final string that is produced. We know the size of the final string to be 2N. So that is one part of the space complexity. The other part is the one occupied by the queue which is O(N). Overall, the space is O(N).
Deserialization : O(N). For deserialization, the space is mostly occupied by the two lists that we use. The space complexity there is O(N). Note that when we re-initialize a list, the memory that was allocated earlier is deallocated by the garbage collector and it's essentially equal to a single list of size O(N).
Where N is the number of nodes in the tree.
Common Pitfalls
Losing Children Count Information
Unlike binary trees where each node has exactly two children slots, n-ary trees have variable numbers of children. Failing to encode how many children each node has makes it impossible to correctly reconstruct the tree during deserialization. Each serialization approach must capture this information somehow (explicit count, sentinels, or parent references).
Incorrect Character Encoding for Values
When converting node values to single characters using ASCII offsets (like chr(val + 48)), values outside the valid range cause issues. This approach only works for small, non-negative integers. Larger values or negative numbers require a different encoding strategy with proper delimiters.
Mishandling the Children List Initialization
When creating nodes during deserialization, forgetting to initialize the children list (or initializing it as null instead of an empty list) causes null pointer exceptions when adding children later. Always initialize the children collection before attempting to add child nodes.
Confusing Parent-Child Relationship Direction
In the parent-child ID approach, mixing up which ID refers to the parent versus the child node leads to a completely incorrect tree structure. The serialized data must clearly distinguish between a node's own identity and its parent's identity.
Off-by-One Errors in Sentinel-Based Approaches
When using sentinels to mark the end of children (like #), forgetting to increment the index after consuming the sentinel causes the deserializer to get stuck or skip actual node values. The index must advance both when creating nodes and when encountering sentinels.