Problem overview
The class Codec has two methods. serialize(root) takes the root of a binary tree, which may be empty (null), 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: new TreeNode(value) makes a node, and its left and right fields start as null.
The tests serialize a tree with one Codec, rebuild it with a second Codec, and print the result level by level. So the string must carry everything the rebuilding needs. Nothing is shared between the two objects except that string.
Approach: preorder with a marker for every missing child
Listing the values alone isn't enough: many different trees have the same values in the same order. The fix is to write the tree in preorder (a node, then its left subtree, then its right subtree) and to write a marker, here #, for every missing child. The root 1 with children 2 and 3 becomes 1,2,#,#,3,#,#.
The markers tell the reader exactly where each subtree ends. Reading the pieces from left to right, a number starts a node whose left subtree follows immediately, then its right subtree. A # is a complete, empty subtree. The empty tree is the string #.
Commas separate the pieces, so values with several digits or a minus sign, like -100, come back whole. Both methods visit each node once, so both run in O(n).
For the first example, [1,2,3,null,null,4,5], the code produces 1,2,#,#,3,4,#,#,5,#,#, and reading it back gives the same tree.
Step by step
- Declare the shared list. A field
var valuesholds the pieces as anscala.collection.mutable.ArrayBuffer[String](). Fields are visible to every method, so the helpers can add to the list and read from it. - Start fresh in
serialize. Assign a new, empty buffer tovalues, so each call builds its own list. - Call the helper
write(root), which adds the pieces of the whole tree in preorder. - Glue the pieces:
values.mkString(",")puts a comma between each two. - Write
write(node: TreeNode): Unit. It adds the pieces of the subtree undernode. - Missing child. If
nodeisnull, add"#"with+=. - Return early after the marker, since an empty child has nothing under it.
- Add the node. Add
node.value.toString. The node comes first, which is what makes it preorder. - Left subtree. Call
write(node.left). - Right subtree. Call
write(node.right). - Add the reader's position. A second field,
var pos = 0, is the index of the next piecedeserializewill read. - Cut the string. In
deserialize,data.split(",")gives an array of pieces, andArrayBuffer.from(...)turns it into anArrayBufferstored invalues. - Reset
posto 0 and return whatread()gives: one whole subtree starting atpos, which here is the whole tree. - Write
read(): TreeNode, the mirror ofwrite. It takes the piece atposintovalueand movesposforward. - Empty subtree. If
valueis"#", returnnull. - Real node. Otherwise create
new TreeNode(value.toInt). - Children. Set
node.lefttoread(), thennode.righttoread(). The first call consumes all the pieces of the left subtree and stops where the right one begins. - Return the node.
The solution in Scala
class Codec {
var values = scala.collection.mutable.ArrayBuffer[String]()
var pos = 0
def write(node: TreeNode): Unit = {
if (node == null) {
values += "#"
return
}
values += node.value.toString
write(node.left)
write(node.right)
}
def serialize(root: TreeNode): String = {
values = scala.collection.mutable.ArrayBuffer[String]()
write(root)
return values.mkString(",")
}
def read(): TreeNode = {
val value = values(pos)
pos += 1
if (value == "#") {
return null
}
val node = new TreeNode(value.toInt)
node.left = read()
node.right = read()
return node
}
def deserialize(data: String): TreeNode = {
values = scala.collection.mutable.ArrayBuffer.from(data.split(","))
pos = 0
return read()
}
}
Scala details worth knowing:
+=on a buffer.values += "#"appends to anArrayBufferin place. It works onvalueseven though the field is avar, because the buffer is a mutable collection and+=calls its append method.varversusval.valuesis avarbecauseserializeanddeserializeeach replace the whole buffer.posis avarbecause it counts upward.splitreturns anArray[String]. For"1,2,#"the pieces are1,2and#, and for"#"it is the single piece#.ArrayBuffer.fromcopies them into a buffer, which is the type ofvalues.- Converting numbers.
node.value.toStringwrites anIntas text, andvalue.toIntreads it back, including negative numbers such as"-100". - Comparing strings.
value == "#"compares contents in Scala, so no special method is needed. Unit.writereturnsUnit, Scala's "no useful result". The barereturnin it leaves the method early.- State between calls.
deserializesetsvaluesandpositself every time, so the sameCodeccan serialize and deserialize repeatedly, and a secondCodeccan read a string it didn't write.
Complexity
Time: O(n) for each method, 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 one piece per call, and mkString and split go through the pieces once.
Space: O(n). The list of pieces and the string both grow with the tree, and the recursion is as deep as the tree is tall (at most 400 levels in this lesson).
Edge cases and common mistakes
- The empty tree.
serialize(null)gives"#", anddeserialize("#")givesnull. Returning an empty string would breakdeserialize:spliton an empty string gives one empty piece, and converting it withtoIntthrows aNumberFormatException. - Leaving out the markers. The values
[1,2]could then be a left child or a right child. Writing every missing child removes that doubt:[1,2]with 2 on the left is1,2,#,#,#, and with 2 on the right it is1,#,2,#,#. - No separator. Without commas, the pieces
1,0and#would run together as10#, which could equally be the number 10 followed by a marker. The commas keep every value whole. - Reading pieces out of order.
readmust take the left subtree before the right one, in the same orderwriteused. Swapping them in only one of the methods mirrors the tree. - Forgetting to advance
pos. Ifreaddoesn't moveposon every piece, including#, it reads the same piece again and never finishes.
Testing
Each row shows the string produced by serialize and the tree that a second Codec rebuilt from it, printed level by level:
| Tree | Serialized 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] |
[7] | 7,#,# | [7] |
[1,null,2,null,3] | 1,#,2,#,3,#,# | [1,null,2,null,3] |
