DSA DSA In Depth
0%
lesson morris-traversalthreaded-treesin-place

Morris In-Order Traversal: O(1) Auxiliary Space via Temporary Threading

7 min read Module 3.1: Binary Trees, Traversals, BSTs & Priority Heaps Updated Oct 23, 2026
📄 Cheat Sheet
Morris Inorder Tree Traversal — O(1) Space Algorithm
Tushar Roy - Coding Made Simple Morris Inorder Tree Traversal — O(1) Space Algorithm Watch on YouTube ↗

Step-by-step trace of predecessor discovery, temporary right-pointer thread creation, and exact restoration of the original tree topology.

0:00 / --:--

The Threading Mechanism & Cycle Detection Without Tag Bits

In a standard binary tree without parent pointers, descending into curr.left destroys our only reference back to curr unless we push curr onto a stack. Notice, however, that in an In-Order traversal, the very last node visited inside curr.left before returning to curr is the in-order predecessor of curr:

$$\text{pred}(u) = \text{rightmost}(u.\text{left})$$

Because $\text{pred}(u)$ is the rightmost node in $u$'s left subtree, its right child pointer is guaranteed to be null in the unmodified tree. Morris Traversal repurposes that exact null slot to store a return address back to $u$. When we later return to $u$ via that thread, how do we know whether we are arriving at $u$ for the first time (from $u$'s parent) or the second time (via the upward thread)? We simply walk down to $\text{pred}(u)$ again:

  • Case 1 (First arrival at $u$, $\text{pred}(u).\text{right} == \text{null}$): Install the thread $\text{pred}(u).\text{right} \leftarrow u$ and descend $u \leftarrow u.\text{left}$. (For Pre-Order, emit $u$ right here before descending!)
  • Case 2 (Second arrival at $u$, $\text{pred}(u).\text{right} == u$): We have finished $u$'s left subtree. Restore the tree by setting $\text{pred}(u).\text{right} \leftarrow \text{null}$, emit $u$ (for In-Order), and advance $u \leftarrow u.\text{right}$.
Traversal Method Total Edge Traversals Time Complexity Auxiliary Memory Mutates Tree During Walk?
Morris In-Order / Pre-Order $\le 4(N - 1)$ edges O(N) Amortized O(1) Words Yes (temporarily; restored on exit)
Iterative Explicit Stack $2(N - 1)$ edges O(N) O(h) Words No (read-only)
Static Threaded Tree (Perlis-Thornton) $2(N - 1)$ edges O(N) O(1) Words No (requires 1 tag bit per pointer)

Amortized Linearity Proof: Why Predecessor Searches Sum to $O(N)$

Finding $\text{pred}(u)$ walks a right spine of length up to $O(h)$, which naively suggests $O(N h)$ total time. To prove strict $O(N)$ time, count how many times any individual tree edge $e = (x, y)$ is traversed across the entire execution:

  1. Main pointer curr descent: Every left edge is traversed at most once downward by curr = curr.left; every right edge is traversed at most once downward by curr = curr.right.
  2. Predecessor search spine: Consider the right-leaning chain inside $u.\text{left}$ leading to $\text{pred}(u)$. Crucially, the predecessor search corridors of two distinct nodes $u_1 \ne u_2$ are strictly edge-disjoint! Why? Every edge in $u$'s predecessor path consists of a single left step from $u$ to $u.\text{left}$, followed exclusively by right steps. Thus, walking backward up the right steps and taking one step up-right uniquely identifies $u$.

Because each edge $e$ belongs to the predecessor corridor of at most one ancestor $u$, $e$ is traversed once to set the thread and once to clear the thread. Summing over all $N - 1$ edges bounds total pointer dereferences to at most $4(N - 1) = \Theta(N)$.

Morris O(1)-Space Threaded In-Order & Euler Tour Subtree Flattening
O(N) Time · O(1) Aux Space / Contiguous [tin, tout]

Trace how Morris Traversal temporarily rewires each in-order predecessor’s null right pointer (pred.right = curr) to climb back up without a call stack, and how Euler Tour timestamps flatten every subtree into a contiguous array slice [tin[u], tout[u]].

Step 1 / 9
Custom Input:
Timeline

Invariant & State

Phase 1 (Create Thread): At curr=4, in-order predecessor in left subtree is pred=3. Since pred.right == null, set pred.right = curr(4) and advance curr = curr.left(2).
curr=4 2 5 1 pred=3 Thread: pred(3).right = curr(4)
Pointer Action
pred(3).right = curr(4)
Thread State
1 Active Thread (3 → 4)
In-Order Output
[]
Aux Call-Stack Space
O(1) Words
[Step 1] Phase 1 (Create Thread): At curr=4, in-order predecessor in left subtree is pred=3. Since pred.right == null, set pred.right = curr(4) and advance curr = curr.left(2).

Visualizing Temporary Thread Creation & Severing

The diagram below illustrates how each node $u$ with a non-empty left child claims a unique, edge-disjoint right spine (highlighted in amber/emerald) leading to its predecessor.

8 4 10 2 6 3 ━━ Disjoint Spine for u = 8: (4 → 6) ╌╌ Thread: pred(8)=6 sets 6.right = 8 ━━ Disjoint Spine for u = 4: (2 → 3) ╌╌ Thread: pred(4)=3 sets 3.right = 4 1st visit to u: pred.right == null (Set thread) 2nd visit to u: pred.right == u (Cut thread)
flowchart TD Start["curr != null"] --> CheckLeft{"curr.left == null ?"} CheckLeft -- "Yes" --> VisitRight["Emit(curr) curr = curr.right"] CheckLeft -- "No" --> FindPred["pred = curr.left while pred.right not in {null, curr}: pred = pred.right"] FindPred --> CheckThread{"pred.right == null ?"} CheckThread -- "Yes (1st Arrival)" --> MakeThread["pred.right = curr (Emit curr here for Pre-Order) curr = curr.left"] CheckThread -- "No (2nd Arrival)" --> CutThread["pred.right = null Emit(curr) (In-Order) curr = curr.right"]

Hardware Reality: Dirty Cache Lines & Read-Only Memory Pages

Although Morris Traversal saves $O(h)$ stack memory, it is often slower in wall-clock latency than an explicit-stack traversal on modern multi-core CPUs for two hardware reasons:

  1. Cache Line Dirtying (MESI Protocol): Writing pred->right = curr transitions the 64-byte cache line containing pred from Shared (S) to Modified (M), invalidating that cache line across all other CPU cores and forcing write-back traffic upon eviction.
  2. Double Spine Chasing: Every internal node with a left child has its predecessor spine walked twice, doubling pointer-chasing memory accesses compared to an L1-resident explicit array stack.

Implementation: Morris In-Order & Pre-Order in Rust / C++

// C++20: Morris O(1) Auxiliary Space In-Order and Pre-Order Traversals
#include <vector>

struct Node {
    int val;
    Node *left = nullptr, *right = nullptr;
};

std::vector<int> morrisInorder(Node* root) {
    std::vector<int> result;
    Node* curr = root;

    while (curr != nullptr) {
        if (curr->left == nullptr) {
            result.push_back(curr->val);
            curr = curr->right; // Follows either a real right child or an upward thread!
        } else {
            // Find the in-order predecessor in curr's left subtree
            Node* pred = curr->left;
            while (pred->right != nullptr && pred->right != curr) {
                pred = pred->right;
            }

            if (pred->right == nullptr) {
                // First arrival at curr: install temporary upward thread
                pred->right = curr;
                curr = curr->left;
            } else {
                // Second arrival at curr via thread: restore tree and visit curr
                pred->right = nullptr;
                result.push_back(curr->val);
                curr = curr->right;
            }
        }
    }
    return result;
}

Failure Modes & Adversarial Pitfalls

  • Catastrophic Concurrency Race & Read-Only Pages: Never use Morris Traversal on a concurrently read tree or a tree mapped in read-only memory (mmap(..., PROT_READ)). Concurrent readers will follow temporary upward threads into infinite loops, and writing pred->right = curr on a read-only page triggers an immediate hardware page fault (SIGSEGV).
  • Early Return / Exception Corruption: If you break out of a Morris Traversal early (e.g., searching for the $k$-th smallest element in a BST) without completing the loop, active threads remain embedded in the tree! This turns the tree into a cyclic graph and causes subsequent destructors (delete root) to double-free or loop infinitely. Always finish or explicitly unwind active threads before returning.
Check yourself Question 1 of 4
Concept Check Target of Temporary Thread in Morris Traversal recall

During Morris In-Order Traversal at a node `curr` that has a non-null left child, which pointer is temporarily modified to create an upward return thread?

⚡ Interactive Challenge · Staff Level

Algorithmic Design & Complexity Check

Analyze the invariant, asymptotic bound, and hardware behavior before selecting.

Why is Morris Traversal hazardous in a multi-threaded read-shared cache or when implementing an early-terminating search (e.g., `findFirstMatchingNode(root)`)?

Key Takeaways

Morris Traversal achieves O(N) time and strict O(1) auxiliary space without parent pointers or tag bits by temporarily wiring pred(u).right = u.
Predecessor spines of distinct nodes are strictly edge-disjoint, bounding total edge traversals across the entire walk to at most 4(N - 1).
Emitting u when installing the thread (pred.right == null) yields Pre-Order; emitting u when severing the thread (pred.right == u) yields In-Order.
Because Morris Traversal mutates node pointers in place, it violates const-correctness, breaks concurrent readers, and leaves cycles if exited early.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue