Problem overview
The class Codec has two methods. serialize(root) turns a binary tree, possibly empty, into a string. deserialize(data) takes a string that serialize produced and returns the root of the same tree: the same shape and the same value in every node. The format is up to you. The lesson provides TreeNode, built as new TreeNode(val), with val, left and right fields.
The tests serialize each tree with one Codec and rebuild it with the deserialize of a second one, so the string must carry everything: deserialize can't rely on anything stored in the first object. For the tree [1,2,3,null,null,4,5] this solution writes "1,2,#,#,3,4,#,#,5,#,#", and for the empty tree it writes "#".
Approach: preorder with a marker for every missing child
A list of values alone doesn't determine a tree: the values 1, 2 could be a root with a left child or with a right child. The fix is to write the missing children too. The solution walks the tree in preorder, a node and then its left and right subtrees, and writes # wherever a child is missing. Each # is a complete, empty subtree, so the reader always knows where a subtree ends.
Reading mirrors writing. Take the next piece. If it is #, the subtree is empty. Otherwise it is a node's value: create the node, read its left subtree, then read its right subtree. Each read call consumes exactly the pieces of one subtree, so the right subtree starts exactly where the left one stopped.
The pieces are separated by commas because values can have several digits and a minus sign. Without a separator, "-100" could be read as other values. Splitting at the commas gives each value back whole.
Step by step
- Start the list of pieces in
serializeasthis.values = []. - Call the helper
this.write(root). It appends the whole tree's pieces in preorder. - Return
this.values.join(','), the pieces glued into one string with commas between them. - Declare the helper
write(node). - Missing child: if
nodeisnull, push'#'. - Return right after, since an empty child has nothing under it.
- Write the node: push
String(node.val). - Left subtree: call
this.write(node.left). - Right subtree: call
this.write(node.right). - In
deserialize, cut the string into pieces withdata.split(',')and store the array inthis.values. - Start the position at
this.pos = 0. - Return
this.read(), which reads the whole tree starting at piecethis.pos. - Declare the helper
read(). - Take the next piece into
valueand advancethis.pos. - Empty subtree: if
valueis'#', returnnull. - Real node: otherwise create
new TreeNode(Number(value)). The pieces are strings, soNumberturns"-1"back into the number -1. - Left child:
node.left = this.read(). - Right child:
node.right = this.read(). - Return the node.
The solution in JavaScript
class Codec {
write(node) {
if (node === null) {
this.values.push('#');
return;
}
this.values.push(String(node.val));
this.write(node.left);
this.write(node.right);
}
serialize(root) {
this.values = [];
this.write(root);
return this.values.join(',');
}
read() {
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) {
this.values = data.split(',');
this.pos = 0;
return this.read();
}
}
Details worth knowing:
- The state lives on
this.valuesandposare properties, so the recursive helpers share them without passing them around.serializeanddeserializeeach resetvaluesfirst, so aCodeccan be used again. joinandsplitare inverses here.join(',')puts a comma between pieces, andsplit(',')cuts at every comma. This is safe because no piece contains a comma: the pieces are integers and#.NumberandString. Values are stored as text and converted back.Number("-100")is -100, with the sign included.===againstnull.writechecksnode === null, which matches thenullthe lesson's nodes use for a missing child.
Complexity
Time: O(n) for both methods. write is called once per node plus once per missing child, which is 2n + 1 calls in all, and each does constant work. join and split are linear in the length of the string. read consumes each of the 2n + 1 pieces once.
Space: O(n). The list and the string hold about 2n + 1 pieces. The recursion is as deep as the tree, which the lesson limits to 400 levels.
Edge cases and common mistakes
- The empty tree.
write(null)pushes only#, so the string is"#".deserialize("#")reads that piece and returnsnull. - A single node. The tree
[7]becomes"7,#,#". - A left chain. The tree
[1,2,null,3]becomes"1,2,3,#,#,#,#", and it is different from a right chain, which has its markers in other places. - Negative and multi-digit values.
[-1,0,null,null,-100]becomes"-1,0,#,-100,#,#,#"and comes back whole, thanks to the commas. - Leaving out the
#markers. A preorder list of values alone can't tell where a subtree ends. - Reading the right subtree before the left.
readmust take the pieces in the same orderwriteput them. - Passing
posby value. A plain number argument would not carry the progress made by the left call to the right call. A property onthisdoes.
Testing
Trees are in level order; each result was serialized by one Codec and rebuilt by another.
[1,2,3,null,null,4,5]gives the string"1,2,#,#,3,4,#,#,5,#,#"and comes back as[1,2,3,null,null,4,5].[]gives"#"and comes back as[].[-1,0,null,null,-100]gives"-1,0,#,-100,#,#,#"and comes back as[-1,0,null,null,-100].[7]gives"7,#,#"and comes back as[7].[1,2,null,3]gives"1,2,3,#,#,#,#"and comes back as[1,2,null,3].
