Topics › Binary Trees › Hard Problems
Morris Preorder Traversal of a Binary Tree
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
Return the preorder traversal of a binary tree (node, then left subtree, then right subtree) as a list of values, using only O(1) extra space apart from the output list. No recursion and no explicit stack. You may modify the tree temporarily, but it must be exactly as it was when the method returns.
Constraints (LeetCode 144): up to 100 nodes, values in [-100, 100]. The space restriction is the point of the exercise; the Java file tests a 100000-node tree as well.
Example 1
1
/ \
2 3
/ \ \
4 5 6
answer = [1, 2, 4, 5, 3, 6]
Example 2
root = [1, null, 2, 3] // 1 has a right child 2, and 2 has a left child 3
answer = [1, 2, 3]
Approach 1: Recursion
Intuition. Preorder is defined recursively, so the code is the definition: record the node, traverse the left subtree, traverse the right subtree. The call stack remembers where to continue after the left subtree is finished.
static List<Integer> recursive(TreeNode root) {
List<Integer> out = new ArrayList<>();
preorder(root, out);
return out;
}
static void preorder(TreeNode node, List<Integer> out) {
if (node == null) return;
out.add(node.val);
preorder(node.left, out);
preorder(node.right, out);
}
Complexity. Time O(n): each node is visited once. Space O(h) for the call stack, where h is the height; that is O(n) for a skewed tree.
Approach 2: Explicit stack
Intuition. Replace the call stack with our own. Pop a node, record it, then push its right child before its left child, so that the left child is popped (and its whole subtree finished) first.
static List<Integer> iterativeStack(TreeNode root) {
List<Integer> out = new ArrayList<>();
if (root == null) return out;
Deque<TreeNode> stack = new ArrayDeque<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode cur = stack.pop();
out.add(cur.val);
if (cur.right != null) stack.push(cur.right); // pushed first so the left child is popped first
if (cur.left != null) stack.push(cur.left);
}
return out;
}
Complexity. Time O(n). Space O(h) for the stack in the worst case. The memory has moved from the call stack to the heap, but it has not gone away.
Approach 3: Morris traversal
Intuition. What is the stack actually for? When we go down into the left subtree of a node cur, we need a way to come back to cur afterwards so that we can continue with cur.right. Morris’s observation is that the tree already has a convenient place to store that “way back”:
- The last node visited in
cur’s left subtree is its rightmost node: start atcur.leftand keep going right. Call itpred(it iscur’s inorder predecessor). predhas no right child, sopred.rightis anullpointer doing nothing.
Morris temporarily sets pred.right = cur. This pointer is called a thread. After the left subtree has been walked, following right pointers from pred leads straight back to cur. When we arrive at cur the second time, we find the thread (pred.right == cur), remove it, and move on to cur.right. So each node with a left child is reached twice, and the thread tells us which time it is:
- First arrival (
pred.right == null): this is when preorder records the node. Then create the thread and go left. - Second arrival (
pred.right == cur): the left subtree is done. Remove the thread and go right.
A node without a left child is recorded immediately and we move right. That right pointer is either a real child or a thread leading back up.
Algorithm.
cur = root. Whilecur != null:- If
cur.left == null: recordcur, setcur = cur.right. - Otherwise find
pred: start atcur.leftand move right whilepred.rightis neithernullnorcur.- If
pred.right == null: recordcur, setpred.right = cur, thencur = cur.left. - Else (
pred.right == cur): setpred.right = null, thencur = cur.right.
- If
static List<Integer> morris(TreeNode root) {
List<Integer> out = new ArrayList<>();
TreeNode cur = root;
while (cur != null) {
if (cur.left == null) {
out.add(cur.val);
cur = cur.right;
} else {
TreeNode pred = cur.left; // rightmost node of the left subtree
while (pred.right != null && pred.right != cur) pred = pred.right;
if (pred.right == null) { // first arrival: visit, leave a thread back, go left
out.add(cur.val);
pred.right = cur;
cur = cur.left;
} else { // second arrival via the thread: left side is done
pred.right = null;
cur = cur.right;
}
}
}
return out;
}
Dry run on Example 1:
| cur | left child? | pred | pred.right was | action | output so far |
|---|---|---|---|---|---|
| 1 | yes | 5 | null | record 1, thread 5→1, go left | 1 |
| 2 | yes | 4 | null | record 2, thread 4→2, go left | 1 2 |
| 4 | no | record 4, follow 4.right (thread) to 2 | 1 2 4 | ||
| 2 | yes | 4 | 2 | remove thread, go right | 1 2 4 |
| 5 | no | record 5, follow 5.right (thread) to 1 | 1 2 4 5 | ||
| 1 | yes | 5 | 1 | remove thread, go right | 1 2 4 5 |
| 3 | no | record 3, go right | 1 2 4 5 3 | ||
| 6 | no | record 6, go right (null) | 1 2 4 5 3 6 |
Both threads were removed, so the tree is back to its original shape.
Complexity. Time O(n). The predecessor search looks expensive, but each search walks down the right spine of one left subtree, and every such spine is walked exactly twice in total: once when the thread is created and once when it is removed. Spines of different nodes do not share edges, so all searches together cost O(n). Space O(1) beyond the output: a couple of pointers.
Morris preorder versus Morris inorder
The two algorithms are identical except for where the node is recorded. Preorder records it on the first arrival, before going left. Inorder records it on the second arrival, after the left subtree is done. If you can write one, change one line and you have the other.
Edge cases and pitfalls
- Empty tree. The loop never runs; the result is empty.
- The predecessor loop condition. It must stop at
pred.right == curas well as atnull. Without the second check, on the second arrival the search follows the thread back tocur, then down the left subtree again, forever. - Recording on the wrong arrival. Recording in the “second arrival” branch gives inorder, not preorder. The node without a left child is recorded in both variants.
- The tree is modified while the traversal runs. Do not stop early (for example, after finding a target value) without first removing the threads that still exist, and do not let another thread of execution read the tree during the traversal. The tests compare the tree with a copy afterwards to confirm it is restored.
- Duplicates and negative values need no special handling; threads compare node references, never values.
Related problems
- Morris Inorder Traversal of a Binary Tree is the same algorithm with the node recorded on the second arrival.
- Preorder Traversal covers the recursive version in detail.
- Iterative Preorder Traversal of Binary Tree covers the explicit-stack version.
- Flatten Binary Tree to Linked List uses the same “rightmost node of the left subtree” pointer trick to rewire a tree in preorder with
O(1)space.