1. Introduction & Motivation
Every traversal covered so far — recursive inorder, preorder, postorder, and BFS with a queue — spends auxiliary space to remember "where to come back to." Recursive traversals spend $O(h)$ call-stack space (worst case $O(n)$ for a skewed tree); an explicit stack-based iterative traversal spends the same. BFS spends $O(w)$ queue space, where $w$ is the widest level. None of them is free.
This lesson asks a sharper question: can a tree be traversed in $O(1)$ auxiliary space, with no stack, no queue, and no recursion? The answer is yes, and the trick is elegant — a leaf's null child pointer is wasted space. Morris traversal temporarily repurposes those null pointers as "threads" back to an ancestor, walks the tree using them, and then removes them to restore the original shape. Threaded binary trees take the same idea and make it permanent, trading a boolean flag per node for a tree that never needs a stack to traverse, forwards or backwards.
The lesson then turns to two problems that do not fit the "visit every node once" traversal family at all: boundary traversal, which reconstructs only the outline of a tree (its left spine, its leaves, its right spine), and serialization/deserialization (LeetCode 297), which converts an entire tree to a string and rebuilds it exactly — the foundation of persisting, transmitting, and testing tree-shaped data.
2. Prerequisites
This lesson builds directly on Complete DSA Series Part 9: Trees. It assumes comfort with recursive inorder/preorder/postorder traversal, BFS level order, and basic pointer manipulation on a binary tree node.
Two traversal topics are deliberately not repeated here because they already have complete, correct implementations in Part 9:
- Zigzag level order and vertical order traversal — fully implemented in the BFS Traversal section of Part 9.
- Reconstructing a tree from preorder+inorder or inorder+postorder — fully implemented in the Tree Construction section of Part 9.
If you have not worked through those sections, do so before continuing — this lesson only covers the traversal and reconstruction techniques that Part 9 does not: Morris traversal, threaded trees, boundary traversal, and full-tree serialization.
3. Morris Inorder Traversal
curr with a left child, the node that should be visited immediately after curr finishes its left subtree is its inorder predecessor — the rightmost node in that left subtree. If we temporarily set that predecessor's null right pointer to point back at curr, we have planted a "thread" that lets us return to curr without a stack. Descend left, do the left subtree's work, and when a thread is followed back up, we know the left subtree is done — visit curr now, detect the thread, remove it (restoring the tree exactly to its original shape), and move right. Each edge is walked at most twice, so the whole traversal is $O(n)$ time using only a fixed number of pointer variables — $O(1)$ extra space.
Trace Morris inorder on a 5-node tree
Tree: root 1, left child 2, right child 3; 2's children are 4 (left) and 5 (right). Expected inorder: 4, 2, 5, 1, 3.
| Step | curr | Predecessor | Action | Output so far |
|---|---|---|---|---|
| 1 | 1 | 5 (rightmost of 2's subtree) | 5.right is null → thread 5→1; curr = 2 | [] |
| 2 | 2 | 4 (4.right is null) | thread 4→2; curr = 4 | [] |
| 3 | 4 | — | no left child → visit 4; curr = 4.right = 2 (thread) | [4] |
| 4 | 2 | 4 (4.right is 2 = curr) | thread found → remove it (4.right = null); visit 2; curr = 5 | [4, 2] |
| 5 | 5 | — | no left child → visit 5; curr = 5.right = 1 (thread) | [4, 2, 5] |
| 6 | 1 | 5 (5.right is 1 = curr) | thread found → remove it (5.right = null); visit 1; curr = 3 | [4, 2, 5, 1] |
| 7 | 3 | — | no left child → visit 3; curr = null, loop ends | [4, 2, 5, 1, 3] |
Every thread created in step 1 or 2 is removed by step 4 or 6 — the tree is bit-for-bit identical to before the traversal started.
4. Morris Preorder Traversal
Morris preorder reuses the exact same threading logic as Morris inorder. The only difference is when a node's value is recorded: inorder records a node the second time it is reached (when the thread is found and removed, after the left subtree is done), while preorder must record a node before descending into its left subtree — the first time it is reached, at the moment the thread is created. Moving that one append/push_back line from the "thread found" branch to the "thread created" branch is the entire change.
5. Threaded Binary Trees
Building the threads requires one inorder pass that links each node to the previously-visited node whenever that previous node's right pointer is still empty. This construction pass itself uses $O(h)$ recursion-stack space (a one-time cost); every traversal after construction is $O(1)$ extra space, because it never recurses or pushes onto a stack again.
6. Boundary Traversal
Boundary traversal returns only the outline of a tree: the root, the left boundary read top-down, every leaf read left-to-right, and the right boundary read bottom-up. It is assembled from three separate walks that never overlap, so no node is ever emitted twice:
- Left boundary (top-down, excluding leaves): start at
root.left; at each step, record the node unless it is a leaf, then move to its left child if one exists, otherwise its right child. - All leaves (left to right): a plain DFS over the whole tree that records only nodes with no children.
- Right boundary (bottom-up, excluding leaves): the mirror image of step 1 starting at
root.right, collected into a temporary list and reversed before appending.
7. Serialization and Deserialization (LeetCode 297)
Serialization converts a tree into a string that can be stored or transmitted; deserialization rebuilds the exact same tree from that string. The simplest correct scheme is preorder traversal with explicit null markers: every node, including a null child, is written; on the way back in, a recursive reader consumes tokens in the same order they were written, so it always knows whether the next token is a real value or a null marker.
Tracing the deserializer over the token stream 1,2,4,#,#,#,3,#,# shows exactly how the recursive reader rebuilds the tree one token at a time:
| Step | Token consumed | Action | Node built |
|---|---|---|---|
| 1 | "1" | Not '#' → create node, then recurse for its left subtree | Node(1) |
| 2 | "2" | Not '#' → create node, recurse for its left subtree | Node(2), 1.left = Node(2) |
| 3 | "4" | Not '#' → create node, recurse for its left subtree | Node(4), 2.left = Node(4) |
| 4 | "#" | Null marker → 4.left = None | — |
| 5 | "#" | Null marker → 4.right = None | — |
| 6 | "#" | Null marker → 2.right = None (2 is fully built) | — |
| 7 | "3" | Not '#' → create node; this becomes 1.right | Node(3), 1.right = Node(3) |
| 8 | "#" | Null marker → 3.left = None | — |
| 9 | "#" | Null marker → 3.right = None (tree fully built) | — |
8. Complexity Analysis
Morris inorder/preorder: $O(n)$ time — each edge in the tree is traversed at most twice (once to create/find a thread, once to remove it) — and $O(1)$ extra space, versus $O(n)$ space for a stack-based iterative traversal or $O(h)$ call-stack space for a recursive one.
Threaded trees: $O(n)$ traversal time and $O(1)$ extra space per traversal once threads exist. Construction is a one-time $O(n)$ pass that uses $O(h)$ recursion-stack space.
Boundary traversal: $O(n)$ time — the left boundary, leaves, and right boundary walks together visit each node a constant number of times — and $O(h)$ space for the leaf-collecting recursion.
Serialization/deserialization: $O(n)$ time and $O(n)$ space for both directions — every node (and every null child) contributes exactly one token to the string, and the recursion depth is $O(h)$.
9. Applications and Alternatives
Where these techniques show up in practice
- Morris traversal: memory-constrained environments (embedded systems, kernel code) where allocating a stack or recursing is undesirable, and any setting that needs traversal without disturbing the call stack's depth budget.
- Threaded trees: classic technique from early compiler symbol tables and iterator implementations that needed cheap, repeated traversal without stack overhead; still relevant wherever a tree is built once and walked many times.
- Boundary traversal: rendering the silhouette/outline of a hierarchical structure (LeetCode 545), UI tree visualizations, and any "draw just the perimeter" task.
- Serialization: persisting trees to disk or a database, sending them across a network (RPC/API payloads), snapshotting expected values in tests, and caching parsed expression trees in compilers and query engines.
The stack-based (or recursive) traversals from Part 9 remain the right default when extra space is not a constraint — they are simpler to read and debug. Reach for Morris traversal or a threaded tree specifically when the $O(1)$-space guarantee matters. For serialization at scale, purpose-built formats like Protocol Buffers or JSON schemas replace the hand-rolled preorder-with-nulls string, trading a larger dependency for cross-language compatibility and schema evolution.
10. Exercises
Easy
- Count the number of nodes in a tree using Morris traversal, without any extra data structure beyond the fixed pointer variables the traversal already uses.
- Modify Morris inorder traversal to return the $k$-th smallest value in a BST without collecting the full result list.
Medium
- Design a serializer/deserializer specifically for a binary search tree. Because a BST's inorder sequence is always sorted, a plain preorder listing of values (with no null markers at all) is sufficient to reconstruct the tree: rebuild recursively by tracking a valid value range for each subtree, using the BST property instead of an explicit marker to know where each subtree ends. Compare the resulting string length against the null-marker scheme from Section 7.
- Extend the single right-threaded tree from Section 5 into a double-threaded tree (both left and right null pointers become threads), and implement a reverse (right-to-left) traversal that uses the left threads.
Hard
- Run the boundary traversal from Section 6 on a tree that has duplicate values (for example, several nodes holding the value 5). Confirm, by tracing the algorithm, that it still produces the correct outline, and explain in one sentence why: the walk is driven entirely by node references and structure (left/right pointers, leaf tests), never by comparing values, so duplicate values cannot cause a node to be skipped or double-counted.
- Design a serialization scheme for a tree whose nodes can have an arbitrary number of children (an N-ary tree) rather than exactly two. One approach: serialize each node as its value followed by its child count, then recursively serialize each child; deserialize by reading the count first so the reader always knows how many children to expect.
Review rhythm: tomorrow, re-derive Morris inorder traversal from the core insight alone, without looking at the code. Next week, implement Morris preorder, the threaded tree, boundary traversal, and the serializer in a single sitting, timing yourself. Several weeks later, apply the "repurpose a null pointer" idea to a new problem you have not seen before, and teach the difference between a temporary thread (Morris) and a permanent thread (threaded tree) to someone who already knows recursive traversal.
11. Summary Cheat Sheet
- Morris inorder traversal threads a node's inorder predecessor to itself temporarily, traverses in $O(n)$ time and $O(1)$ space, then removes the thread to restore the tree.
- Morris preorder is Morris inorder with one line moved: visit the node when its thread is created, not when it is removed.
- Threaded binary trees make Morris's temporary threads permanent, trading a per-node flag for stack-free traversal on every future pass; single-threaded trees thread only the right pointers, double-threaded trees thread both.
- Boundary traversal assembles the root, the left spine (excluding leaves, top-down), all leaves (left to right), and the right spine (excluding leaves, reversed to read bottom-up) — three non-overlapping walks, $O(n)$ time.
- Preorder-with-null-markers serialization (LeetCode 297) round-trips any binary tree exactly in $O(n)$ time and space; a BST can drop the null markers entirely by using value ranges during reconstruction.
- Zigzag level order, vertical order traversal, and traversal-pair reconstruction (preorder+inorder, inorder+postorder) live in Part 9, not here.