Problem overview
A Codec turns a binary tree into a string and back. serialize takes the root of a tree, which may be empty, and returns a String. deserialize takes a string that serialize made 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 TreeNode, with the fields val, left and right, and TreeNode::new(val), which makes a node with no children. The tests serialize with one Codec and rebuild with a second one, so a Codec must not rely on anything it remembers from its own serialize call.
For the tree [1,2,3,null,null,4,5] the reference solution produces the string 1,2,#,#,3,4,#,#,5,#,#, and reading it back gives [1,2,3,null,null,4,5]. The empty tree becomes #, and [-1,0,null,null,-100] becomes -1,0,#,-100,#,#,#.
Approach: preorder with a marker for every missing child
A traversal on its own doesn't identify a tree: the values 1, 2 could be a left child or a right child. The fix is to write down the gaps too. Walk the tree in preorder, a node, then its left subtree, then its right subtree, and write # for every missing child. Now the string says exactly where each subtree ends.
Reading it back is the same walk in reverse. Take the next piece. If it is #, this subtree is empty. Otherwise it is a node's value: create the node, read its whole left subtree, then its whole right subtree. Each recursive call consumes exactly the pieces of one subtree, so when the left call returns, the position is right where the right subtree begins.
Commas between the pieces keep multi-digit and negative values whole: -100 is one piece, not four characters. Every piece is written once and read once, so both methods run in O(n).
Step by step
- Declare the struct's fields.
valuesis aVec<String>, the pieces of the string, shared by both methods. - Add
pos, ausize, the position of the next piece thatdeserializereads. newreturns aCodecwith an emptyVec::new()andposset to 0, written as an expression with no semicolon.serializestarts with a fresh list, so each call builds its pieces from scratch.- Call
self.write(&root). The helper only reads the tree, so lending it is enough. - Return
self.values.join(","), gluing the pieces into one string with commas between them. - Declare
writeasfn write(&mut self, node: &Option<Box<TreeNode>>). It changesselfbut only borrows the tree. - Match on
node. Amatchmust cover both cases of theOption. - The
Nonearm pushes"#"as aString. - The
Some(node)arm gets a block. - Push the node's value with
node.val.to_string(). The node comes first: that is preorder. - Write the left subtree with
self.write(&node.left). - Write the right subtree the same way.
- In
deserialize, split the input withdata.split(','), turn each part into aStringand collect them intoself.values. - Reset
posto 0, the first piece. - Return
self.read(), which rebuilds the whole tree. - Declare
readasfn read(&mut self) -> Option<Box<TreeNode>>. It mirrorswrite. - Copy the next piece with
self.values[self.pos].clone()intovalue, and then add 1 toself.pos. The clone meansvaluedoesn't borrow fromselfwhile the recursive calls change it. - If
valueis"#", returnNone: an empty subtree. - Otherwise create the node from
value.parse().unwrap(), boxed, declaredmut. - Set
node.lefttoself.read(). That call takes every piece of the left subtree, markers included. - Set
node.rightthe same way. Nowposis just past this node's whole subtree. - Return
Some(node).
The solution in Rust
struct Codec {
values: Vec<String>,
pos: usize,
}
impl Codec {
fn new() -> Self {
Codec { values: Vec::new(), pos: 0 }
}
fn write(&mut self, node: &Option<Box<TreeNode>>) {
match node {
None => self.values.push(String::from("#")),
Some(node) => {
self.values.push(node.val.to_string());
self.write(&node.left);
self.write(&node.right);
}
}
}
fn serialize(&mut self, root: Option<Box<TreeNode>>) -> String {
self.values = Vec::new();
self.write(&root);
self.values.join(",")
}
fn read(&mut self) -> Option<Box<TreeNode>> {
let value = self.values[self.pos].clone();
self.pos += 1;
if value == "#" {
return None;
}
let mut node = Box::new(TreeNode::new(value.parse().unwrap()));
node.left = self.read();
node.right = self.read();
Some(node)
}
fn deserialize(&mut self, data: String) -> Option<Box<TreeNode>> {
self.values = data.split(',').map(|piece| piece.to_string()).collect();
self.pos = 0;
self.read()
}
}
Rust details worth a closer look:
- Methods that change the codec.
writeandreadtake&mut selfbecause they push tovaluesor advancepos. The recursive calls are alsoself.write(...)andself.read(), with no extra borrowing to manage. - Why
clone().self.values[self.pos]is aStringinside the vector, and it can't be moved out of the index. Cloning gives an owned copy, andselfis free to change afterwards. parse()and its type.value.parse().unwrap()has no annotation. Rust works out fromTreeNode::new, which takes ani32, that it should parse ani32.unwrappanics if the text isn't a number, which can't happen with textserializewrote: it contains only numbers and#.- Comparing a
Stringwith a literal.value == "#"compares the contents, so it works without converting either side. split(','). It takes acharhere and returns an iterator over&strparts. Themapturns each into an ownedString, andcollect()builds theVec<String>becauseself.valueshas that type.- Last expressions.
self.values.join(","),self.read(),Some(node)and the struct innewhave no semicolon, so they are the values returned. - Taking ownership of the tree.
serializereceivesrootby value and lends it with&root. The tree is dropped when the method ends, which is fine because the string holds everything.
Complexity
Time: O(n) for both methods. serialize visits each node once and each missing child once, which is about 2n + 1 pieces, then joins them. deserialize splits the string once and reads each piece once.
Space: O(n). The list of pieces and the string are linear in the number of nodes, and the recursion is as deep as the tree, at most 400 levels in this lesson.
Edge cases and common mistakes
- The empty tree.
writepushes one#, so the string is#, andreadreturnsNoneat once. The string is never empty, so splitting it always gives at least one piece. - Skipping the markers. Without
#, the tree1with left child2and the tree1with right child2would both give1,2. The markers are what make the string unambiguous. - No separator. Without commas,
-100can't be told apart from the pieces-1,0,0, and12from1followed by2. - Stale state.
serializeresetsvalues, anddeserializeoverwritesvaluesand resetspos. A codec used twice would otherwise append to the old pieces or start reading in the middle. - Reading in a different order.
readhas to take the left subtree before the right one, the orderwriteused. Swapping the two assignments would rebuild a mirrored tree. - Negative values. The minus sign is part of the piece, so
"-1".parse::<i32>()reads it back correctly.
Testing
Serializing each tree with one Codec and rebuilding it with another gives:
[1,2,3,null,null,4,5]becomes1,2,#,#,3,4,#,#,5,#,#and comes back as[1,2,3,null,null,4,5].[]becomes#and comes back as[].[-1,0,null,null,-100]becomes-1,0,#,-100,#,#,#and comes back unchanged.[7]becomes7,#,#.[1,2,null,3]becomes1,2,3,#,#,#,#and comes back as[1,2,null,3].
