Start learning free

LESSON 423 OF 541 · BINARY TREES

Serialize and Deserialize Binary Tree in Swift: Preorder in O(n)

Practice O(n) Read the full explanation

Serializing a tree turns it into a string, so it can be saved in a file or sent over a network. Deserializing rebuilds the same tree from that string.

Implement the class Codec:

  • serialize(root) gets the root of a binary tree, which may be empty, and returns a string.
  • deserialize(data) gets a string made by serialize and returns the root of the same tree: the same shape, with the same value in every node.

The string's format is up to you. The tests serialize each tree with one Codec, rebuild it with a second Codec's deserialize, and print the rebuilt tree level by level, from left to right, with null where a child is missing and the trailing nulls left out. The trees below are written the same way. serialize must return a string. The starter code spells these names the way your language does.

 

Example 1:

Input: root = [1,2,3,null,null,4,5]
Output: [1,2,3,null,null,4,5]
    1
   / \
  2   3
     / \
    4   5

Explanation: One string that works lists the nodes in preorder, with # for a missing child: "1,2,#,#,3,4,#,#,5,#,#". Reading it back gives the same tree.

Example 2:

Input: root = []
Output: []
Explanation: The empty tree needs a string too, for example "#", and deserialize gives back an empty tree.

Example 3:

Input: root = [-1,0,null,null,-100]
Output: [-1,0,null,null,-100]
    -1
    /
   0
    \
   -100

Explanation: Values can be negative and have several digits. With commas between the pieces, as in "-1,0,#,-100,#,#,#", every value is read back whole.

 

Constraints:

  • The number of nodes in the tree is in the range [0, 2 * 105].
  • Each node value is between -1000 and 1000.
  • The tree is at most 400 levels deep.

Note:

Both serialize and deserialize should run in O(n) time, where n is the number of nodes.

STEP 1 OF 21
    Ctrl + Enter to check

    SWIFT · BINARY TREES

    Serialize And Deserialize Binary Tree in Swift, explained

    Problem overview

    The class Codec has two methods. serialize(_ root: TreeNode?) turns a binary tree, possibly empty, into a String. deserialize(_ data: String) takes such a string and returns the root of the same tree, with the same shape and the same value in every node. The format is up to you. The lesson provides the TreeNode class, with val, left, right and init(_ x: Int).

    The tests serialize a tree with one Codec and rebuild it with another, so the string has to carry everything. For [1,2,3,null,null,4,5] this lesson's format produces 1,2,#,#,3,4,#,#,5,#,#, and reading it back gives the same tree.

    Approach: preorder with a marker for every missing child

    Values alone don't determine a tree: the same preorder list fits many shapes. The fix is to write a marker, #, for every missing child. Then the string says exactly where each subtree stops. A node's piece, followed by everything under its left child, followed by everything under its right child, forms a self-delimiting unit, and the reader never needs to guess.

    The reader mirrors the writer. It takes the next piece. A # means an empty subtree. A number means a node, and its left subtree is whatever the next recursive read consumes, followed by its right subtree. Every piece is written once and read once, so both directions run in O(n).

    Commas separate the pieces because values can be negative and have several digits: without separators, 12 followed by 3 would be read as one number.

    Step by step

    1. The list of pieces. A property values, an empty [String], is shared by both methods and their helpers.
    2. Start fresh. serialize sets values to [], so each call builds its own list.
    3. Write the tree. It calls the helper write on root, which appends the pieces of the whole tree in preorder.
    4. Glue the pieces. values.joined(separator: ",") returns one string with a comma between neighbours.
    5. The helper write. It takes a TreeNode? and returns nothing.
    6. Missing child. guard let node = node fails for nil. The else block appends "#" and returns, since nothing is under an empty child.
    7. The node. After the guard, append String(node.val): the node comes before its subtrees.
    8. Children. Call write on node.left, then on node.right.
    9. The read position. A second property, pos, starting at 0, is the index of the next piece to read.
    10. Split the string. In deserialize, data.split(separator: ",") cuts at the commas, and .map { String($0) } turns each part into a String. Store it in values.
    11. Reset the position to 0 and return what read() gives.
    12. The helper read. It returns a TreeNode?: it reads one whole subtree that starts at pos.
    13. Take a piece into value and move pos forward.
    14. Empty subtree. If value is "#", return nil.
    15. Otherwise make a node from Int(value)!, then set node.left and node.right with two calls to read(), in that order, and return the node.

    The solution in Swift

    class Codec {
        var values = [String]()
        var pos = 0
    
        func write(_ node: TreeNode?) {
            guard let node = node else {
                values.append("#")
                return
            }
            values.append(String(node.val))
            write(node.left)
            write(node.right)
        }
    
        func serialize(_ root: TreeNode?) -> String {
            values = []
            write(root)
            return values.joined(separator: ",")
        }
    
        func read() -> TreeNode? {
            let value = values[pos]
            pos += 1
            if value == "#" {
                return nil
            }
            let node = TreeNode(Int(value)!)
            node.left = read()
            node.right = read()
            return node
        }
    
        func deserialize(_ data: String) -> TreeNode? {
            values = data.split(separator: ",").map { String($0) }
            pos = 0
            return read()
        }
    }
    

    The Swift details worth a closer look:

    • split returns substrings. data.split(separator: ",") gives an array of Substring, not String, which is why each part goes through String($0). By default split leaves out empty parts; the format never has an empty piece, so that doesn't matter here.
    • Int(value)!. Int(_:) on a string returns an optional, nil when the text isn't a number. A leading minus sign is fine: Int("-100") is -100. The ! is safe because the code reaches it only for pieces that serialize wrote as numbers.
    • Order of evaluation. In read, node.left = read() runs before node.right = read(), and each call moves the shared pos. That order has to match the order in write.
    • guard and early return. The else of a guard must leave the scope, so the return there is required, and it prevents the code below from touching a missing node.
    • State in properties. values and pos are properties of a class, so the recursive helpers share them without passing anything around. serialize and deserialize both reset what they use, so one Codec can be used for several trees.

    Complexity

    Time: O(n) for each method. write visits each node once and each missing child once, which is at most 2n + 1 pieces. joined and split each pass over the text once, and read consumes each piece once.

    Space: O(n). The list of pieces and the string both grow with the number of nodes. The recursion is as deep as the tree, at most 400 levels in this lesson.

    Edge cases and common mistakes

    • The empty tree. serialize(nil) returns "#", and deserialize("#") returns nil. The same code handles it, with no special case.
    • Leaving out the markers. Preorder values alone can't tell [1,2] with 2 on the left from 2 on the right. Writing # for every missing child is what makes the string unambiguous.
    • No separator. Without commas, the pieces 12 and 3 can't be told from 1 and 23. Negative values need their - kept inside the piece.
    • Reading in a different order than writing. If write does node, left, right, then read must build the node, then the left subtree, then the right subtree.
    • Forgetting to reset pos. A pos left over from a previous call would start reading in the middle of the new list.

    Testing

    Serializing each tree and reading it back gives:

    • [1,2,3,null,null,4,5] becomes 1,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.
    • [1] becomes 1,#,#.
    • [1,2,null,3] becomes 1,2,3,#,#,#,# and comes back unchanged.
    • [5,null,6,null,7] becomes 5,#,6,#,7,#,# and comes back unchanged.
    Keep going with Swift500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free