|
| 1 | +# 5.6 Binary Tree Inorder Traversal (Iterative) |
| 2 | + |
| 3 | +> **Source:** [`src/main/kotlin/tree/BInaryTreeInOrderTraversalIterative.kt`](https://github.com/arpanpathak/AdvancedAlgorithmPatterns/blob/main/src/main/kotlin/tree/BInaryTreeInOrderTraversalIterative.kt) |
| 4 | +> **Pattern:** explicit stack · **Core page** |
| 5 | +
|
| 6 | +## The Problem |
| 7 | + |
| 8 | +Given the root of a binary tree, return the **in-order traversal** of its nodes' values — left subtree, node, right subtree — **without recursion**. |
| 9 | + |
| 10 | +- Constraints: $0 \le n \le 100$ (LeetCode), but the *point* is trees large or skewed enough that the $O(h)$ call stack would be a real risk. |
| 11 | + |
| 12 | +## Examples |
| 13 | + |
| 14 | +``` |
| 15 | +Input: 1 |
| 16 | + \ |
| 17 | + 2 |
| 18 | + / |
| 19 | + 3 |
| 20 | +Output: [1, 3, 2] |
| 21 | +
|
| 22 | +Input: root = [] -> Output: [] |
| 23 | +Input: root = [1] -> Output: [1] |
| 24 | +``` |
| 25 | + |
| 26 | +## Intuition — the call stack becomes an explicit stack |
| 27 | + |
| 28 | +Recursion's in-order is deceptively simple: `inorder(node) = inorder(node.left); visit(node); inorder(node.right)`. The recursion *implicitly* uses the call stack to remember "I was here, now resume with the right subtree." The iterative version must build that same memory **by hand** — which is exactly why the interviewer asks. |
| 29 | + |
| 30 | +The shape of the stack is the insight: you never visit a node when you first see it — you **push** it and keep going left. Only when you can't go left anymore do you **pop** and visit, then step right. In terms of the [traversal table](pattern-primer.md): in-order visits the left spine first, then the node, then the right spine — and the stack *is* the spine. |
| 31 | + |
| 32 | +**The two-phase loop:** the outer `while (current != null || stack.isNotEmpty())` has two inner phases — a *descend* phase (push and go left) and a *visit* phase (pop, record, jump right). Each node is pushed exactly once and popped exactly once, so the whole walk is $O(n)$. |
| 33 | + |
| 34 | +## Approach 1 — Recursion (baseline, for contrast) |
| 35 | + |
| 36 | +```kotlin |
| 37 | +fun inorder(root: TreeNode?): List<Int> = root?.let { |
| 38 | + inorder(it.left) + listOf(it.`val`) + inorder(it.right) |
| 39 | +} ?: emptyList() |
| 40 | +``` |
| 41 | + |
| 42 | +Two lines, obviously correct — and exactly why this problem exists: the *implicit* stack costs $O(h)$ real call-stack memory, which overflows on a skewed tree of ~$10^5$ nodes. The iterative version below is the same algorithm with the stack made explicit and moved to the heap. |
| 43 | + |
| 44 | +## Approach 2 — Explicit-stack simulation (the repo's version, optimal) |
| 45 | + |
| 46 | +```kotlin |
| 47 | +class BInaryTreeInOrderTraversalIterative { |
| 48 | + /** |
| 49 | + * @param root the root of the binary tree |
| 50 | + * @return the in-order (left, node, right) node values |
| 51 | + */ |
| 52 | + fun inorderTraversal(root: TreeNode?): List<Int> { |
| 53 | + val stack = ArrayDeque<TreeNode>() |
| 54 | + val result = mutableListOf<Int>() |
| 55 | + var current = root |
| 56 | + |
| 57 | + while (current != null || stack.isNotEmpty()) { |
| 58 | + // Traverse to the leftmost node |
| 59 | + while (current != null) { |
| 60 | + stack.addLast(current) |
| 61 | + current = current.left |
| 62 | + } |
| 63 | + |
| 64 | + // Visit the node |
| 65 | + current = stack.removeLast() |
| 66 | + result.add(current.`val`) |
| 67 | + |
| 68 | + // Move to the right subtree |
| 69 | + current = current.right |
| 70 | + } |
| 71 | + return result |
| 72 | + } |
| 73 | +} |
| 74 | +``` |
| 75 | + |
| 76 | +```java |
| 77 | +import java.util.*; |
| 78 | + |
| 79 | +public class BinaryTreeInorderTraversalIterative { |
| 80 | + /** |
| 81 | + * @param root the root of the binary tree |
| 82 | + * @return the in-order (left, node, right) node values |
| 83 | + */ |
| 84 | + public List<Integer> inorderTraversal(TreeNode root) { |
| 85 | + Deque<TreeNode> stack = new ArrayDeque<>(); |
| 86 | + List<Integer> result = new ArrayList<>(); |
| 87 | + TreeNode current = root; |
| 88 | + |
| 89 | + while (current != null || !stack.isEmpty()) { |
| 90 | + while (current != null) { // descend the left spine |
| 91 | + stack.push(current); |
| 92 | + current = current.left; |
| 93 | + } |
| 94 | + current = stack.pop(); // leftmost unvisited node |
| 95 | + result.add(current.val); // visit |
| 96 | + current = current.right; // now its right subtree |
| 97 | + } |
| 98 | + return result; |
| 99 | + } |
| 100 | +} |
| 101 | +``` |
| 102 | + |
| 103 | +```cpp |
| 104 | +#include <vector> |
| 105 | +#include <stack> |
| 106 | + |
| 107 | +class BInaryTreeInOrderTraversalIterative { |
| 108 | +public: |
| 109 | + /** |
| 110 | + * @param root the root of the binary tree |
| 111 | + * @return the in-order (left, node, right) node values |
| 112 | + */ |
| 113 | + std::vector<int> inorderTraversal(TreeNode* root) { |
| 114 | + std::vector<int> result; |
| 115 | + std::stack<TreeNode*> st; |
| 116 | + TreeNode* current = root; |
| 117 | + |
| 118 | + while (current != nullptr || !st.empty()) { |
| 119 | + while (current != nullptr) { // descend the left spine |
| 120 | + st.push(current); |
| 121 | + current = current->left; |
| 122 | + } |
| 123 | + current = st.top(); |
| 124 | + st.pop(); |
| 125 | + result.push_back(current->val); // visit |
| 126 | + current = current->right; // now its right subtree |
| 127 | + } |
| 128 | + return result; |
| 129 | + } |
| 130 | +}; |
| 131 | +``` |
| 132 | + |
| 133 | +```python |
| 134 | +def inorder_traversal(root: TreeNode | None) -> list[int]: |
| 135 | + """ |
| 136 | + @param root: the root of the binary tree |
| 137 | + @return: the in-order (left, node, right) node values |
| 138 | + """ |
| 139 | + stack: list[TreeNode] = [] |
| 140 | + result: list[int] = [] |
| 141 | + current = root |
| 142 | + |
| 143 | + while current is not None or stack: |
| 144 | + while current is not None: # descend the left spine |
| 145 | + stack.append(current) |
| 146 | + current = current.left |
| 147 | + current = stack.pop() # leftmost unvisited node |
| 148 | + result.append(current.val) # visit |
| 149 | + current = current.right # now its right subtree |
| 150 | + return result |
| 151 | +``` |
| 152 | + |
| 153 | +```rust |
| 154 | +impl Solution { |
| 155 | + /// @param root the root of the binary tree |
| 156 | + /// @return the in-order (left, node, right) node values |
| 157 | + pub fn inorder_traversal(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<i32> { |
| 158 | + let mut stack: Vec<Rc<RefCell<TreeNode>>> = Vec::new(); |
| 159 | + let mut result = Vec::new(); |
| 160 | + let mut current = root; |
| 161 | + |
| 162 | + while current.is_some() || !stack.is_empty() { |
| 163 | + while let Some(node) = current { // descend the left spine |
| 164 | + stack.push(node.clone()); |
| 165 | + current = node.borrow().left.clone(); |
| 166 | + } |
| 167 | + let node = stack.pop().unwrap(); // leftmost unvisited node |
| 168 | + result.push(node.borrow().val); // visit |
| 169 | + current = node.borrow().right.clone(); // now its right subtree |
| 170 | + } |
| 171 | + result |
| 172 | + } |
| 173 | +} |
| 174 | +``` |
| 175 | + |
| 176 | +> **Rust note:** `stack` holds `Rc` clones (cheap refcount bumps); each node is cloned on push and dropped after pop — net effect: every node alive on the stack exactly once, mirroring the other languages. |
| 177 | +
|
| 178 | +## Dry run |
| 179 | + |
| 180 | +**Input:** the tree above (`1 -> right 2 -> left 3`). |
| 181 | + |
| 182 | +``` |
| 183 | +stack=[], current=1, result=[] |
| 184 | + descend: push 1, current=null stack=[1] |
| 185 | + visit: pop 1, result=[1], current=2 |
| 186 | + descend: push 2, current=3; push 3, current=null stack=[2,3] |
| 187 | + visit: pop 3, result=[1,3], current=null |
| 188 | + visit: pop 2, result=[1,3,2], current=null |
| 189 | + stack empty, current=null -> stop |
| 190 | +Output: [1, 3, 2] ✓ |
| 191 | +``` |
| 192 | + |
| 193 | +A nice property of in-order on a BST: the result is the *sorted* order — the same `[1, 2, 3]`-style sequence you'd get from a sorted array, which is why "in-order = sorted" is the standard BST litmus test. |
| 194 | + |
| 195 | +## Complexity |
| 196 | + |
| 197 | +**Time.** Each node pushed once, popped once: |
| 198 | + |
| 199 | +$$ |
| 200 | +T(n) = O(n) |
| 201 | +$$ |
| 202 | + |
| 203 | +**Space.** The stack holds at most one left-spine at a time — worst case is a skewed tree: |
| 204 | + |
| 205 | +$$ |
| 206 | +S(n) = O(h) \text{ on the heap (no call-stack risk)}, \quad h \in [\log n, n] |
| 207 | +$$ |
| 208 | + |
| 209 | +This is the whole point of the problem: identical asymptotics to recursion, but the memory lives on the *heap*, so a $10^5$-node skewed tree runs where recursion would `StackOverflowError`. |
| 210 | + |
| 211 | +## Variants & follow-ups |
| 212 | + |
| 213 | +- **Iterative pre/post-order** — same skeleton, different visit timing: pre-order visits at *push* time; post-order needs a "was I here before?" marker (two-stack or reversed-pre-order tricks). |
| 214 | +- **Morris Traversal** — $O(1)$ space by temporarily *threading* right pointers: if a node's left subtree has no rightmost node yet, wire it to the current node, walk left; when you return, unthread and visit. Interviewers rarely demand it — mention it as the "constant-space curiosity." |
| 215 | +- **BST Iterator** (`src/main/kotlin/tree/bst/`) — this exact loop turned into `hasNext()` / `next()`: the stack *is* the iterator state, and each `next()` does one descend+visit. |
| 216 | +- **Kth Smallest Element In A BST** — run this traversal and stop at the $k$-th visit. |
| 217 | +- **Interview follow-up:** "Recursive version?" Write it first — two lines, obviously correct — *then* offer this page as the stack-safe refinement. Showing both and explaining the tradeoff is the answer. |
0 commit comments