Problem overview
The class Codec has two methods. Serialize(TreeNode root) turns a binary tree, possibly empty, into a string. Deserialize(string data) takes a string that Serialize produced and rebuilds the same tree, with the same shape and the same value in every node. The format is up to you. The lesson provides the TreeNode class (val, left, right), as the starter code's comment shows.
The tests serialize a tree with one Codec object and rebuild it with the Deserialize of a second one. So everything the rebuild needs must be inside the string; the two objects share nothing else.
For the tree with root 1, left child 2 and right child 3, whose children are 4 and 5, the format used here gives 1,2,#,#,3,4,#,#,5,#,#. Reading it back gives the tree again.
Approach: preorder with a marker for every missing child
A list of values alone is not enough. The values 1,2,3 could be a line going left, a line going right, or a root with two children. What removes the doubt is recording the missing children too.
Write the tree in preorder: a node, then its whole left subtree, then its whole right subtree. For every null child, write the marker #. Now each subtree in the string is self-describing: reading a subtree means reading one piece, and if it is a number, reading a left subtree and then a right subtree right after it; if it is #, the subtree is empty and reading stops. The reader always knows where a subtree ends, so the string alone is enough.
Commas separate the pieces, so values with several digits or a minus sign, like -100, are read back whole.
Step by step
- Field
values: aList<string>holding the pieces. Both methods work with it, and the helpers need to reach it. - In
Serialize, startvaluesas a new empty list, so each call begins from scratch. - Call the helper
Write(root), which adds the pieces of the whole tree in preorder. - Return
string.Join(",", values), the pieces glued into one line with commas between them. - Write
void Write(TreeNode node). It adds the pieces for the subtree undernode. - Missing child: if
nodeisnull, add"#"tovalues. - Return early after the marker: an empty child has nothing below it.
- Write the node: add
node.val.ToString(). The node comes first, which is what preorder means. - Write the left subtree with
Write(node.left). - Write the right subtree with
Write(node.right). - Field
pos: anint, the position of the next pieceDeserializereads. - In
Deserialize, cut the string back into pieces withdata.Split(','), and put them into a newList<string>stored invalues. - Start
posat 0. The first piece is the root, or#for an empty tree. - Return
Read(), which reads one whole subtree starting atpos. Called at the start, that is the whole tree. - Write
TreeNode Read(). It mirrorsWrite: pieces are read in the order they were written. - Take the next piece:
value = values[pos], thenpos++. - Marker: if
valueis"#", returnnull. - Otherwise create the node with
new TreeNode(int.Parse(value)). - Left subtree:
node.left = Read(). This call consumes all the pieces of the left subtree, markers included, and stops exactly where the right subtree begins. - Right subtree:
node.right = Read(). - Return
node.
The solution in C#
class Codec {
List<string> values;
int pos;
void Write(TreeNode node) {
if (node == null) {
values.Add("#");
return;
}
values.Add(node.val.ToString());
Write(node.left);
Write(node.right);
}
public string Serialize(TreeNode root) {
values = new List<string>();
Write(root);
return string.Join(",", values);
}
TreeNode Read() {
string value = values[pos];
pos++;
if (value == "#") {
return null;
}
TreeNode node = new TreeNode(int.Parse(value));
node.left = Read();
node.right = Read();
return node;
}
public TreeNode Deserialize(string data) {
values = new List<string>(data.Split(','));
pos = 0;
return Read();
}
}
C# details worth knowing:
- Method names are PascalCase. In C# the public methods are
SerializeandDeserialize. Code ported from other languages often has lowercase names that C# would not match. string.Join(",", values)takes a separator and any sequence of strings, such as aList<string>. For the empty tree,valuesholds just"#", so the result is#.Split(',')takes acharand returns astring[]. TheList<string>constructor accepts that array (anyIEnumerable<string>) and copies it into a list.int.Parseunderstands a leading minus sign, so"-100"becomes-100. It throws aFormatExceptionon text that isn't a number; here it only sees pieces thatSerializewrote.- Fields without a modifier are private.
values,pos,WriteandReadare private toCodec, and onlySerializeandDeserializearepublic. - Deep recursion. Each call of
WriteorReadhandles one node, so the stack is as deep as the tree is tall. The lesson limits depth to 400 levels, which is safe.
Complexity
Time: O(n) for both methods, where n is the number of nodes. Write is called once per node plus once per missing child, which is 2n + 1 calls in all, and each adds one piece. Read consumes each of those pieces exactly once. Joining and splitting the string also take time proportional to its length.
Space: O(n). The list and the string both hold about 2n + 1 pieces, plus the recursion stack, whose depth is the height of the tree.
Edge cases and common mistakes
- The empty tree.
Serialize(null)returns#andDeserialize("#")returnsnull. Returning an empty string instead would makeSplitproduce one empty piece, andint.Parse("")would throw. - A single node. A tree with only the value 7 becomes
7,#,#. - Negative and multi-digit values. The tree
[-1,0,null,null,-100]becomes-1,0,#,-100,#,#,#and comes back unchanged. Without a separator,-1and-100could not be told apart from other digit runs. - Leaving out the
#markers. A preorder list of values alone does not identify a tree, because different shapes give the same list. - Not resetting
pos.Deserializesetspos = 0before reading. Forgetting this would break any second use of the sameCodecobject. - Using the same
valueslist for both directions without rebuilding it. Each method starts by assigning a fresh list, so a leftover list from an earlier call can't leak in.
Testing
Serializing the tree below with one Codec, rebuilding it with another, and printing it level by level gives:
tree [1,2,3,null,null,4,5]
string: 1,2,#,#,3,4,#,#,5,#,#
rebuilt: [1,2,3,null,null,4,5]
tree [-1,0,null,null,-100]
string: -1,0,#,-100,#,#,#
rebuilt: [-1,0,null,null,-100]
empty tree
string: #
rebuilt: []
tree [7]
string: 7,#,#
