Problem overview
The class Codec has two methods. serialize(root) takes the root of a binary tree, which may be empty (None), and returns a string. deserialize(data) takes a string made by serialize and returns the root of the same tree, with the same shape and the same value in every node. The format of the string is up to you. The lesson provides the TreeNode class: TreeNode(x) sets val to x and left and right to None.
The tests serialize a tree with one Codec and rebuild it with the deserialize of a second one, so the string has to carry everything. Nothing may be remembered in the object between the two calls.
Approach: preorder with markers for missing children
A list of values alone is not enough to rebuild a tree, because many different trees share the same preorder list. The fix is to write down the empty children too. Walk the tree in preorder (a node, then its left subtree, then its right subtree), write # for every None, and put a comma between the pieces. The tree with root 1 and children 2 and 3 becomes 1,2,#,#,3,#,#.
The markers say exactly where each subtree ends: after a node's piece, the reader takes one whole subtree for its left child, then one whole subtree for its right child. A # is the smallest possible subtree, an empty one. This makes the string enough to rebuild the tree with a recursion that mirrors the one that wrote it.
The commas matter because values can have several digits and a minus sign. Without them, 1 followed by 2 and the single value 12 would write the same text. With them, split(",") cuts the string back into the original pieces.
For the second example's tree, [1,2,3,null,null,4,5], the string is 1,2,#,#,3,4,#,#,5,#,#. The empty tree is the string #. The tree [-1,0,null,null,-100] gives -1,0,#,-100,#,#,#.
Step by step
Writing the tree
- Start a list of pieces in
serializeas an attribute,self.values = [], so the helper can add to it. - Call the helper
self.write(root). It appends the pieces of the whole tree in preorder. - Join the pieces into one string with
",".join(self.values). - Declare the helper
write(self, node). It adds the pieces of the subtree undernode. - Mark a missing child. If
nodeisNone, append"#". - Return right after the marker: an empty child has nothing under it.
- Write the node's value as text,
str(node.val). The node comes first, which is what makes it preorder. - Write the left subtree by calling
self.write(node.left). - Write the right subtree by calling
self.write(node.right).
Reading it back
- Cut the string into pieces with
data.split(",")and store the list inself.values. - Start a position counter
self.posat 0, the first piece. - Return what
self.read()gives. Called here, it reads one whole subtree, which is the whole tree. - Declare the helper
read(self). It reads the subtree that starts atself.posand returns its root. - Take the next piece as
value = self.values[self.pos]and moveself.posforward by one. Each piece is read exactly once. - An empty child. If
valueis"#", returnNone. - A real node. Otherwise create
TreeNode(int(value)).intunderstands a leading minus sign, so negative values come back whole. - Read the left subtree with
node.left = self.read(). It consumes all the pieces of the left subtree, markers included, and stops where the right subtree begins. - Read the right subtree with
node.right = self.read(). Nowself.posis just past this node's whole subtree. - Return the node.
The solution in Python
class Codec:
def write(self, node):
if node is None:
self.values.append("#")
return
self.values.append(str(node.val))
self.write(node.left)
self.write(node.right)
def serialize(self, root: Optional[TreeNode]) -> str:
self.values = []
self.write(root)
return ",".join(self.values)
def read(self):
value = self.values[self.pos]
self.pos += 1
if value == "#":
return None
node = TreeNode(int(value))
node.left = self.read()
node.right = self.read()
return node
def deserialize(self, data: str) -> Optional[TreeNode]:
self.values = data.split(",")
self.pos = 0
return self.read()
Python details worth a closer look:
joinneeds strings.",".join(self.values)raisesTypeErrorif the list holds integers, which is whywriteappendsstr(node.val)instead ofnode.val.joinandsplitare opposites.",".join(["1", "2", "#"])gives"1,2,#", and"1,2,#".split(",")gives['1', '2', '#']. For"#",splitreturns the list['#'], so the empty tree reads back asNonewithout a special case.- One attribute, two meanings.
self.valuesholds the pieces being written inserializeand the pieces being read indeserialize. Each method starts by assigning it fresh, so reusing oneCodecobject for several trees works: callingserializeon[1,2]and then on[3]gives1,2,#,#,#and3,#,#. - The counter lives on
self.self.posadvances across all the recursive calls ofread. A local variable would reset in every call and the reads would overlap. is None. Inwrite,node is Noneis the idiomatic test for a missing child.- Recursion depth. Trees here are at most 400 levels deep, within Python's default recursion limit of 1,000.
Complexity
Time: O(n) for both methods, where n is the number of nodes. A tree with n nodes has n + 1 empty children, so the string has 2n + 1 pieces. write and read each handle every piece once with constant work, and join and split are linear in the length of the text.
Space: O(n). The list of pieces and the string both grow with n, and the recursion stack is as deep as the tree is tall.
Edge cases and common mistakes
- The empty tree. It needs a string too:
"#". Returning""would breakdeserialize, because"".split(",")is[''], and that piece is neither"#"nor a number. - Leaving out the missing-child markers. Then
[1,2](2 on the left) and[1,null,2](2 on the right) would both be written as1,2, and the reader could not tell them apart. - Leaving out the commas. Values such as
-100or12would run together with their neighbours. - Reading the left before the right. The two reads in
readmust stay in the same order as the two writes inwrite. Swapping only one of them rebuilds a mirrored or broken tree. - Passing data between objects.
deserializemust rely ondataalone. It cannot readself.valuesfrom an earlierserialize, because the tests use a differentCodecfor it.
Testing
For each tree, serialize gave the string shown, and deserialize on that string gave the same tree back:
[1,2,3,null,null,4,5]gives1,2,#,#,3,4,#,#,5,#,#.[]gives#.[-1,0,null,null,-100]gives-1,0,#,-100,#,#,#.[7]gives7,#,#.[1,2]gives1,2,#,#,#.
