Problem overview
The lesson asks for a Codec type with two methods:
serialize(root *TreeNode) stringturns a binary tree, possibly empty, into a string.deserialize(data string) *TreeNodetakes a string made byserializeand rebuilds the same tree: the same shape and the same value in every node.
The string format is yours to choose. The lesson's tests serialize each tree with one Codec, rebuild it with the deserialize method of a second Codec made by Constructor(), and print the result level by level, with null for missing children. The lesson provides the TreeNode struct, with the fields Val, Left and Right.
For the tree [1,2,3,null,null,4,5], one string that works is 1,2,#,#,3,4,#,#,5,#,#. For the empty tree, the string # is enough, and deserialize gives back an empty tree. Values can be negative or have several digits, such as -100, so the pieces need a separator between them.
Approach: preorder with a marker for every missing child
A list of values alone does not describe a tree: [1,2] could be a node with a left child or with a right child. The fix is to write down the missing children too. The format of this lesson visits the tree in preorder, a node, then its left subtree, then its right subtree, and writes # wherever a child is missing. Pieces are separated by commas.
The markers are what make reading it back unambiguous. When the reader takes a piece, it knows what to do:
- A
#is a whole empty subtree. Nothing more belongs to it. - A number is a node. The pieces that follow are first its entire left subtree, then its entire right subtree, and each of those is read the same way, recursively.
Because each subtree consumes exactly its own pieces, the reader always knows where the left subtree stops and the right one begins. The reading mirrors the writing, and each piece is visited once, so both methods run in O(n), as the statement requires.
The Codec keeps its working data in struct fields: the slice of pieces and the position of the next piece to read. The helpers write and read are methods that use those fields, so they do not need extra parameters.
Step by step
- Declare the first field.
valuesis a[]string: the list of pieces, used by both directions. - Write
Constructor. Return an emptyCodec{}. Every field starts at its zero value, an empty slice and 0, so nothing else needs setting. - Start
serializeclean. Setthis.valuesto an empty[]string{}, so each call builds its list from scratch. - Call the helper
this.write(root). It appends the pieces of the whole tree tothis.valuesin preorder. - Glue the pieces. Return
strings.Join(this.values, ","), which joins the slice with a comma between each two pieces. - Declare
writeas a method:func (this *Codec) write(node *TreeNode). It writes the subtree undernode. - Write the marker for a missing child. If
nodeisnil, append"#"tothis.values.appendreturns the longer slice, so the result is assigned back tothis.values. - Return right after the marker. An empty child has nothing under it.
- Write the node's value. Append
strconv.Itoa(node.Val), which converts theintto a string. The node comes first: that is preorder. - Write the left subtree with
this.write(node.Left). - Write the right subtree with
this.write(node.Right). - Add the second field.
posis anint: the index of the next piece fordeserializeto read. - Cut the string back into pieces. In
deserialize,strings.Split(data, ",")returns a slice of the strings between the commas. Store it inthis.values. - Reset the position with
this.pos = 0. The first piece is the root, or#for an empty tree. - Return what
this.read()gives. One call reads one whole subtree, which here is the whole tree. - Declare
readasfunc (this *Codec) read() *TreeNode. It reads the subtree that starts atthis.pos. - Take the next piece into
valueand movethis.posforward by one. - Stop on
#. Ifvalueis"#", returnnil. - Convert a number.
strconv.Atoi(value)returns the number and an error. The pieces were produced byserialize, so the error is ignored with_. Then create the node with that value asVal. - Read the left subtree with
node.Left = this.read(). The call consumes all the pieces of the left subtree, markers included. - Read the right subtree the same way into
node.Right. - Return the node.
The solution in Go
type Codec struct {
values []string
pos int
}
func Constructor() Codec {
return Codec{}
}
func (this *Codec) serialize(root *TreeNode) string {
this.values = []string{}
this.write(root)
return strings.Join(this.values, ",")
}
func (this *Codec) write(node *TreeNode) {
if node == nil {
this.values = append(this.values, "#")
return
}
this.values = append(this.values, strconv.Itoa(node.Val))
this.write(node.Left)
this.write(node.Right)
}
func (this *Codec) deserialize(data string) *TreeNode {
this.values = strings.Split(data, ",")
this.pos = 0
return this.read()
}
func (this *Codec) read() *TreeNode {
value := this.values[this.pos]
this.pos++
if value == "#" {
return nil
}
number, _ := strconv.Atoi(value)
node := &TreeNode{Val: number}
node.Left = this.read()
node.Right = this.read()
return node
}
The Go details worth knowing:
- Methods on a struct. Go has no classes.
Codecis a struct, andfunc (this *Codec) write(...)attaches a method to it. The receiver is a pointer,*Codec, so the method changes the caller's fields. With a value receiver, the changes tovaluesandposwould be lost when the method returns. The namethisis only a convention; any name works. appendand reassignment.appendmay allocate a new, bigger array, so it returns the slice to use from then on. Writing justappend(this.values, "#")and discarding the result would be a compile error, because the result ofappendmust be used.- Zero values.
Codec{}givesvaluesas anilslice andposas 0. Anilslice works withappend, andserializereplaces it anyway. strconv.Itoaandstrconv.Atoi.Itoaturns anintinto its decimal text, with the minus sign for negatives.Atoigoes the other way and returns(int, error). Converting withstring(node.Val)instead would produce the character with that code point, not the digits.- Why a separator. Without commas, the pieces
1,12and-3could not be told apart when read back as one string.strings.Spliton the comma recovers them exactly. - Shared state.
valuesandposare reused by every call. Both methods reset what they need at the start, so oneCodeccan serialize several trees in a row.
Complexity
Time: O(n) for both methods, where n is the number of nodes. write is called once per node and once per missing child, which is at most 2n + 1 calls in total. strings.Join and strings.Split each pass over the string once. read consumes one piece per call, so it runs the same number of times.
Space: O(n). The slice of pieces holds 2n + 1 strings, and the output string has a similar size. The recursion adds a stack as deep as the tree is tall, up to 400 levels in this lesson. Go grows its stacks as needed, so even a tree that is a chain of 200,000 nodes works.
Edge cases and common mistakes
- The empty tree.
serialize(nil)returns"#".deserialize("#")reads that one piece and returnsnil. Skipping the marker fornilwould make the empty tree produce an empty string, whichstrings.Splitturns into one empty piece, not a#, andAtoiwould fail on it. - 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 whole, because every number is a separate piece. - Left-only versus right-only trees. The tree
[1,2]becomes1,2,#,#,#, while a tree with only a right child 2 becomes1,#,2,#,#. The markers keep these apart. - Forgetting to advance
pos. Every call ofreadmust move forward by one, or the recursion reads the same piece forever. - Using
#as a value. The marker is safe here because node values are numbers, and a number's text never equals#. - Using a value receiver. A method declared on
Codecinstead of*Codecwould work on a copy, soposwould never advance for the caller.
Testing
Each tree below is serialized, then rebuilt with a second Codec.
[1,2,3,null,null,4,5]becomes1,2,#,#,3,4,#,#,5,#,#and is rebuilt as[1,2,3,null,null,4,5].- The empty tree becomes
#and is rebuilt as[]. [-1,0,null,null,-100]becomes-1,0,#,-100,#,#,#and is rebuilt as[-1,0,null,null,-100].[7]becomes7,#,#and is rebuilt as[7].[1,2,3]becomes1,2,#,#,3,#,#and is rebuilt as[1,2,3].[1000,-1000]becomes1000,-1000,#,#,#and is rebuilt as[1000,-1000].
