← All Posts
DSA Series · Trees · Threaded · Part 3

Threaded Tree Traversal

The Problem: Why Is Standard Traversal Expensive?

In a normal binary tree, in-order traversal requires auxiliary space, either the implicit call stack of recursion or an explicit stack data structure. Both cost O(h) space, where h is the tree height.

Why? Because after visiting the entire left subtree of a node, we need to find our way back to the node itself. In a standard tree, there are no upward links, the only way back is to have saved the node on a stack before going left.

The key question: What if the tree already contained links that let us jump back to the right place without a stack? That's exactly what threaded trees provide.

The Core Idea

In a right-threaded BST, every node whose right child would normally be null instead has a thread pointing to its in-order successor. This means:

These two rules completely eliminate the need for a stack. The threads serve as built-in "return addresses."

The Algorithm, Step by Step

Starting Point

We start at the leftmost node in the entire tree. This is the smallest element, the first node in the in-order sequence. To find it, we simply keep going left from the root until we can't anymore:

ThreadedNode* leftmost(ThreadedNode* node) {
    while (node && node->left && !node->leftThread)
        node = node->left;
    return node;
}

But Wait: Why Don't We Visit Nodes on the Way Down?

This is the most common source of confusion. Consider this tree with root 4:

        4
       / \
      2   6
     / \
    1   3

We go from 4 → 2 → 1 and only visit node 1. Why did we "skip" nodes 4 and 2?

The answer comes directly from what in-order means: for every node, you must finish its entire left subtree before you visit the node itself. That's the definition, Left, Root, Right.

Let's trace the logic concretely:

The rule is simple: You cannot visit a node until its entire left subtree has been processed. The leftmost node is the only node that has no pending left subtree work, that's why it's always first. Every other node on the path down has unfinished left-side business and must wait its turn.

So how do 4 and 2 eventually get visited? They don't get forgotten, the threads bring us back to them! After visiting 1, node 1's right-thread points to 2 (its in-order successor). After visiting 2 and its right subtree (3), node 3's right-thread points to 4. The threads act as "reminders" that guide us back to each skipped node at exactly the right moment.

This is also why the same leftmost() logic appears in Case 2 below. When we visit a node and it has a right subtree, the next in-order node is the leftmost of that right subtree, for the same reason: we must finish the left side of that subtree before visiting anything else in it.

The Main Loop

Once we're at the leftmost node, we repeatedly ask: "What comes next?" There are exactly two cases:

Case 1: The node is right-threaded (rightThread == true)

The right pointer is a thread to the in-order successor. We simply follow it:

cur = cur->right;  // follow thread — O(1), no searching needed

Why this works: During construction, we specifically set this pointer to point to the node that comes next in in-order sequence. The thread is essentially a pre-computed "next" link.

Case 2: The node has a real right child (rightThread == false)

There's a right subtree. The next node in in-order is the leftmost (smallest) node in that subtree:

cur = leftmost(cur->right);  // go right once, then all the way left

Why this works: In-order traversal visits left subtree, then root, then right subtree. When we finish processing a node and it has a right subtree, the next thing to visit is the leftmost element of that right subtree.

Termination

The loop ends when cur becomes null. This happens when the last node in the in-order sequence (the rightmost node in the tree) has rightThread == true and its right pointer is null (no successor exists).

Detailed Walkthrough

Let's trace the algorithm on this right-threaded BST with nodes [1, 2, 3, 4, 5, 6, 7]. Threads (dashed) go from 1→2, 3→4, and 5→6:

StepcurActionWhyOutput So Far
01 leftmost(root) Start at 4, go left to 2, go left to 1. Can't go further, 1 is leftmost. -
11 Visit 1. rightThread = true → follow thread 1 has no right child. Its thread points to 2 (the parent, which is the in-order successor). 1
22 Visit 2. rightThread = falseleftmost(right) 2 has a real right child (3). Go to leftmost of that subtree, 3 itself (it has no left children). 1, 2
33 Visit 3. rightThread = true → follow thread 3 has no right child. Its thread points to 4 (the grandparent, the next in-order node). 1, 2, 3
44 Visit 4. rightThread = falseleftmost(right) 4 has a real right child (6). Go to leftmost: 6 → left child 5. 5 has no further left, so stop at 5. 1, 2, 3, 4
55 Visit 5. rightThread = true → follow thread 5 has no right child. Its thread points to 6 (the parent). 1, 2, 3, 4, 5
66 Visit 6. rightThread = falseleftmost(right) 6 has a real right child (7). Leftmost of that subtree is 7 itself. 1, 2, 3, 4, 5, 6
77 Visit 7. rightThread = true, right is null → done 7 is the last node. Its thread points to null (no successor). Loop terminates. 1, 2, 3, 4, 5, 6, 7

Observation: At every step, we either follow a thread (O(1)) or descend to the leftmost of a right subtree. Over the entire traversal, each edge in the tree is followed at most twice. Total work: O(n).

▶ Threaded In-order Traversal Animation

Watch the cursor follow children and threads to traverse without any stack.

1 2 3 4 5 6 7 ▲ cur

Complete C++ Implementation

Here's the full implementation with detailed comments explaining each decision:

// Helper: find the leftmost node in a subtree
// This is always the first node to visit in an in-order traversal
ThreadedNode* leftmost(ThreadedNode* node) {
    if (!node) return nullptr;
    while (node->left && !node->leftThread)
        node = node->left;     // keep going left while it's a real child
    return node;               // either a leaf or a doubly-threaded node
}

// In-order traversal: O(n) time, O(1) space
void threadedInorder(ThreadedNode* root) {
    ThreadedNode* cur = leftmost(root);   // start at smallest node

    while (cur) {
        visit(cur);                        // process current node

        if (cur->rightThread) {
            cur = cur->right;              // Case 1: follow thread to successor
        } else {
            cur = leftmost(cur->right);    // Case 2: leftmost in right subtree
        }
    }
    // When cur becomes null, we've visited all nodes
}

Why leftmost() Is Correct

The leftmost function stops when it encounters a null left pointer or a left thread. In a doubly-threaded tree, left threads point to predecessors, not children, following them would go backward. The check !node->leftThread ensures we only follow real child pointers.

In a singly (right-only) threaded tree, leftThread is always false, so the check simplifies to just while (node->left).

Why This Produces In-order Sequence

Let's prove this more rigorously. In-order traversal visits: left subtree, root, right subtree. Our algorithm maintains this invariant:

  1. We start at the leftmost node. This is correct, it's the node with no left subtree (or whose entire left subtree has been "skipped" because we started at the bottom).
  2. After visiting a node, the next in-order node is either:
    • The leftmost node in its right subtree (if one exists), this is the start of the right subtree's in-order.
    • Its in-order successor further up the tree (if no right subtree), the thread provides this directly.

In both cases, we jump to exactly the right node. No node is skipped, and no node is visited twice. The output is exactly the in-order sequence.

Connection to Morris Traversal

Morris traversal (covered in detail in the next post) achieves the exact same O(1)-space in-order traversal on a standard (unthreaded) tree. It does this by creating temporary threads on the fly, using them to traverse, and then removing them.

The relationship is straightforward:

Threaded Tree TraversalMorris Traversal
Thread creationDone once during constructionCreated on the fly, removed after use
Tree modificationNone during traversalTemporarily modifies tree (adds/removes threads)
Repeated traversalsFree, threads are permanentMust re-create threads every time
Thread safetySafe for concurrent readsNot safe, tree has cycles during traversal
Extra storageBoolean flags per node (permanent)None, but tree must be mutable

When to use which:

Pre-order Traversal on Threaded Trees

Pre-order (root, left, right) is also possible without a stack. The idea: visit the node immediately, then go left if possible. When there's no left child, go right. When there's no right child either, follow threads upward until we find a node with an unvisited right subtree.

The Algorithm

  1. Visit the current node (pre-order visits before children).
  2. If it has a real left child → go left.
  3. If it has a real right child (but no left) → go right.
  4. If it's a leaf (both threaded or null) → follow right threads upward until we find a node with a real right child. Go to that right child.
void threadedPreorder(ThreadedNode* root) {
    ThreadedNode* cur = root;
    while (cur) {
        visit(cur);                         // visit FIRST (pre-order)

        if (!cur->leftThread && cur->left) {
            cur = cur->left;                // go to left child
        } else if (!cur->rightThread && cur->right) {
            cur = cur->right;               // no left, go to right child
        } else {
            // Leaf node: follow right threads upward
            // until we find a node with a real right child
            while (cur && cur->rightThread)
                cur = cur->right;           // follow thread up
            if (cur)
                cur = cur->right;           // go to the right child
        }
    }
}

Why the Thread-following Loop Works

When we're at a leaf and follow the right thread, we jump to an ancestor. That ancestor was already visited (pre-order visits before descending). We need its right subtree. If the ancestor is also threaded (no right subtree), we keep following threads until we find one with a real right child. This effectively "unwinds" the left-side descent without a stack.

Post-order Traversal on Threaded Trees

Post-order (left, right, root) is the hardest of the three standard traversals to implement on threaded trees. The reason is fundamental: post-order visits a node after both its children. This means we arrive at a node, can't visit it yet, must process both subtrees, and only then come back to visit it.

Why Threads Alone Aren't Enough

Consider the root node 4 in our tree. Post-order says: process the entire left subtree [1, 3, 2], then the entire right subtree [5, 7, 6], and only then visit 4.

After visiting 3, the right thread takes us to 4, but we can't visit 4 yet. We still need to process {5, 6, 7}. There is no thread from 3 to 5 that would let us skip over 4. The right thread goes to 4 (the in-order successor), not to 5 (the next post-order node).

The core issue: In in-order, following a thread means "visit this ancestor now", the left subtree is done and the thread delivers us to exactly the right moment. In post-order, following a thread means "I'm back at this ancestor, but I still have right-subtree work to do." Threads provide one return path per node, but post-order effectively needs two returns per internal node (once after left, once after right).

The Approach: Stack + Last-Visited Tracking

The cleanest practical approach uses a small stack (O(h) space) and tracks which node was visited last. The lastVisited pointer lets us distinguish "returned from left child" vs "returned from right child", the exact information threads can't provide:

void threadedPostorder(ThreadedNode* root) {
    if (!root) return;

    std::stack<ThreadedNode*> stk;
    ThreadedNode* cur = root;
    ThreadedNode* lastVisited = nullptr;

    while (cur || !stk.empty()) {
        // Go as far left as possible (real children only)
        while (cur) {
            stk.push(cur);
            cur = (!cur->leftThread && cur->left) ? cur->left : nullptr;
        }

        ThreadedNode* top = stk.top();

        // If top has a real right child we haven't visited yet
        if (top->right && !top->rightThread && top->right != lastVisited) {
            cur = top->right;   // explore right subtree next
        } else {
            // Both subtrees done, visit this node
            visit(top);
            lastVisited = top;
            stk.pop();
        }
    }
}

How It Works

  1. Push leftward: From the current node, push every node on the leftward path onto the stack until there are no more real left children (checking !leftThread).
  2. Check the top: The top of the stack is the deepest unvisited node. If it has a real right child (!rightThread) that we haven't visited yet (!= lastVisited), go right and repeat step 1.
  3. Visit: If both subtrees are done (right child is a thread, null, or already visited), visit the node and pop it. Set lastVisited so the parent knows we've finished this side.

Why !rightThread Is Critical

In a standard (unthreaded) tree, we'd check top->right != nullptr to see if a right child exists. In a threaded tree, top->right might be non-null but point to the in-order successor, not a child. The !top->rightThread check ensures we only descend into real right children, not threads.

Space: O(h) Is Hard to Avoid

True O(1)-space post-order requires the reverse right-boundary technique (as in Morris post-order), which temporarily reverses chains of right-child pointers, visits them in reverse, and restores them. For an already-threaded tree, this is particularly complex because the threads (which also use right pointers) must be carefully preserved and restored. In practice, the stack-based approach is almost always preferred, O(h) is typically O(log n) for balanced trees.

The key insight: in-order and pre-order naturally follow the "forward" direction of threads. Post-order fights against this direction, which is why it requires extra machinery that threads alone can't provide.

▶ Threaded Post-order Traversal Animation

Watch post-order process both children before visiting each parent. The stack panel shows the current state of the explicit stack.

1 2 3 4 5 6 7 ▲ cur

Complexity Analysis

Time: O(n)

At first glance, Case 2 (finding the leftmost node in a right subtree) seems like it could be O(h) per step, making the total O(n·h). But this isn't the case. Here's why:

Each edge in the tree is traversed at most twice during the entire traversal, once going down (via leftmost) and once going up (via a thread). Since there are n−1 edges, the total work across all leftmost calls is O(n). Each visit is O(1). So the total is O(n).

This is the same amortized argument used for Morris traversal.

Space: O(1)

We use only a single pointer variable (cur). No stack, no recursion, no hash set. This is the entire point of threading.

Comparison Table

Traversal Method Time Space Modifies Tree? Works on Read-only?
Recursive (standard)O(n)O(h) call stackNoYes
Iterative with stackO(n)O(h) explicit stackNoYes
Morris traversalO(n)O(1)Temporarily yesNo
Threaded treeO(n)O(1)NoYes

Threaded tree traversal is the only method that achieves O(1) space and doesn't modify the tree during traversal. The trade-off is the upfront cost of threading the tree and the boolean flags per node.

Edge Cases to Watch For

Real-World Applications

Summary