Problem overview
You implement a class Codec with two methods. serialize(root) turns a binary tree, possibly empty, into 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 your choice.
The lesson provides TreeNode: a class with an Int property value and two nullable properties, left and right. In the lesson's tests one Codec serializes the tree and a second, separate Codec rebuilds it. So the string has to carry everything: the second object knows nothing but data.
For the tree [1,2,3,null,null,4,5] the solution below produces 1,2,#,#,3,4,#,#,5,#,#, and deserialize turns that back into [1,2,3,null,null,4,5].
Approach
A list of the values in preorder (node, left subtree, right subtree) is not enough on its own: [1,2] could be a root with a left child or a root with a right child. The fix is to write a marker, here #, for every missing child. With the markers, the end of every subtree is visible in the text, so the string describes exactly one tree.
Reading the string back mirrors writing it. Take the next piece. If it is #, this subtree is empty. Otherwise it is a node's value: create the node, then read its left subtree, which consumes the pieces right after it, then read its right subtree. Each recursive read takes exactly the pieces of one subtree and stops where the next one begins, so a single position counter that only moves forward is all the bookkeeping needed.
Commas separate the pieces. They matter because values have several digits and can be negative: without a separator, the pieces 1, 0, 0 and the single value 100 would look the same. With commas, every value is read back whole.
Step by step
Writing the string
- Keep the pieces. Add a property
var values = mutableListOf<String>(). Both methods and both helpers use it. serialize. Start with a new empty list, call the helperwrite(root), and returnvalues.joinToString(","), which glues the pieces together with a comma between each two. Resetting the list at the start means every call toserializebuilds its string from scratch.- The helper
write(node). Ifnodeisnull, add"#"and return: an empty child has nothing below it. - Otherwise add
node.value.toString(), then callwriteonnode.leftand onnode.right, in that order. The node comes first, which is what makes it preorder.
Reading it back
- Keep the position. Add a property
var pos = 0: the index of the next piece to read. deserialize. Cut the string into pieces withdata.split(","), convert to the same kind of list withtoMutableList(), store it invalues, setposto 0 and return the result ofread().- The helper
read(). Takevalues[pos]intovalueand moveposforward by one. Ifvalueis"#", returnnull. - Otherwise create
TreeNode(value.toInt()), set itslefttoread()and then itsrighttoread(), and return the node. The order of those two lines must match the order inwrite.
The solution in Kotlin
class Codec {
var values = mutableListOf<String>()
var pos = 0
fun write(node: TreeNode?) {
if (node == null) {
values.add("#")
return
}
values.add(node.value.toString())
write(node.left)
write(node.right)
}
fun serialize(root: TreeNode?): String {
values = mutableListOf()
write(root)
return values.joinToString(",")
}
fun read(): TreeNode? {
val value = values[pos]
pos++
if (value == "#") {
return null
}
val node = TreeNode(value.toInt())
node.left = read()
node.right = read()
return node
}
fun deserialize(data: String): TreeNode? {
values = data.split(",").toMutableList()
pos = 0
return read()
}
}
Kotlin details worth knowing
varproperties.valuesis reassigned in bothserializeanddeserialize, andposchanges while reading, so both arevar. The list type isMutableList<String>: a plainListhas noadd. That is whydeserializecallstoMutableList()on the result ofsplit, which returns a read-onlyList<String>.mutableListOf()with no type. Invalues = mutableListOf()Kotlin infersStringfrom the property's type, so the type argument can be left out there.joinToString(","). It converts each element to text and puts the separator between elements, not after the last one. A list with the single piece"#"gives#, and an empty list gives an empty string.- Negative numbers.
"-1".toInt()is-1, so the minus sign inside a piece needs no special care. For[-1,0,null,null,-100]the string is-1,0,#,-100,#,#,#. - Smart cast on the parameter. After the
node == nullcheck returns, Kotlin treatsnodeas non-null, sonode.valuecompiles without!!. ==on strings.value == "#"compares contents, as Kotlin's==always does. Noequalscall is needed.
Complexity
- Time: O(n) for both methods, where n is the number of nodes.
writeis called once for each node and once for each missing child, which is n + 1 empty children, so about 2n + 1 calls with O(1) work each.joinToStringandspliteach go over a string of that size once.readconsumes each piece exactly once. - Space: O(n) for the list of pieces and the string, plus the recursion stack, as deep as the tree is tall (the constraints keep it at 400 levels).
Edge cases and common mistakes
- The empty tree.
write(null)adds"#", soserializereturns#anddeserialize("#")returnsnull. Returning an empty string for an empty tree would breakdeserialize: the split of an empty string gives one empty piece, which is not#, and"".toInt()fails with aNumberFormatException. - Leaving out the markers. Without
#for missing children, different trees give the same string. - Leaving out the commas. Values like
-100and1000have several characters, so the reader needs separators to cut the string at the right places. - Different order in the two directions. If
readfilledrightbeforeleftwhilewritewrote left first, the subtrees would swap places. - Stale state.
serializestarts with a new list on purpose. Without that line, a second call on the same object would append to the old pieces and return a longer string. - Deep trees. A tree that is a line of 400 nodes writes 801 pieces, and the recursion goes 400 levels deep, which is fine here.
Testing
Each tree below was serialized by one Codec and rebuilt by another. The tree is written in level order, as the lesson's tests print it:
| 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] |
[1] | 1,#,# | [1] |
[1,2,3] | 1,2,#,#,3,#,# | [1,2,3] |
[1000,-1000] | 1000,-1000,#,#,# | [1000,-1000] |
