Topics › Binary Trees › Hard Problems
Morris Inorder Traversal
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
Return the inorder traversal of a binary tree (left subtree, node, right subtree) as a list of values, using only O(1) extra memory besides the output list: no recursion and no explicit stack. You may modify the tree while the traversal runs, but it must be exactly as it was when the method returns.
Constraints (LeetCode 94, whose follow-up asks for this): up to 100 nodes, values in [-100, 100]. The real requirement is the memory bound; the Java file also runs a 100000-node tree and checks that the tree is unchanged afterwards.
Example 1
1
/ \
2 3
/ \ \
4 5 6
answer = [4, 2, 5, 1, 3, 6]
The left subtree of 1 gives 4, 2, 5, then 1, then the right subtree gives 3, 6.
Example 2
root = [1, null, 2, 3] // 1 has a right child 2, and 2 has a left child 3
answer = [1, 3, 2]
Approach 1: Recursion
Intuition. Inorder is defined recursively, so the simplest correct solution follows the definition: traverse the left subtree, record the node, traverse the right subtree. It is the baseline the other approaches must match, but it uses the call stack, which is O(h) memory.
Algorithm.
- If the node is
null, return. - Recurse left, append the node’s value, recurse right.
static List<Integer> recursive(TreeNode root) {
List<Integer> out = new ArrayList<>();
inorder(root, out);
return out;
}
static void inorder(TreeNode node, List<Integer> out) {
if (node == null) return;
inorder(node.left, out);
out.add(node.val);
inorder(node.right, out);
}
Complexity. Time O(n), each node is visited once. Space O(h) for the call stack, where h is the height: O(log n) for a balanced tree, O(n) for a chain.
Approach 2: Explicit stack
Intuition. Replace the call stack with our own stack. Push the whole left spine; the top node then has a finished left side, so pop it, record it, and continue with its right child. This avoids recursion but still stores up to h nodes, so it does not meet the O(1) bound.
Algorithm.
cur = root. Whilecuris notnullor the stack is not empty:- Push
curand move left untilcurisnull. - Pop, record, set
curto the popped node’s right child.
static List<Integer> iterative(TreeNode root) {
List<Integer> out = new ArrayList<>();
Deque<TreeNode> stack = new ArrayDeque<>();
TreeNode cur = root;
while (cur != null || !stack.isEmpty()) {
while (cur != null) {
stack.push(cur);
cur = cur.left;
}
cur = stack.pop();
out.add(cur.val);
cur = cur.right;
}
return out;
}
Complexity. Time O(n), each node is pushed and popped once. Space O(h) for the stack.
Approach 3: Morris traversal
Intuition. Why do we need a stack at all? Only to find our way back up to a node after finishing its left subtree. Ask where that left subtree’s traversal ends: at its rightmost node, which is exactly the inorder predecessor of the current node. Being the rightmost node, its right pointer is null, which is an unused slot.
Morris traversal borrows that slot. Before descending into the left subtree of cur, it sets pred.right = cur. This temporary link is called a thread. When the traversal later finishes the left subtree, it reaches pred and simply follows right as usual, which carries it back to cur. No stack is needed because the path back up is stored inside the tree itself.
That means every node with a left child is reached twice, and we need to tell the two arrivals apart. We look for the predecessor again. If pred.right is null, this is the first arrival: plant the thread and go left. If pred.right == cur, we came back through the thread, so the left subtree is done: remove the thread (restoring the tree), record cur, and go right. A node without a left child has nothing to wait for, so it is recorded immediately and we go right; that right may be a real child or a thread leading back to an ancestor.
Algorithm.
cur = root.- While
cur != null:- If
cur.left == null: recordcur, setcur = cur.right. - Otherwise let
predbe the rightmost node ofcur.left, stopping early ifpred.right == cur.- If
pred.right == null(first arrival): setpred.right = cur, thencur = cur.left. - Otherwise (second arrival): set
pred.right = null, recordcur, thencur = cur.right.
- If
- If
static List<Integer> morris(TreeNode root) {
List<Integer> out = new ArrayList<>();
TreeNode cur = root;
while (cur != null) {
if (cur.left == null) { // nothing to the left: visit and move on
out.add(cur.val);
cur = cur.right; // may be a real child or a thread back to an ancestor
continue;
}
TreeNode pred = cur.left; // inorder predecessor = rightmost node of the left subtree
while (pred.right != null && pred.right != cur) pred = pred.right;
if (pred.right == null) { // first visit of cur: plant the thread, go left
pred.right = cur;
cur = cur.left;
} else { // came back through the thread: left side is done
pred.right = null; // restore the tree
out.add(cur.val);
cur = cur.right;
}
}
return out;
}
Dry run on Example 1:
cur |
situation | action | output so far |
|---|---|---|---|
| 1 | pred = 5, no thread | thread 5 → 1, go left | |
| 2 | pred = 4, no thread | thread 4 → 2, go left | |
| 4 | no left child | record, follow right (thread) to 2 | 4 |
| 2 | pred = 4, thread found | remove thread, record, go right | 4, 2 |
| 5 | no left child | record, follow right (thread) to 1 | 4, 2, 5 |
| 1 | pred = 5, thread found | remove thread, record, go right | 4, 2, 5, 1 |
| 3 | no left child | record, go right | 4, 2, 5, 1, 3 |
| 6 | no left child | record, go right (null) |
4, 2, 5, 1, 3, 6 |
Every thread that was planted is removed, so the tree ends exactly as it started.
Complexity. Time O(n). The predecessor searches look expensive, but count edges: each search walks down the right spine of cur.left. An edge x → x.right belongs to the right spine of the left subtree of at most one node (the nearest ancestor whose left child starts that spine), so across the whole run every edge is walked at most twice by predecessor searches, once when the thread is planted and once when it is removed. Together with the ordinary moves this is O(n). Space O(1): only cur and pred, apart from the output list.
Edge cases and pitfalls
- Empty tree. The loop never runs and the result is empty.
- The stop condition of the predecessor search. The inner loop must stop on
pred.right == curas well as onnull. Without the second check, the second arrival would follow the thread back intocurand loop forever. - Recording in the wrong branch. Inorder records in the “no left child” branch and on the second arrival. Recording on the first arrival gives preorder instead.
- Leaving threads behind. Stopping early (for example, after finding the k-th element) leaves threads in place, so the tree may now contain cycles. Either finish the traversal or undo the remaining threads first. The tests compare the tree with a deep copy after every run and then run Morris a second time.
- Concurrency. The tree is modified temporarily, so nobody else may read it during the traversal. That is the price of
O(1)space. - Duplicates. Threads are detected by reference (
pred.right == cur), never by value, so repeated values are safe. The tests include a tree of equal values.
Related problems
- Morris Preorder Traversal records each node on its first arrival instead.
- Inorder Traversal covers the recursive and stack versions in detail.
- Morris Inorder Traversal of a Binary Tree is the same problem in the A2Z sheet.
- Kth Smallest and Largest element in BST can use Morris inorder for constant extra space.
- Correct BST with two nodes swapped is an inorder walk that Morris makes
O(1)in space. - Flatten Binary Tree to Linked List uses the same “rightmost node of the left subtree” rewiring idea.