Skip to content

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.

  • 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 O(n)O(n) 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.

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.

treeMorris in-order on a balanced BST: threads stand in for the call stackLC 94 · O(n) time, O(1) space
1234567
cur4threads0visited0
startIn-order without recursion and without a stack. The trick: before descending into a left subtree, hang a temporary **thread** from that subtree's rightmost node back up to the current node. When the walk falls off the bottom right of the subtree it lands on the thread and arrives back at the ancestor — which is exactly what popping the call stack would have done, at O(1) extra space instead of O(h).
1/12

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 O(n)O(n) stack frames and Python would raise RecursionError:

treeA left-leaning chain: recursion needs n frames, Morris needs two variablesthe case that motivates it
12345
cur5threads0visited0
startIn-order without recursion and without a stack. The trick: before descending into a left subtree, hang a temporary **thread** from that subtree's rightmost node back up to the current node. When the walk falls off the bottom right of the subtree it lands on the thread and arrives back at the ancestor — which is exactly what popping the call stack would have done, at O(1) extra space instead of O(h).
1/8

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.

morris.py
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 out

Three lines carry the whole idea:

  • while pred.right and pred.right is not cur — the is not cur half 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 = cur installs 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 = None restores it. If the traversal exits early — a return the 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.

Tree [4, 2, 6, 1, 3, 5, 7] — the one in the visualization above:

text
        4
      /   \
     2     6
    / \   / \
   1   3 5   7
stepcurleft child?actionthreads liveoutput
14yespred = 3 (rightmost of left subtree); thread 34; go left34
22yespred = 1; thread 12; go left34, 12
31novisit 1; right is the thread → climb to 2341
42yespred = 1, and 1.right is cur → second arrival: cut thread, visit 2, go right to 3341 2
53novisit 3; right is the thread → climb to 41 2 3
64yessecond arrival: cut thread, visit 4, go right to 61 2 3 4
76yespred = 5; thread 56; go left561 2 3 4
85novisit 5; climb the thread to 61 2 3 4 5
96yessecond arrival: cut thread, visit 6, go right to 7… 6
107novisit 7; right is null → cur = None, loop ends1 2 3 4 5 6 7

Three things this makes concrete that the code does not:

  • Threads live at most h at a time, and always exactly the pending ancestors. At step 2 the live threads are 34 and 12 — 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 cur column. Counting arrivals is how you convince someone the algorithm is O(n)O(n) and not O(nlogn)O(n \log n): the extra work is one downward walk per thread, each edge is traversed a bounded number of times, and 2n2n arrivals is still O(n)O(n).
  • 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”.
TraversalTimeExtra spaceNotes
Recursive in-orderO(n)O(n)O(h)O(h)O(n)O(n) on a degenerate tree; RecursionError past ~1000 in CPython
Explicit stackO(n)O(n)O(h)O(h)same bound, no recursion limit
Morris in-orderO(n)O(n)O(1)O(1)two pointers; mutates transiently
Morris pre-orderO(n)O(n)O(1)O(1)one line moved
Morris post-orderO(n)O(n)O(1)O(1)needs a reversed sub-list walk; rarely worth it

The O(n)O(n) 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 3(n1)\le 3(n-1) pointer moves — the same reasoning as for a monotonic stack, where a nested loop is also amortised.

Problem / variantWhere the visit happensThe one thing that changes
LC 94 In-order traversalsecond arrivalthe base template
Morris pre-orderfirst arrivalmove out.append above pred.right = cur
Morris post-orderreverse-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 BSTsecond arrivalcompare each visited value with the previous one; the two out-of-order pairs identify the swapped nodes
LC 230 Kth smallest in BSTsecond arrivalcount visits and stop at k — but you must finish undoing the threads first, or the tree is left corrupted
LC 114 Flatten to linked listfirst arrivalthe 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 treesecond arrivalrewire as you visit instead of appending
Sum / min / max in one passeitherreplace 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.

  • 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 k cutoff). 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 O(n)O(n) and the extra space is O(1)O(1). 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 O(h)O(h) 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.
They askWhat they’re checkingThe answer
“Do it in O(1) space”Whether you know this existsMorris: thread the in-order predecessor’s null right pointer back to the current node, so the walk returns without a stack. O(n)O(n) time, two pointers of extra space
“Why is it O(n) and not O(n log n)?”Amortised reasoningEach edge is traversed a bounded number of times — down, during one predecessor search, and back up along one thread — so at most about 3n3n pointer moves in total
“Does it modify the tree?”Honesty and precisionYes, 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 structureMove 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?”JudgementPossible — 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 tradeOnly when O(h)O(h) is genuinely unaffordable: a degenerate tree of 10510^5 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 trapCount 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)CompositionYes — 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
6 problems
2 easy3 medium1 hard

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.

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”
pch.quizTag Morris traversal — self-check
  1. What exactly is a Morris 'thread'?

    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.

  2. Why does the predecessor search need the condition `pred.right is not cur`?

    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.

  3. Does Morris traversal modify the tree?

    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.

  4. You adapt Morris to LC 230 and return as soon as the visit count reaches k. What is wrong?

    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.

  5. How do you turn Morris in-order into Morris pre-order?

    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.

  6. Why is the nested `while` still O(n) overall?

    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.

  • 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.
  • CostO(n)O(n) time (each edge walked a constant number of times), O(1)O(1) 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 O(h)O(h) 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 O(n)O(n) 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 O(1)O(1) 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.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading