Skip to main content

Building and Serializing Trees

Editorial

One list loses the shape

A traversal writes a tree down as a list. One list isn't enough to get the tree back: the preorder 1, 2, 3 is a chain going left, a chain going right, a zigzag either way, or a root with two children. Five trees, one list. Two lists can be enough, depending on which two:

Two listsFix the tree?Why
Preorder and inorderYespreorder names each root; the inorder puts its left subtree before it and its right after
Inorder and postorderYespostorder names each root, last; the inorder splits the same way
Preorder and postorderNot alwaysthey name the roots, but nothing says which side a lone child is on
One list with a mark for every missing childYesthe marks say where each subtree ends

Counted on every tree shape of up to 7 nodes: preorder with inorder told all 429 seven-node shapes apart. Preorder with postorder told only 51 apart, and one pair of lists fitted 64 different trees. Everything in this lesson is about what a list keeps:

The preorder 1 2 4 5 7 3 6 above the inorder 4 2 7 5 1 3 6. The 1 is ringed in both: first in the preorder, and in the middle of the inorder, where it splits 4 2 7 5 on its left from 3 6 on its right. The same two blocks are coloured in the preorder, after the 1, and in the tree below: 1 at the top, its left subtree 2, 4, 5, 7 in one colour and its right subtree 3, 6 in the other.

Preorder finds the root, the inorder splits it

Take the tree's preorder [1, 2, 4, 5, 7, 3, 6] and inorder [4, 2, 7, 5, 1, 3, 6]. Preorder lists a root before anything below it, so 1 is the root. In the inorder, everything in 1's left subtree comes before 1 and everything in its right subtree after it: [4, 2, 7, 5] on the left, [3, 6] on the right. The left part has 4 values, so the 4 preorder values after the root, [2, 4, 5, 7], are the left subtree's own preorder, and [3, 6] the right's. Each half is the same problem again.

Searching the inorder for each root costs a pass. A map from each value to its place in the inorder makes it one lookup. And the next root is always the next preorder value, since the left subtree is built before the right, so one index walks the preorder from front to back with no copies:

// Preorder gives each root; its place in the inorder splits the left subtree from the right.
TreeNode* buildTree(const vector<int>& preorder, const vector<int>& inorder) {
    unordered_map<int, int> at;                     // each value's place in the inorder
    for (int k = 0; k < (int)inorder.size(); k++) at[inorder[k]] = k;
    int next = 0;                                   // the next preorder value is the next root
    function<TreeNode*(int, int)> grow = [&](int lo, int hi) -> TreeNode* {
        if (lo > hi) return nullptr;                // an empty part of the inorder: no subtree
        TreeNode* root = new TreeNode(preorder[next++]);
        int k = at[root->val];
        root->left = grow(lo, k - 1);               // what's left of the root in the inorder
        root->right = grow(k + 1, hi);              // and what's right of it
        return root;
    };
    return grow(0, (int)inorder.size() - 1);
}
import sys
sys.setrecursionlimit(10_000)          # a 3,000-node tree that leans one way is 3,000 calls deep


def build_tree(preorder, inorder):
    """Preorder gives each root; its place in the inorder splits the left subtree from the right."""
    at = {v: k for k, v in enumerate(inorder)}      # each value's place in the inorder
    next_root = 0                                   # the next preorder value is the next root

    def grow(lo, hi):
        nonlocal next_root
        if lo > hi:                                 # an empty part of the inorder: no subtree
            return None
        root = TreeNode(preorder[next_root])
        next_root += 1
        k = at[root.val]
        root.left = grow(lo, k - 1)                 # what's left of the root in the inorder
        root.right = grow(k + 1, hi)                # and what's right of it
        return root

    return grow(0, len(inorder) - 1)
// Preorder gives each root; its place in the inorder splits the left subtree from the right.
static int next;                                    // the next preorder value is the next root
static Map<Integer, Integer> at;                    // each value's place in the inorder

static TreeNode buildTree(int[] preorder, int[] inorder) {
    at = new HashMap<>();
    for (int k = 0; k < inorder.length; k++) at.put(inorder[k], k);
    next = 0;
    return grow(preorder, 0, inorder.length - 1);
}

static TreeNode grow(int[] preorder, int lo, int hi) {
    if (lo > hi) return null;                       // an empty part of the inorder: no subtree
    TreeNode root = new TreeNode(preorder[next++]);
    int k = at.get(root.val);
    root.left = grow(preorder, lo, k - 1);          // what's left of the root in the inorder
    root.right = grow(preorder, k + 1, hi);         // and what's right of it
    return root;
}
// Preorder gives each root; its place in the inorder splits the left subtree from the right.
function buildTree(preorder, inorder) {
    const at = new Map(inorder.map((v, k) => [v, k]));     // each value's place in the inorder
    let next = 0;                                         // the next preorder value is the next root
    const grow = (lo, hi) => {
        if (lo > hi) return null;                         // an empty part of the inorder: no subtree
        const root = new TreeNode(preorder[next++]);
        const k = at.get(root.val);
        root.left = grow(lo, k - 1);                      // what's left of the root in the inorder
        root.right = grow(k + 1, hi);                     // and what's right of it
        return root;
    };
    return grow(0, inorder.length - 1);
}

Each printed the tree as LeetCode's list: [3, 9, 20, null, null, 15, 7] for the problem's first example, [-1] for its one-node tree, and [1, 2, 3, 4, 5, null, 6, null, null, 7] for ours. Every node is made once and every lookup is one step: O(n) time, and O(n) for the map. The values must be unique, or a value would have two places in the inorder.

The recursion is as deep as the tree. A 3,000-node tree that leans one way, the problem's largest, is 3,000 calls deep, past Python's limit of about 1,000 (Introduction to Binary Trees measured it), so the Python version raises it.

Without the map, each root is found by searching, and the usual Python for that also slices both lists at every call. On trees that lean left, where each search runs to the end of its part:

NodesSearching, with slicesA map of positions
1,00028.2 ms1.24 ms
2,000112.9 ms2.71 ms
3,000245.5 ms4.29 ms
3,000, a random shape4.8 ms4.19 ms

Both built the same tree every time. Doubling the nodes roughly quadruples the search; on a random tree, shallow and bushy, the two are close. With the map, the leaning tree builds as fast as the bushy one.

Now you split the inorder

Below, you rebuild a tree from two of its lists. The amber values are the part being built, the same values in both lists, and the amber node in the tree is where its root goes. Its root is the first amber value of the preorder: click it in the inorder. Everything left of it there becomes its left subtree, everything right of it its right subtree. A wrong click is refused, with the reason. Then switch to postorder and inorder, where each root is the last amber value.

Split the Inorder: each root from the preorder, each split from its place in the inorder

Inorder and postorder, and the pair that can't

Postorder lists a root after everything below it, so the root is the last value. Read from the back, postorder gives a root, then all of its right subtree, then all of its left. The same split works; the right subtree is built first:

def build_tree(inorder, postorder):
    """Postorder lists each root last. Read from the back, it gives a root, then its right subtree's roots."""
    at = {v: k for k, v in enumerate(inorder)}
    next_root = len(postorder) - 1

    def grow(lo, hi):
        nonlocal next_root
        if lo > hi:
            return None
        root = TreeNode(postorder[next_root])
        next_root -= 1
        k = at[root.val]
        root.right = grow(k + 1, hi)                # the right subtree first: its roots come next
        root.left = grow(lo, k - 1)
        return root

    return grow(0, len(inorder) - 1)

It printed [3, 9, 20, null, null, 15, 7] for Construct Binary Tree from Inorder and Postorder Traversal's first example, and [1, 2, 3, 4, 5, null, 6, null, null, 7] from our tree's inorder and postorder [4, 7, 5, 2, 6, 3, 1].

Preorder with postorder names every root, from both ends, and still can't place a node with one child: in both lists the child comes straight after (or before) its parent whichever side it hangs on. The three-node chains show it:

Four trees of three nodes, each a chain: 1 with 2 below it on the left or the right, and 3 below 2 on the left or the right. Under every tree the same preorder, 1 2 3, and the same postorder, 3 2 1, and a different inorder: 3 2 1, 2 3 1, 1 3 2 and 1 2 3.

Of the five shapes with 3 nodes, four share that pair. In general, a pair is shared by 2^s trees, where s is the number of nodes with one child: the count above held for every one of the 429 shapes with 7 nodes. When every node has 0 or 2 children, there's no lone child to place, and preorder with postorder does fix the tree (Construct Binary Tree from Preorder and Postorder Traversal asks for any tree that fits).

Conditions for Unique Tree Construction asks exactly this, with 1 for preorder, 2 for inorder and 3 for postorder:

def is_possible(a, b):
    """1 is preorder, 2 inorder, 3 postorder: two lists fix a tree when exactly one of them is the inorder."""
    return int((a == 2) != (b == 2))

Of the nine pairs, it said yes to (1, 2), (2, 1), (2, 3) and (3, 2): one of the two must be the inorder, and two inorders tell you nothing about the roots.

Writing a tree down

Serialize and Deserialize Binary Tree asks for a string that holds a whole tree, and a way to read it back. One traversal is enough once every missing child is written as a mark: the marks say where each subtree ends, so nothing is left to guess. Preorder with a # for each missing child, read back in the same order:

class Codec:
    def serialize(self, root):
        """Preorder, with '#' for every missing child: the marks say where each subtree ends."""
        out = []

        def write(node):
            if node is None:
                out.append("#")
                return
            out.append(str(node.val))
            write(node.left)
            write(node.right)

        write(root)
        return ",".join(out)

    def deserialize(self, data):
        """Read it back in the same order: each call takes a value and builds its two subtrees, or takes a '#'."""
        values = iter(data.split(","))

        def read():
            v = next(values)
            if v == "#":
                return None
            node = TreeNode(int(v))
            node.left = read()
            node.right = read()
            return node

        return read()

LeetCode's first example came out as 1,2,#,#,3,4,#,#,5,#,#: 1, then 2 with no children, then 3 with 4 and 5. It read back to the same tree, and the empty tree went to # and back.

Both halves recurse as deep as the tree, and the problem allows 10,000 nodes, which can be one long chain. Run on chains, the recursive serialize gave out in three of the four languages:

LanguageOn a chain of 10,000 nodesWhat it took
Pythonfailedfailed at 1,000 nodes and passed at 900: its limit is about 1,000 calls
Javafailedran out of stack after about 9,860 calls
JavaScriptfailed on a fresh runits limit sits near 10,000 and moves as the code warms up; warmed up, it passed
C++passedits stack held all 10,000 calls

Level order needs no recursion. It is also LeetCode's own format: a queue writes the tree level by level with a mark for each missing child, and reading it back is the queue of parents from Introduction to Binary Trees, each taking the next two entries as its children:

class Codec {
public:
    // Level by level, '#' for every missing child: LeetCode's own format, with no recursion.
    string serialize(TreeNode* root) {
        string out;
        queue<TreeNode*> q;
        q.push(root);
        while (!q.empty()) {
            TreeNode* node = q.front();
            q.pop();
            if (!out.empty()) out += ',';
            if (!node) {
                out += '#';
                continue;
            }
            out += to_string(node->val);
            q.push(node->left);
            q.push(node->right);
        }
        return out;
    }

    // The same order back: each node in the queue takes the next two entries as its children.
    TreeNode* deserialize(string data) {
        vector<string> values;
        stringstream ss(data);
        for (string v; getline(ss, v, ',');) values.push_back(v);
        if (values[0] == "#") return nullptr;
        TreeNode* root = new TreeNode(stoi(values[0]));
        queue<TreeNode*> q;
        q.push(root);
        size_t i = 1;
        while (!q.empty()) {
            TreeNode* node = q.front();
            q.pop();
            if (values[i] != "#") {
                node->left = new TreeNode(stoi(values[i]));
                q.push(node->left);
            }
            if (values[i + 1] != "#") {
                node->right = new TreeNode(stoi(values[i + 1]));
                q.push(node->right);
            }
            i += 2;
        }
        return root;
    }
};
from collections import deque


class Codec:
    def serialize(self, root):
        """Level by level, '#' for every missing child: LeetCode's own format, with no recursion."""
        out, queue = [], deque([root])
        while queue:
            node = queue.popleft()
            if node is None:
                out.append("#")
                continue
            out.append(str(node.val))
            queue.append(node.left)
            queue.append(node.right)
        return ",".join(out)

    def deserialize(self, data):
        """The same order back: each node in the queue takes the next two entries as its children."""
        values = data.split(",")
        if values[0] == "#":
            return None
        root = TreeNode(int(values[0]))
        queue, i = deque([root]), 1
        while queue:
            node = queue.popleft()
            if values[i] != "#":
                node.left = TreeNode(int(values[i]))
                queue.append(node.left)
            if values[i + 1] != "#":
                node.right = TreeNode(int(values[i + 1]))
                queue.append(node.right)
            i += 2
        return root
static class Codec {
    // Level by level, '#' for every missing child: LeetCode's own format, with no recursion.
    public String serialize(TreeNode root) {
        StringBuilder out = new StringBuilder();      // not +=, which copies the whole string each time
        Queue<TreeNode> queue = new LinkedList<>();   // it holds nulls, which an ArrayDeque refuses
        queue.add(root);
        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();
            if (out.length() > 0) out.append(',');
            if (node == null) {
                out.append('#');
                continue;
            }
            out.append(node.val);
            queue.add(node.left);
            queue.add(node.right);
        }
        return out.toString();
    }

    // The same order back: each node in the queue takes the next two entries as its children.
    public TreeNode deserialize(String data) {
        String[] values = data.split(",");
        if (values[0].equals("#")) return null;
        TreeNode root = new TreeNode(Integer.parseInt(values[0]));
        Queue<TreeNode> queue = new ArrayDeque<>();
        queue.add(root);
        int i = 1;
        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();
            if (!values[i].equals("#")) {
                node.left = new TreeNode(Integer.parseInt(values[i]));
                queue.add(node.left);
            }
            if (!values[i + 1].equals("#")) {
                node.right = new TreeNode(Integer.parseInt(values[i + 1]));
                queue.add(node.right);
            }
            i += 2;
        }
        return root;
    }
}
// Level by level, '#' for every missing child: LeetCode's own format, with no recursion.
var serialize = function (root) {
    const out = [];
    const queue = [root];
    for (let head = 0; head < queue.length; head++) {
        const node = queue[head];
        if (node === null) {
            out.push("#");
            continue;
        }
        out.push(String(node.val));
        queue.push(node.left, node.right);
    }
    return out.join(",");
};

// The same order back: each node in the queue takes the next two entries as its children.
var deserialize = function (data) {
    const values = data.split(",");
    if (values[0] === "#") return null;
    const root = new TreeNode(Number(values[0]));
    const queue = [root];
    let i = 1;
    for (let head = 0; head < queue.length; head++) {
        const node = queue[head];
        if (values[i] !== "#") {
            node.left = new TreeNode(Number(values[i]));
            queue.push(node.left);
        }
        if (values[i + 1] !== "#") {
            node.right = new TreeNode(Number(values[i + 1]));
            queue.push(node.right);
        }
        i += 2;
    }
    return root;
};

Each wrote and read back the problem's example, the empty tree and a tree with negative values: 1,2,3,#,#,4,5,#,#,#,#, # and -1,0,1000,#,-1000,#,#,#,#. The commas keep -1000 one value, and every string read back to the tree it came from. A tree of n nodes has n + 1 missing children, so the string holds 2n + 1 entries: O(n) to write and to read.

Flattening a tree in place

The last two problems here work on the tree itself, not on its lists, and each uses what a traversal order says about the tree's shape. Flatten Binary Tree to Linked List wants the nodes in preorder along the right pointers, every left pointer empty, with no new nodes. Preorder puts a node's left subtree straight after the node and its right subtree after the left one's last node. So for each node with a left subtree: find that subtree's last node in preorder, which is its rightmost, hang the right subtree off it, and move the left subtree to the right. Then go one step right and repeat:

// Preorder along the right pointers, in place: each left subtree moves to the right, and the old
// right subtree hangs off the left subtree's last node.
void flatten(TreeNode* root) {
    for (TreeNode* node = root; node != nullptr; node = node->right) {
        if (node->left == nullptr) continue;
        TreeNode* last = node->left;                // the left subtree's last node in preorder: its rightmost
        while (last->right != nullptr) last = last->right;
        last->right = node->right;                  // the right subtree comes after it
        node->right = node->left;                   // the left subtree comes straight after the node
        node->left = nullptr;
    }
}
def flatten(root):
    """Preorder along the right pointers, in place: each left subtree moves to the right, and the old right
    subtree hangs off the left subtree's last node."""
    node = root
    while node:
        if node.left:
            last = node.left                        # the left subtree's last node in preorder: its rightmost
            while last.right:
                last = last.right
            last.right = node.right                 # the right subtree comes after it
            node.right = node.left                  # the left subtree comes straight after the node
            node.left = None
        node = node.right
// Preorder along the right pointers, in place: each left subtree moves to the right, and the old
// right subtree hangs off the left subtree's last node.
static void flatten(TreeNode root) {
    for (TreeNode node = root; node != null; node = node.right) {
        if (node.left == null) continue;
        TreeNode last = node.left;                  // the left subtree's last node in preorder: its rightmost
        while (last.right != null) last = last.right;
        last.right = node.right;                    // the right subtree comes after it
        node.right = node.left;                     // the left subtree comes straight after the node
        node.left = null;
    }
}
// Preorder along the right pointers, in place: each left subtree moves to the right, and the old
// right subtree hangs off the left subtree's last node.
function flatten(root) {
    for (let node = root; node !== null; node = node.right) {
        if (node.left === null) continue;
        let last = node.left;                       // the left subtree's last node in preorder: its rightmost
        while (last.right !== null) last = last.right;
        last.right = node.right;                    // the right subtree comes after it
        node.right = node.left;                     // the left subtree comes straight after the node
        node.left = null;
    }
}

Each printed [1, null, 2, null, 3, null, 4, null, 5, null, 6] for the problem's first example, the chain written as LeetCode's list, then the empty tree and a single node unchanged. Step through it:

Rewiring 1, 2, 5, 3, 4, null, 6 — Indigo is the node being rewired; amber the walk down its left subtree's right edge; green that subtree's last node, where the right subtree is hung; violet the finished chain.

Each edge is walked at most twice, once by a search for a rightmost node and once by the main walk: O(n) time, O(1) extra space, and no recursion.

The recursive version flattens both subtrees first, then walks to the end of the flattened left part to attach the right one. On a tree that leans left, that walk gets longer at every level:

NodesRecursing, then walking to the endRewiring in place
1,00018.9 ms0.36 ms
2,00073.5 ms0.72 ms
2,000, a random shape1.2 ms0.73 ms

Both produced the same chain every time.

Counting a complete tree without visiting it

Count Complete Tree Nodes gives a complete tree: every level full except perhaps the last, which fills from the left. A walk counts it in O(n), and the problem asks for better. In a complete tree, if a node's leftmost path and rightmost path have the same length h, its subtree is perfect and holds 2^h - 1 nodes with no walk at all. If they differ, the last level ends somewhere inside, and one of the node's two halves is still perfect:

A complete tree of 12 nodes. At the root, the leftmost path has 4 nodes and the rightmost 3, so it isn't perfect. Its left half is shaded green and labelled 2 to the 3 minus 1 is 7: its paths are both 3 long, and the nodes inside it are pale: counted without a visit. Its right half is not perfect, and inside it the same check repeats, down to single nodes. A tag at every node the count visits shows its two path lengths: amber where they differ, green where they match. Six calls count all 12.
// In a complete tree, a subtree whose leftmost and rightmost paths are as long as each other is
// perfect: 2^h - 1 nodes. Otherwise one of its two halves is perfect, and the calls go down one path.
int countNodes(TreeNode* root) {
    if (root == nullptr) return 0;
    int left = 0, right = 0;
    for (TreeNode* n = root; n; n = n->left) left++;
    for (TreeNode* n = root; n; n = n->right) right++;
    if (left == right) return (1 << left) - 1;
    return 1 + countNodes(root->left) + countNodes(root->right);
}
def count_nodes(root):
    """In a complete tree, a subtree whose leftmost and rightmost paths are as long as each other is perfect:
    2^h - 1 nodes. Otherwise one of its two halves is perfect, and the calls go down one path."""
    if root is None:
        return 0
    left, node = 0, root
    while node:
        left += 1
        node = node.left
    right, node = 0, root
    while node:
        right += 1
        node = node.right
    if left == right:
        return (1 << left) - 1
    return 1 + count_nodes(root.left) + count_nodes(root.right)
// In a complete tree, a subtree whose leftmost and rightmost paths are as long as each other is
// perfect: 2^h - 1 nodes. Otherwise one of its two halves is perfect, and the calls go down one path.
static int countNodes(TreeNode root) {
    if (root == null) return 0;
    int left = 0, right = 0;
    for (TreeNode n = root; n != null; n = n.left) left++;
    for (TreeNode n = root; n != null; n = n.right) right++;
    if (left == right) return (1 << left) - 1;
    return 1 + countNodes(root.left) + countNodes(root.right);
}
// In a complete tree, a subtree whose leftmost and rightmost paths are as long as each other is
// perfect: 2^h - 1 nodes. Otherwise one of its two halves is perfect, and the calls go down one path.
function countNodes(root) {
    if (root === null) return 0;
    let left = 0, right = 0;
    for (let n = root; n !== null; n = n.left) left++;
    for (let n = root; n !== null; n = n.right) right++;
    if (left === right) return (1 << left) - 1;
    return 1 + countNodes(root.left) + countNodes(root.right);
}

Each printed 6, 0, 1 and 12: the problem's examples and our 12-node tree. Only one child of each call can be imperfect, so the calls follow one path down, each walking two paths of O(log n): O(log² n) in all. On complete trees of the problem's size:

NodesA full walkComparing the two paths
12,50012,500 visits, 3.03 ms27 calls, 0.031 ms
25,00025,000 visits, 6.29 ms29 calls, 0.035 ms
50,00050,000 visits, 12.49 ms31 calls, 0.039 ms

Doubling the nodes adds one level, and two more calls.

In your language

Each language has one trap in this lesson's code, each run on the judge:

LanguageThe trapWhat happened
Pythonrecursion stops at about 1,000 callsthe recursive serialize failed on a chain of 1,000 nodes; raise the limit, or use a queue
JavaString += copies the whole string every timeserializing 10,000 nodes took 241.7 ms, against 7.07 ms with a StringBuilder
JavaScriptthe stack runs out near 10,000 callsthe recursive serialize failed on a chain of 10,000 in a fresh run; the queue version has no limit
C++stoi("#") throwsreading a mark as a number stopped the program with std::invalid_argument: compare it to "#" first

Java's cost grows with the square of the string: 21.8 ms at 2,500 nodes, 82.7 ms at 5,000 nodes, 241.7 ms at 10,000 nodes, against 1.95 to 7.07 ms with a StringBuilder.

Spotting it, and when it breaks

The clues. "Construct", "rebuild", "from its traversals": preorder or postorder for the roots, the inorder to split, a map of positions. "Serialize", "encode", "store and restore": a mark for every missing child, in preorder or level order. "Flatten", "in place", "into a linked list": rewire as you walk. "Complete tree" with "faster than O(n)": compare the two outer paths.

The near-misses. Construct Binary Tree from Preorder and Postorder Traversal accepts any tree that fits, because there can be several. Building a binary search tree from its preorder needs no inorder at all, since the values say which side each goes (Binary Search Trees). Serializing a tree with any number of children writes each node's child count instead of marks.

When it breaks. Repeated values break the inorder lookup: a value can sit in two places. A tree that leans one way breaks recursion in Python, Java and JavaScript at the problems' own sizes; the level-order codec and the rewiring don't recurse.

Bugs that cost a submission

Slices one value short.

def build_tree(preorder, inorder):
    if not preorder:
        return None
    root = TreeNode(preorder[0])
    k = inorder.index(preorder[0])
    root.left = build_tree(preorder[1:k], inorder[:k])          # one value short
    root.right = build_tree(preorder[k:], inorder[k + 1:])
    return root

On the problem's first example it stopped with ValueError: 9 is not in list. The left subtree's preorder is the k values after the root, preorder[1:k + 1]; one short, and the right side starts with a value that belongs on the left.

The left subtree first, from the back of the postorder.

def build_tree(inorder, postorder):
    """Postorder lists each root last. Read from the back, it gives a root, then its right subtree's roots."""
    at = {v: k for k, v in enumerate(inorder)}
    next_root = len(postorder) - 1

    def grow(lo, hi):
        nonlocal next_root
        if lo > hi:
            return None
        root = TreeNode(postorder[next_root])
        next_root -= 1
        k = at[root.val]
        root.left = grow(lo, k - 1)
        root.right = grow(k + 1, hi)
        return root

    return grow(0, len(inorder) - 1)

From our tree's inorder and postorder it stopped with an IndexError instead of building [1, 2, 3, 4, 5, null, 6, null, null, 7]: each part was handed a root that wasn't in it, and the calls ran off the postorder. Read from the back, the value after a root is its right subtree's root.

No marks for missing children.

def serialize(root):
    """Preorder, values only."""
    if root is None:
        return ""
    rest = [serialize(root.left), serialize(root.right)]
    return ",".join([str(root.val)] + [r for r in rest if r])

[1, 2] and [1, null, 2] both came out as 1,2: 2 on the left and 2 on the right wrote the same string, and no reader can tell them apart.

Flattening without clearing the left pointer.

def flatten(root):
    """Preorder along the right pointers, in place: each left subtree moves to the right, and the old right
    subtree hangs off the left subtree's last node."""
    node = root
    while node:
        if node.left:
            last = node.left                        # the left subtree's last node in preorder: its rightmost
            while last.right:
                last = last.right
            last.right = node.right                 # the right subtree comes after it
            node.right = node.left                  # the left subtree comes straight after the node
        node = node.right

The right pointers held all 6 nodes in order, but 2 of them still had a left child. The problem checks that every left pointer is empty.

Trusting the leftmost path alone.

def count_nodes(root):
    h, node = 0, root
    while node:                                     # only the leftmost path
        h += 1
        node = node.left
    return (1 << h) - 1

On [1, 2, 3, 4, 5, 6] it returned 7; the tree has 6 nodes. The leftmost path is 3 long, but the last level isn't full: only when the rightmost path is as long is the tree perfect.

Check yourself

Quiz

1 / 7

Multiple choice

The preorder is [3, 9, 20, 15, 7] and the inorder [9, 3, 15, 20, 7]. How many nodes are in the root's left subtree?

Practice problems

In this order:

  1. Conditions for Unique Tree Construction: which pairs of lists fix a tree.
  2. Construct Binary Tree from Preorder and Inorder Traversal: roots from the front, splits from a map.
  3. Construct Binary Tree from Inorder and Postorder Traversal: roots from the back, the right subtree first.
  4. Serialize and Deserialize Binary Tree: a mark for every missing child.
  5. Flatten Binary Tree to Linked List: rewire as you walk.
  6. Count Complete Tree Nodes: two paths at each call.

Then the section's quiz, and after it Binary Search Trees: trees whose values are in order, so a search goes straight down.