Problem overview
The class Codec has two methods. serialize(root) takes the root of a binary tree, which may be empty, and returns a string. deserialize(data) takes a string made by serialize and returns the root of the same tree: same shape, same value in every node. The format of the string is up to you. The lesson provides TreeNode, a struct with an int val, pointers left and right, and a constructor TreeNode(int x) that sets both pointers to NULL.
The tests serialize a tree with one Codec, rebuild it with a second Codec, and print the result level by level, with null for a missing child. For root = [1,2,3,null,null,4,5], the rebuilt tree prints as [1,2,3,null,null,4,5].
Approach: preorder with a marker for every missing child
A list of values alone does not identify a tree: the values 1, 2, 3 could be a root with two children or a chain of three nodes. The string also has to say where each subtree ends.
The lesson writes the tree in preorder (a node, then its left subtree, then its right subtree) and writes # for every missing child. Because each empty child leaves a marker, there is only one tree that fits the string. A reader that follows the same preorder knows after every node that its left subtree comes next, and that a # ends a subtree. Commas separate the pieces, so a value such as -100 is read back whole.
For the tree in the first example, the string is 1,2,#,#,3,4,#,#,5,#,#. An empty tree is the single piece #.
Both directions visit each node once, so each runs in O(n). Another common format is level order, as the tests print, but the recursive preorder version is shorter to write.
Step by step
The lesson builds serialize first, then deserialize. Both work on a list of pieces kept as a member of the class.
- Declare the list. The member
vector<string> valuesholds the pieces. Every method of the class sees it, so the helpers can add to it and read from it. - Clear it.
serializestarts withvalues.clear(), because each call builds its pieces from scratch. - Call
write(root). This helper, written next, adds the pieces of the whole tree tovaluesin preorder. - Glue the pieces with commas. Start a
string datawithvalues[0]. There is always a first piece, since even the empty tree is written as#. - Loop over the rest with
ifrom 1 whilei < values.size(). Starting at 1 puts a comma between pieces, never before the first or after the last. - Append a comma and
values[i]todatawith+=. - Return
data. - Declare
void write(TreeNode* node). It adds the pieces of the subtree undernode. - Mark a missing child. If
node == NULL,push_back("#"). Writing every empty child is what makes the string rebuildable. - Return right after the marker. An empty child has nothing under it.
- Write the node's value as text with
to_string(node->val). The node comes first: that is preorder. - Write the left subtree by calling
write(node->left). - Write the right subtree with
write(node->right). - Add the read position. A second member,
int pos, is the index of the next piecedeserializereads. - Clear
valuesagain at the start ofdeserialize. It will hold the pieces ofdata. - Prepare the split. C++ strings have no split method, so create a
stringstream stream(data)and astring piece. - Read the pieces. The loop
while (getline(stream, piece, ','))reads up to the next comma intopieceand ends when nothing is left. - Store each piece with
values.push_back(piece). - Set
pos = 0, the first piece. - Return
read(), a helper that reads one whole subtree starting atpos. Called here, it reads the whole tree. - Declare
TreeNode* read(). It mirrorswrite: it reads pieces in the order they were written. - Take the next piece into
string valueand add 1 topos. Each piece is read exactly once. - A
#means an empty subtree. Ifvalue == "#", returnNULL. - Otherwise make a node with
new TreeNode(stoi(value)), kept innode. The minus sign belongs to the piece, so negative values come back. - Read the left subtree:
node->left = read(). This call consumes all the pieces of the left subtree, markers included, and stops where the right subtree begins. - Read the right subtree the same way.
- Return
node.
The solution in C++
class Codec {
public:
vector<string> values;
int pos;
void write(TreeNode* node) {
if (node == NULL) {
values.push_back("#");
return;
}
values.push_back(to_string(node->val));
write(node->left);
write(node->right);
}
string serialize(TreeNode* root) {
values.clear();
write(root);
string data = values[0];
for (int i = 1; i < values.size(); i++) {
data += "," + values[i];
}
return data;
}
TreeNode* read() {
string value = values[pos];
pos++;
if (value == "#") {
return NULL;
}
TreeNode* node = new TreeNode(stoi(value));
node->left = read();
node->right = read();
return node;
}
TreeNode* deserialize(string data) {
values.clear();
stringstream stream(data);
string piece;
while (getline(stream, piece, ',')) {
values.push_back(piece);
}
pos = 0;
return read();
}
};
Round trips through the code give these results, with the string on the left and the tree rebuilt from it on the right:
1,2,#,#,3,4,#,#,5,#,# -> [1,2,3,null,null,4,5]
# -> []
-1,0,#,-100,#,#,# -> [-1,0,null,null,-100]
7,#,# -> [7]
1,#,2,#,3,#,# -> [1,null,2,null,3]
The C++ details worth a closer look:
- Numbers and text.
to_stringturns anintinto astring, andstoiturns a string back into anint, including a leading minus sign.stoithrows an exception when the text is not a number, which can't happen with a string produced byserialize. - Building the comma piece. In
"," + values[i], the left side is aconst char*and the right side astd::string, and the+operator for that pair makes a newstring. Two string literals added together, such as"," + "#", would not compile. stringstreamandgetline. Astringstreamreads from a string the waycinreads from the keyboard.getlinewith a third argument stops at that character instead of the end of the line. The comma is discarded, and the stream becomes false after the last piece. A string with no trailing comma still yields its last piece.- Members as shared state.
valuesandposlet the recursive helpers share one list and one position without passing them around. Becauseserializeanddeserializeboth clearvaluesfirst, oneCodeccan be used for many trees, one after another. Using it from two threads at once would not be safe. poshas no initial value. The member is not set untildeserializeassigns 0, which happens beforeread()runs.read()is not meant to be called on its own.NULLand the==on text.value == "#"compares the contents of two strings, not their addresses, becausevalueis astd::string.
Complexity
Time: O(n). write is called once for each of the n nodes and once for each of the n + 1 empty children, with constant work per call. Joining the pieces appends each one to the end of data once, which is O(n) overall for these short pieces. deserialize splits the string into the same number of pieces, and read consumes each piece once.
Space: O(n). The list holds 2n + 1 pieces and the string has a similar size. The recursion adds a stack as deep as the tree is tall, up to 400 levels in the lesson's inputs.
Edge cases and common mistakes
- The empty tree.
serialize(NULL)returns#, anddeserialize("#")returnsNULL. Skipping the marker for an empty tree would leavevalues[0]reading from an empty vector. - Negative and multi-digit values. Without separators,
-1followed by0could not be told apart from other splits. The commas keep each value whole, so-1,0,#,-100,#,#,#reads back exactly. - Leaving out the markers. A preorder list of values alone fits many trees. The markers are what pin the shape down.
- Reading in a different order.
readmust take the left subtree before the right one, becausewritewrote them in that order. - Not returning after the marker. If
writekept going after pushing#for a null node, it would usenode->valon a null pointer. - Splitting on a different character. The separator in
getlinehas to match the one used when gluing the pieces, here a comma.
Testing
Each pair shows the tree, the string that serialize produces, and the tree deserialize returns.
[1,2,3,null,null,4,5]gives1,2,#,#,3,4,#,#,5,#,#and back to[1,2,3,null,null,4,5].[]gives#and back to[].[-1,0,null,null,-100]gives-1,0,#,-100,#,#,#and back to the same tree.[7]gives7,#,#.[1,2,3]gives1,2,#,#,3,#,#.[1,null,2,null,3]gives1,#,2,#,3,#,#.
