Morris Traversal — Stop Stack Overflow in Skewed Trees
A sensor node crashed because recursion depth matched node count on a skewed tree.
20+ years shipping performance-critical code where algorithms decide the bill. Notes here come from systems that actually shipped.
- Morris Traversal walks a binary tree without recursion or stack using temporary right-pointer threads
- Each left subtree's rightmost node gets a thread back to the current node — that's your bookmark
- Creating and removing each thread costs O(1) amortized; total time is O(n), space is O(1)
- Every thread is removed before traversal ends — the tree is fully restored
- The biggest mistake: forgetting that threads modify the tree — unsafe for concurrent reads without locks
Imagine you're hiking through a forest maze with no map and no pen to mark where you've been. Morris Traversal is like secretly bending a twig on your way past each junction — so when you loop back, you know you've already visited that spot and can straighten it out behind you. You never carry a backpack of notes (no stack, no recursion). You just temporarily 'thread' the path behind you, use it, then restore the forest exactly as you found it.
Every serious DSA practitioner hits a wall when they realise that recursive tree traversal — elegant as it is — carries a hidden O(h) space cost on the call stack, where h is the tree height. On a skewed tree with a million nodes, that's a million stack frames. In memory-constrained environments — embedded systems, high-throughput stream processors, certain competitive programming judges — that cost is unacceptable. Morris Traversal, published by Joseph Morris in 1979, solves this with an audacious idea: temporarily rewrite the tree's own null pointers to encode traversal state, then undo every change before you leave.
The problem Morris Traversal solves is surprisingly fundamental: how do you know where to 'go back' after finishing a left subtree, without a stack keeping that address for you? The classic iterative approach just swaps the implicit call stack for an explicit one — same asymptotic cost, different constant. Morris's insight was that null right-child pointers in inorder predecessors are wasted space. There are exactly n-1 edges in a binary tree, leaving n+1 null pointers sitting idle. He used a subset of those to store temporary back-links — called 'threads' — that guide traversal back up the tree without any auxiliary structure.
By the end of this article you'll be able to implement Morris Inorder and Morris Preorder from scratch, explain the thread-creation and thread-detection logic in an interview, reason about exactly when the tree is in a temporarily modified state (and why that matters for concurrent code), and confidently handle every edge case — single nodes, skewed trees, complete binary trees, and trees with duplicate values.
What is Morris Traversal? — Plain English
Morris Traversal performs in-order traversal of a binary tree using O(1) extra space — no recursion stack, no auxiliary stack. The trick is to temporarily modify the tree by creating 'threads': right pointers that point back to the in-order successor. Normal in-order traversal uses a call stack (O(h) space) to remember where to return after finishing the left subtree. Morris Traversal avoids this by using the rightmost node of each left subtree as a bookmark — it threads a right pointer from that node back to the current node, allowing the traversal to return without a stack. After using the thread, it is removed to restore the original tree. The tree ends up unchanged after the traversal completes.
How Morris Traversal Works — Step by Step
Algorithm for in-order Morris Traversal:
- Set current = root.
- While current is not None:
- a. If current has no left child: visit current (append to result), then current = current.right.
- b. If current has a left child:
- i. Find the in-order predecessor: the rightmost node of current.left subtree (predecessor = current.left, then go right until right is None OR right == current).
- ii. If predecessor.right == None (no thread yet): create thread: predecessor.right = current. Move current = current.left.
- iii. If predecessor.right == current (thread exists, we're returning): remove thread: predecessor.right = None. Visit current. Move current = current.right.
- When current becomes None, traversal is complete.
Worked Example — Tracing Morris Traversal on a Tree
Tree: 4 / \ 2 5 / \ 1 3
In-order should produce: [1,2,3,4,5].
current=4. Has left. Predecessor of 4 = rightmost of 2's subtree = node 3 (3.right=None). Create thread: 3.right=4. Move current=2. current=2. Has left. Predecessor of 2 = rightmost of 1's subtree = node 1 (1.right=None). Create thread: 1.right=2. Move current=1. current=1. No left. Visit 1 → result=[1]. current=1.right=2 (thread). current=2. Has left. Predecessor of 2 = node 1. 1.right=2 (thread exists). Remove thread: 1.right=None. Visit 2 → result=[1,2]. current=2.right=3. current=3. No left. Visit 3 → result=[1,2,3]. current=3.right=4 (thread). current=4. Has left. Predecessor of 4 = node 3. 3.right=4 (thread exists). Remove thread: 3.right=None. Visit 4 → result=[1,2,3,4]. current=4.right=5. current=5. No left. Visit 5 → result=[1,2,3,4,5]. current=5.right=None. Done.
Implementation
Morris Traversal modifies right pointers temporarily as threads, then restores them. Finding each predecessor takes O(1) amortized time because each edge is traversed at most twice (once to create the thread, once to remove it), giving O(n) total. Space is O(1) — only a few pointers are needed. The tradeoff versus recursive DFS is increased code complexity and the temporary mutation of the tree during traversal.
Complexity Analysis & Trade-offs
Morris Traversal runs in O(n) time and O(1) extra space. The constant factor is higher than recursive or iterative stack approaches because each node's predecessor search may traverse several edges. However, amortised analysis shows each edge is traversed at most twice: once when creating the thread and once when removing it. The total number of pointer assignments is 2*(n-1) = O(n).
- Recursive: O(n) time, O(h) stack space (h = height, worst-case O(n)).
- Iterative with explicit stack: O(n) time, O(h) space (similar, but stack allocated on heap).
- Morris: O(n) time, O(1) extra space — but mutates the tree during traversal.
- Stack space is extremely limited (embedded, kernel).
- You cannot allocate dynamic memory (real-time systems).
- The interviewer explicitly asks for O(1) space solution.
- The tree is shared with concurrent readers — mutation causes race conditions.
- The tree is read-only (you'd need to copy it first).
- Code clarity is more important than memory (Morris is harder to read/debug).
Morris Preorder Traversal
The same threading idea works for preorder traversal with a slight modification: visit the node before creating the thread (or before descending into the left subtree). In inorder, you visit after removing the thread. In preorder, you visit the current node as soon as you encounter it, then create the thread and descend.
Algorithm: 1. current = root. 2. While current != None: a. If current.left is None: visit(current) current = current.right b. Else: pred = current.left while pred.right and pred.right is not current: pred = pred.right if pred.right is None: visit(current) // visit before creating thread pred.right = current current = current.left else: pred.right = None current = current.right
The key difference: in preorder, you visit the node when you first reach it (before any thread creation). In inorder, you visit after removing the thread (when returning from left subtree).
The Threading Trick That Bends Time (and Pointers)
Most engineers think Morris Traversal is a party trick. Wrong. It's a production-grade hack that exploits a simple observation: inorder traversal fundamentally needs a way to return to the parent after exhausting the left subtree. Recursion uses the call stack. Iteration uses an explicit stack. Morris says: why not just borrow the right pointer of the rightmost node in the left subtree?
That's the threading. You create a temporary link from the inorder predecessor back to the current node. When you later arrive at that same node through the threaded right pointer, you know the left subtree is fully visited. You break the link and visit the node. This eliminates O(h) space entirely, where h is the height of the tree.
But here's where juniors fold: you must know when to remove the thread. If you forget to cut the temporary link, you'll have a corrupted tree after traversal. The condition is strict — only remove the thread when you re-encounter a node whose left child's rightmost pointer already points back to it. That's the signal: left subtree done, visit now, restore the tree.
predecessor.right = null assignment in the else branch, your binary tree becomes a linked list. No runtime error — just silent corruption. Always restore the tree.The One Constraint That Kills Morris in Real Systems
Morris traversal looks elegant on a whiteboard. In production, its most vocal critics are the developers maintaining immutable trees. Threading requires mutation of the tree structure. If your binary tree lives behind an immutable data structure (common in functional programming or event-sourced systems), Morris is dead on arrival.
Even in mutable trees, consider thread safety. If two threads traverse the same tree concurrently, one will yank the thread from under the other's feet. The temporary links are not atomic — you'll get unpredictable results. For read-heavy workloads, stick with the stack-based approach unless you can guarantee single-threaded access or use a read-copy-update pattern.
But here's where Morris shines: embedded systems with tight memory budgets, or interview loops where the interviewer asks for O(1) space traversal. In those constraints, threading is your only hammer. Just don't use it to traverse a tree that another thread is modifying — you'll be debugging a segfault instead of solving the problem.
Memory-Constrained Sensor Node Crashed During Recursive Tree Walk
- Never assume tree structure. Always handle worst-case skewness.
- In memory-constrained environments, Morris Traversal is safer than recursion or even explicit stacks.
- Always profile stack usage — a single recursive call can hide catastrophic depth.
Insert debug print: print(f'Visiting {current.val} at thread state: {pred.right is current}')Verify the condition: if pred.right is None: create thread; else: remove thread and visit.Key takeaways
Common mistakes to avoid
4 patternsForgetting the second condition in predecessor search
Placing the visit at the wrong point in the algorithm
Not restoring the tree after traversal (thread leak)
Using Morris Traversal on a tree that is being concurrently modified
Practice These on LeetCode
Interview Questions on This Topic
Explain how Morris Traversal achieves O(1) space for in-order traversal.
Frequently Asked Questions
20+ years shipping performance-critical code where algorithms decide the bill. Notes here come from systems that actually shipped.
That's Trees. Mark it forged?
6 min read · try the examples if you haven't