Morris Traversal and O(1)-Space Tree Walks
You write the recursive in-order traversal, it is accepted, and then: “can you do it without recursion?” You write the explicit stack. “Can you do it in O(1) space?” — and this is where most candidates stop, because with no stack and no recursion there is nothing left to remember where to return to.
Morris traversal’s answer is to store the return path in the tree itself. The
rightmost node of a left subtree has a null right pointer doing nothing; borrow it
to point back up at the ancestor. The walk falls off the bottom of the subtree,
lands on that pointer, and arrives exactly where a return would have taken it.
Undo the pointer on the way past and the tree is unchanged at the end.
This is worth knowing for two reasons, and only one of them is the technique. The other is that “O(1) space” in tree questions is a precise claim about what counts — and being able to say why recursion is O(h), why the stack version is also O(h), and where Morris’s O(1) is actually paid for, is the part of the answer that scores.
What you’ll learn
Section titled “What you’ll learn”- The thread — what it is, when it is created, when it is removed, and why each node with a left child is visited exactly twice.
- Why the traversal is still even though it walks down the right spine of every left subtree.
- The honest cost: the tree is temporarily mutated, which rules the technique out in concurrent or read-only settings.
- The variants: Morris pre-order (a two-line change), and why Morris post-order is much worse than the other two.
- The three follow-up questions this pattern is really asked as: LC 94, LC 99 and LC 114.
The cue
Section titled “The cue”Visual intuition
Section titled “Visual intuition”Watch the thread edges appear and disappear. Every node with a left child is arrived at twice: the first time installs the thread, the second consumes it and emits the value.
Count the thread edges across the whole trace: each one is created once and removed once, so the tree ends identical to how it started. The 'threads' watch is never above the number of ancestors currently pending -- that is the stack, expressed in pointers the tree was not using.
And on a left-degenerate tree, where the recursive version would use stack
frames and Python would raise RecursionError:
Each node threads to its parent on the way down, and the chain unwinds one thread at a time on the way back. This is the shape where Morris is not a party trick: at n = 10^5 the recursive solution raises RecursionError and the stack solution allocates a list of 10^5 nodes.
The template
Section titled “The template”def morris_inorder(root):
"""In-order traversal in O(n) time and O(1) extra space."""
out, cur = [], root
while cur:
if cur.left is None:
out.append(cur.val) # visit: nothing is to the left
cur = cur.right # a real edge, or a thread back up
else:
pred = cur.left # in-order predecessor of cur ...
while pred.right and pred.right is not cur:
pred = pred.right # ... = rightmost node of left subtree
if pred.right is None:
pred.right = cur # FIRST arrival: install the thread
cur = cur.left
else:
pred.right = None # SECOND arrival: undo the thread
out.append(cur.val) # ... and only now visit cur
cur = cur.right
return out
def morris_preorder(root):
"""Pre-order: identical, except the visit moves to the FIRST arrival."""
out, cur = [], root
while cur:
if cur.left is None:
out.append(cur.val)
cur = cur.right
else:
pred = cur.left
while pred.right and pred.right is not cur:
pred = pred.right
if pred.right is None:
out.append(cur.val) # <-- the only change: visit here
pred.right = cur
cur = cur.left
else:
pred.right = None
cur = cur.right
return outThree lines carry the whole idea:
while pred.right and pred.right is not cur— theis not curhalf is what makes the second arrival detectable. Without it the loop follows its own thread in a circle and hangs. This is the single most common Morris bug.pred.right = curinstalls the return path. It is a write to the tree, and the reason this technique is unsafe to run on a structure anyone else can see.pred.right = Nonerestores it. If the traversal exits early — areturnthe moment you find the answer — some threads are still installed and the tree is left corrupted. That matters for LC 230, and it is the trap in that problem’s Morris solution.
Dry run
Section titled “Dry run”Tree [4, 2, 6, 1, 3, 5, 7] — the one in the visualization above:
4
/ \
2 6
/ \ / \
1 3 5 7| step | cur | left child? | action | threads live | output |
|---|---|---|---|---|---|
| 1 | 4 | yes | pred = 3 (rightmost of left subtree); thread 3→4; go left | 3→4 | — |
| 2 | 2 | yes | pred = 1; thread 1→2; go left | 3→4, 1→2 | — |
| 3 | 1 | no | visit 1; right is the thread → climb to 2 | 3→4 | 1 |
| 4 | 2 | yes | pred = 1, and 1.right is cur → second arrival: cut thread, visit 2, go right to 3 | 3→4 | 1 2 |
| 5 | 3 | no | visit 3; right is the thread → climb to 4 | — | 1 2 3 |
| 6 | 4 | yes | second arrival: cut thread, visit 4, go right to 6 | — | 1 2 3 4 |
| 7 | 6 | yes | pred = 5; thread 5→6; go left | 5→6 | 1 2 3 4 |
| 8 | 5 | no | visit 5; climb the thread to 6 | — | 1 2 3 4 5 |
| 9 | 6 | yes | second arrival: cut thread, visit 6, go right to 7 | — | … 6 |
| 10 | 7 | no | visit 7; right is null → cur = None, loop ends | — | 1 2 3 4 5 6 7 |
Three things this makes concrete that the code does not:
- Threads live at most
hat a time, and always exactly the pending ancestors. At step 2 the live threads are3→4and1→2— the two nodes whose values have not been emitted yet and whose left subtrees are still being walked. That set is the recursion stack, stored in pointers that were null. - Every value is emitted exactly once, but nodes with a left child are arrived at
twice. 4 and 2 and 6 each appear twice in the
curcolumn. Counting arrivals is how you convince someone the algorithm is and not : the extra work is one downward walk per thread, each edge is traversed a bounded number of times, and arrivals is still . - The tree is fully restored by step 10. The threads column ends empty. Say that out loud when asked “does it modify the tree?” — the answer is “yes, transiently, and it is provably restored if the walk runs to completion”.
Complexity
Section titled “Complexity”| Traversal | Time | Extra space | Notes |
|---|---|---|---|
| Recursive in-order | on a degenerate tree; RecursionError past ~1000 in CPython | ||
| Explicit stack | same bound, no recursion limit | ||
| Morris in-order | two pointers; mutates transiently | ||
| Morris pre-order | one line moved | ||
| Morris post-order | needs a reversed sub-list walk; rarely worth it |
The time claim deserves the argument, because the nested while looks
suspicious. Each edge of the tree is walked at most a constant number of times: once
descending, once during the predecessor search that installs a thread, and once when
that thread is followed back up. Summed over all edges that is pointer
moves — the same reasoning as for a monotonic
stack, where a nested
loop is also amortised.
The variant map
Section titled “The variant map”| Problem / variant | Where the visit happens | The one thing that changes |
|---|---|---|
| LC 94 In-order traversal | second arrival | the base template |
| Morris pre-order | first arrival | move out.append above pred.right = cur |
| Morris post-order | — | reverse-thread on right children and reverse the output, or emit the reversed right spine per thread; four times the code for the same bound |
| LC 99 Recover BST | second arrival | compare each visited value with the previous one; the two out-of-order pairs identify the swapped nodes |
| LC 230 Kth smallest in BST | second arrival | count visits and stop at k — but you must finish undoing the threads first, or the tree is left corrupted |
| LC 114 Flatten to linked list | first arrival | the same threading idea applied directly: for each node, thread its predecessor’s right to its right subtree, then move the left subtree to the right |
| LC 897 Increasing order search tree | second arrival | rewire as you visit instead of appending |
| Sum / min / max in one pass | either | replace the output list with an accumulator, and the space claim becomes unambiguous |
Morris pre-order is worth a moment: it is the same traversal, and only the line where the value is emitted differs. If you can explain why moving one line converts in-order into pre-order, you have understood the two-arrivals structure — and that is a better answer than reciting the code.
Pitfalls
Section titled “Pitfalls”- Omitting
and pred.right is not cur. The predecessor search then follows the thread it installed earlier and loops forever. Symptom: a hang, not a wrong answer. - Forgetting
pred.right = None. The traversal still produces the right values the first time, and leaves a tree with cycles. Any later use of that tree hangs or recurses forever, so the failure appears far from the cause. - Returning early with threads installed (LC 230 with a
kcutoff). The values are correct and the tree is corrupt. Either finish the walk and discard the rest, or restore the outstanding threads before returning. - Claiming O(1) while building a list of every node. If the problem asks for the traversal as a list, the output is and the extra space is . State which you mean; conflating them makes the claim sound wrong.
- Using it on a tree someone else can read. Between the install and the undo, the tree has cycles. In production code that is a data race, and in an interview saying so unprompted is worth more than the algorithm.
- Reaching for Morris first. The recursive version is three lines and correct. Write it, name its space, then offer Morris as the constant-space answer. Leading with Morris reads as memorisation.
- Mixing up predecessor and successor. The thread hangs from the rightmost node
of the left subtree (the in-order predecessor) to
cur. Threading from the leftmost node of the right subtree is a different, broken algorithm.
Interview follow-ups
Section titled “Interview follow-ups”| They ask | What they’re checking | The answer |
|---|---|---|
| “Do it in O(1) space” | Whether you know this exists | Morris: thread the in-order predecessor’s null right pointer back to the current node, so the walk returns without a stack. time, two pointers of extra space |
| “Why is it O(n) and not O(n log n)?” | Amortised reasoning | Each edge is traversed a bounded number of times — down, during one predecessor search, and back up along one thread — so at most about pointer moves in total |
| “Does it modify the tree?” | Honesty and precision | Yes, transiently. Every thread installed is removed on the second arrival, so a completed traversal leaves the tree identical. An early exit does not, and concurrent readers would observe cycles |
| “Make it pre-order” | Whether you understand the structure | Move the visit from the second arrival to the first — one line. In-order emits when the left subtree is finished; pre-order emits before descending |
| “What about post-order?” | Judgement | Possible — mirror the algorithm on left children and reverse the output — but it is four times the code for the same asymptotics. In an interview I would say that and then write the stack version |
| “Where does Morris actually beat the stack version?” | Whether you can justify the trade | Only when is genuinely unaffordable: a degenerate tree of nodes, or an embedded/memory-bounded setting. Otherwise the stack version is clearer and safe under sharing |
| “Use it for LC 230, stopping at k” | The early-exit trap | Count on each visit, but do not return the instant the count hits k — first undo the outstanding threads, otherwise you hand back a tree containing cycles |
| “Could you find the two swapped nodes in a BST in O(1) space?” (LC 99) | Composition | Yes — Morris in-order, comparing each value to the previous. The first descent gives the larger node, the second the smaller; with only one descent the pair is adjacent |
Practice
Section titled “Practice”Work down the ladder. Tick each problem off as you go — progress is saved in this browser, and the Export button in the filter bar writes it to a file you can keep.
- 94Binary Tree Inorder Traversaleasy
- 897Increasing Order Search Treeeasy
- 114Flatten Binary Tree to Linked Listmedium
- 173Binary Search Tree Iteratormedium
- 230Kth Smallest Element in a BSTmedium
- 99Recover Binary Search Treehard
Exercises
Section titled “Exercises”LC 94 — Binary Tree Inorder Traversal, in O(1) space · Easy
Section titled “LC 94 — Binary Tree Inorder Traversal, in O(1) space · Easy”LC 99 — Recover Binary Search Tree, in O(1) space · Hard
Section titled “LC 99 — Recover Binary Search Tree, in O(1) space · Hard”LC 114 — Flatten Binary Tree to Linked List, in O(1) space · Medium
Section titled “LC 114 — Flatten Binary Tree to Linked List, in O(1) space · Medium”Self-check
Section titled “Self-check”-
What exactly is a Morris 'thread'?
It reuses a pointer that was null. That is why the space is O(1): the return information moves into space the data structure was not using.
pch.quizShowAnswer
B — A temporary right pointer from the rightmost node of a left subtree back up to that subtree's root's ancestor — replacing the stack entry that would have said where to return — It reuses a pointer that was null. That is why the space is O(1): the return information moves into space the data structure was not using.
-
Why does the predecessor search need the condition `pred.right is not cur`?
The symptom of dropping it is a hang, not a wrong answer, which makes it hard to spot by testing on small inputs. The condition does double duty: loop guard and arrival counter.
pch.quizShowAnswer
B — Because without it the search follows the thread it installed earlier and loops forever — it is also the test that distinguishes a first arrival from a second — The symptom of dropping it is a hang, not a wrong answer, which makes it hard to spot by testing on small inputs. The condition does double duty: loop guard and arrival counter.
-
Does Morris traversal modify the tree?
The precise version of this answer is what an interviewer is after: restored if the walk completes, corrupt if you exit early, and unsafe for concurrent readers either way.
pch.quizShowAnswer
B — Yes, transiently — each thread is removed on the second arrival, so a completed traversal leaves the tree identical, but mid-walk it contains cycles — The precise version of this answer is what an interviewer is after: restored if the walk completes, corrupt if you exit early, and unsafe for concurrent readers either way.
-
You adapt Morris to LC 230 and return as soon as the visit count reaches k. What is wrong?
The values returned are correct, which is what makes this dangerous. Either walk to completion and discard the rest, or explicitly undo the outstanding threads before returning.
pch.quizShowAnswer
B — Threads installed for ancestors you never returned to are still in the tree, so you hand back a structure containing cycles — The values returned are correct, which is what makes this dangerous. Either walk to completion and discard the rest, or explicitly undo the outstanding threads before returning.
-
How do you turn Morris in-order into Morris pre-order?
One line moves. Being able to say why is a better demonstration of understanding than reproducing the in-order code: in-order emits once the left subtree is done, pre-order emits before descending into it.
pch.quizShowAnswer
B — Move the visit from the second arrival to the first — emit the value just before installing the thread and descending left — One line moves. Being able to say why is a better demonstration of understanding than reproducing the in-order code: in-order emits once the left subtree is done, pre-order emits before descending into it.
-
Why is the nested `while` still O(n) overall?
Same amortised argument as the monotonic stack: count total work per edge rather than worst-case work per iteration. On a balanced tree the inner loop is short; on a degenerate one it is long but happens rarely.
pch.quizShowAnswer
B — Because each edge is traversed a bounded number of times — descending, during one predecessor search, and once following the thread back up — for about 3n pointer moves total — Same amortised argument as the monotonic stack: count total work per edge rather than worst-case work per iteration. On a balanced tree the inner loop is short; on a degenerate one it is long but happens rarely.
Recall card
Section titled “Recall card”- Cue — “O(1) extra space” asked of a tree traversal, or a follow-up to the stack version.
- Idea — store the return path in the tree: thread the in-order predecessor’s null right pointer up to the current node.
- Loop — no left child → visit, go right. Left child → find rightmost of the
left subtree; if its right is null, thread it and descend left; if it is
cur, cut the thread, visit, go right. - Two arrivals — every node with a left child is reached twice: install, then consume. In-order visits on the second, pre-order on the first.
- Cost — time (each edge walked a constant number of times), extra space.
- Caveats — the tree has cycles mid-walk; an early exit leaves it corrupt; unsafe with concurrent readers.
- Ladder — LC 94 (the base), LC 99 (compare with the previous value), LC 114 (the same rewiring, made permanent).
- Morris traversal removes the stack by putting the return path into pointers the tree was not using. That is the only idea; everything else is the bookkeeping that installs and removes them.
- The structure to hold in your head is two arrivals per node with a left child — which also explains pre-order (visit on the first) and the bound (bounded work per edge).
- The honest cost is transient mutation. Say it before you are asked; it is the difference between reciting an algorithm and understanding a trade-off.
- Do not lead with it. Recursion, then the stack, then “and if you want space…” — that ordering is what the follow-up is testing.
Next: Binary Lifting and Sparse LCA — the other way to answer ancestor questions, when a single tree serves millions of queries.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading