Problem overview
The class Codec has two methods. serialize(root) takes the root of a binary tree, which may be empty, and returns a string. deserialize(data) takes a string made by serialize and returns the root of the same tree: the same shape and the same value in every node. The format of the string is up to you, as long as the two methods agree on it.
The lesson provides the TreeNode class, with val, left and right; its constructor takes the value and starts both children as null. The tests serialize a tree with one Codec, rebuild it with a different Codec, and print the result in level order.
For the tree [1,2,3,null,null,4,5], the solution's string is 1,2,#,#,3,4,#,#,5,#,#. For the empty tree it is #. For [-1,0,null,null,-100] it is -1,0,#,-100,#,#,#.
Approach: preorder with markers for missing children
A list of node values alone can't be turned back into a tree, because many shapes share the same values. The fix is to also write down where the tree is empty. We walk the tree in preorder (a node, its left subtree, its right subtree) and write # for every missing child.
With those markers, the string says exactly where each subtree ends, so reading it back is unambiguous. To rebuild, read one piece. A # is an empty subtree. A number is a node, and the pieces that follow are first its entire left subtree and then its entire right subtree, so the same routine can be called twice for the children. The reading pointer moves forward once per piece, so each direction is O(n).
The pieces are separated by commas. Without separators, -100 and a run of values like 12 would be ambiguous; with them, each value is read whole, minus sign included.
Step by step
Serialize
- State. The class keeps
values, astring[]that starts empty, as a property, so the helpers can add to it and read from it. - Fresh list.
serializesetsthis.values = []so each call starts from scratch. - Walk the tree. It calls the helper
this.write(root), which adds the pieces of the whole tree in preorder. - Join.
this.values.join(',')glues the pieces into one string with commas between them. - The helper
write. Ifnode === null, push'#'and return: an empty child has nothing below it. - A real node. Push
String(node.val)first (preorder), then callthis.write(node.left), thenthis.write(node.right).
Deserialize
- A second property.
pos, anumberstarting at 0, is the position of the next piece to read. - Cut the string.
data.split(',')gives the pieces back, in the order they were written, and goes intothis.values. - Reset the position to 0, then return what
this.read()gives: it reads one whole subtree, and called first it reads the whole tree. - The helper
read. Takethis.values[this.pos]intovalueand movethis.posforward. - Empty child. If
value === '#', returnnull. - A real node. Create
new TreeNode(Number(value)). Setnode.lefttothis.read()and thennode.righttothis.read(). The first call consumes every piece of the left subtree,#markers included, and leaves the position at the start of the right subtree. Return the node.
The solution in TypeScript
class Codec {
values: string[] = [];
pos: number = 0;
write(node: TreeNode | null): void {
if (node === null) {
this.values.push('#');
return;
}
this.values.push(String(node.val));
this.write(node.left);
this.write(node.right);
}
serialize(root: TreeNode | null): string {
this.values = [];
this.write(root);
return this.values.join(',');
}
read(): TreeNode | null {
let value = this.values[this.pos];
this.pos++;
if (value === '#') {
return null;
}
let node = new TreeNode(Number(value));
node.left = this.read();
node.right = this.read();
return node;
}
deserialize(data: string): TreeNode | null {
this.values = data.split(',');
this.pos = 0;
return this.read();
}
}
TypeScript details
String(node.val)andNumber(value)are real conversions that happen at run time. A cast such asvalue as numberwould convert nothing, because types are erased when the code compiles to JavaScript.Number('#')would giveNaN, which is why the marker is checked first.join(',')andsplit(',')are inverses here. Negative numbers survive the trip:String(-100)is'-100'andNumber('-100')is-100.- Comparing with
===againstnulland'#'avoids the loose-equality surprises of==. - The empty tree is not special.
serialize(null)pushes one marker and returns#, anddeserialize('#')reads that marker and returnsnull. Note that''.split(',')gives[''], not an empty array, so an empty string would not be a valid encoding of the empty tree here.
Complexity
Time: O(n) for both methods. A tree with n nodes has n + 1 empty children, so the string has 2n + 1 pieces. Each piece is written once and read once, with constant work per piece, and join and split are linear in the size of the string.
Space: O(n) for the list of pieces and the string, plus recursion depth equal to the tree's height (at most 400 here).
Edge cases and common mistakes
- Leaving out the
#markers. Values alone don't fix the shape: the trees[1,2]and[1,null,2]have the same preorder values and different shapes. - No separator. Without commas,
-1,0and-10could not be told apart. The comma also makes multi-digit and negative values safe. - Reading the right side first.
readmust take the left subtree before the right one, in the same orderwriteused. - Forgetting to reset.
deserializesetsposto 0 each time, andserializeclearsvalueseach time. Because of that, oneCodeccan serialize and deserialize several trees in a row. - The empty tree. It must produce a non-empty string,
#.
Testing
Serializing with one Codec, rebuilding with a second one, and printing the rebuilt tree in level order gives:
| Tree | String | Rebuilt tree |
|---|---|---|
[1,2,3,null,null,4,5] | 1,2,#,#,3,4,#,#,5,#,# | [1,2,3,null,null,4,5] |
[] | # | [] |
[-1,0,null,null,-100] | -1,0,#,-100,#,#,# | [-1,0,null,null,-100] |
[5] | 5,#,# | [5] |
[1,null,2,null,3] | 1,#,2,#,3,#,# | [1,null,2,null,3] |
