Morris Traversal Inorder Without Stack in DSA Rust - Time & Space Complexity
We want to understand how the Morris Traversal algorithm performs as the tree size grows.
Specifically, how many steps it takes to visit all nodes without using extra memory for a stack.
Analyze the time complexity of the following Morris Inorder Traversal code.
fn morris_inorder(root: Option<&Box>) {
let mut current = root;
while let Some(node) = current {
if node.left.is_none() {
// Visit node
current = node.right.as_ref();
} else {
let mut pre = node.left.as_ref();
while pre.unwrap().right.is_some() && pre.unwrap().right.as_ref() != current {
pre = pre.unwrap().right.as_ref();
}
if pre.unwrap().right.is_none() {
// Unsafe block needed to modify right pointer
// This code is conceptual; actual Rust code requires mutable references
// pre.unwrap().right = current;
current = node.left.as_ref();
} else {
// pre.unwrap().right = None;
// Visit node
current = node.right.as_ref();
}
}
}
}
This code visits all nodes of a binary tree in order without using a stack or recursion by temporarily modifying tree links.
Look for loops and repeated steps in the code.
- Primary operation: The inner while loop that finds the rightmost node in the left subtree (predecessor).
- How many times: Each edge in the tree is visited at most twice during these pointer changes.
As the number of nodes (n) increases, the algorithm visits each node and its edges a limited number of times.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | About 20 visits (nodes and edges) |
| 100 | About 200 visits |
| 1000 | About 2000 visits |
Pattern observation: The operations grow roughly linearly with the number of nodes.
Time Complexity: O(n)
This means the algorithm visits each node a constant number of times, so the total steps grow directly with tree size.
[X] Wrong: "Because of the inner loop, Morris traversal is slower than normal inorder traversal and can be O(n²)."
[OK] Correct: Each edge is visited at most twice, so the total work is still proportional to n, not n squared.
Understanding Morris traversal shows you can save memory by clever pointer use, a skill valued in interviews and real coding.
"What if we changed the tree to be a linked list (all nodes have only right child)? How would the time complexity change?"
Practice
Solution
Step 1: Understand Morris Traversal space usage
Morris Traversal modifies tree links temporarily to avoid using stack or recursion.Step 2: Compare with traditional methods
Traditional inorder traversal uses stack or recursion which require extra space. Morris Traversal uses O(1) extra space.Final Answer:
It uses no extra space like stack or recursion -> Option DQuick Check:
Space complexity = O(1) [OK]
- Thinking Morris is faster in time complexity
- Assuming it only works on BSTs
- Confusing it with recursive traversal
Solution
Step 1: Identify mutable pointer requirement
Morris Traversal modifies tree links, so mutable access is needed.Step 2: Choose correct Rust method
root.as_mut() gives mutable reference, root.as_ref() is immutable, clone copies, &root is immutable reference.Final Answer:
let mut current = root.as_mut(); -> Option BQuick Check:
Mutable reference needed = let mut current = root.as_mut(); [OK]
- Using as_ref() which is immutable
- Cloning unnecessarily
- Using immutable references when mutation is needed
struct Node {
val: i32,
left: Option,
right: Option,
}
fn morris_inorder(root: &mut Option) {
let mut current = root.as_mut();
while let Some(node) = current {
if node.left.is_none() {
print!("{} ", node.val);
current = node.right.as_mut();
} else {
let mut pre = node.left.as_mut();
while let Some(p) = pre {
if p.right.is_some() && p.right.as_ref().unwrap().val != node.val {
pre = p.right.as_mut();
} else {
break;
}
}
if pre.is_some() && pre.as_ref().unwrap().right.is_none() {
pre.as_mut().unwrap().right = Some(Box::new(Node { val: node.val, left: None, right: None }));
current = node.left.as_mut();
} else {
pre.as_mut().unwrap().right = None;
print!("{} ", node.val);
current = node.right.as_mut();
}
}
}
}
Solution
Step 1: Understand tree structure
Root=1, left=2, right=3. Inorder is left-root-right.Step 2: Trace Morris traversal output
Visit left (2), then root (1), then right (3). So output is "2 1 3".Final Answer:
2 1 3 -> Option AQuick Check:
Inorder traversal = 2 1 3 [OK]
- Confusing preorder or postorder output
- Not restoring links causing infinite loops
- Misreading tree structure
fn morris_inorder_bug(root: &mut Option) {
let mut current = root.as_mut();
while let Some(node) = current {
if node.left.is_some() {
let mut pre = node.left.as_mut();
while pre.is_some() && pre.as_ref().unwrap().right.is_some() {
pre = pre.unwrap().right.as_mut();
}
if pre.is_some() {
pre.as_mut().unwrap().right = Some(Box::new(Node { val: node.val, left: None, right: None }));
current = node.left.as_mut();
}
} else {
print!("{} ", node.val);
current = node.right.as_mut();
}
}
}
Solution
Step 1: Analyze threaded link handling
Code creates temporary right links but never removes them.Step 2: Understand infinite loop cause
Without removing threaded links, traversal cycles endlessly on same nodes.Final Answer:
Not removing temporary threaded links after use -> Option CQuick Check:
Threaded links must be removed to avoid loops [OK]
- Ignoring threaded link removal
- Confusing printing with traversal logic
- Not using mutable references properly
fn morris_inorder_collect(root: &mut Option) -> Vec<i32> {
let mut result = Vec::new();
let mut current = root.as_mut();
while let Some(node) = current {
if node.left.is_none() {
// Change here
current = node.right.as_mut();
} else {
let mut pre = node.left.as_mut();
while let Some(p) = pre {
if p.right.is_some() && p.right.as_ref().unwrap().val != node.val {
pre = p.right.as_mut();
} else {
break;
}
}
if pre.is_some() && pre.as_ref().unwrap().right.is_none() {
pre.as_mut().unwrap().right = Some(Box::new(Node { val: node.val, left: None, right: None }));
current = node.left.as_mut();
} else {
pre.as_mut().unwrap().right = None;
// Change here
current = node.right.as_mut();
}
}
}
result
}
Solution
Step 1: Identify where node values are processed
Values are printed at two places: when left is None and after restoring threaded links.Step 2: Replace printing with pushing to vector
To collect values, replace both prints with result.push(node.val) to keep traversal logic intact.Final Answer:
Replace both comments with result.push(node.val); -> Option AQuick Check:
Collect values by pushing at both print points [OK]
- Mixing print and push causing partial collection
- Pushing only once missing some nodes
- Breaking traversal by adding prints
Ready to prove it?
Pass the test and this pattern moves unknown
