Problem overview
The method PostOrderTraversal receives the root of a binary tree and returns a List<int> with its values in post order: left subtree, right subtree, then the node itself. This lesson answers the statement's follow up and does it iteratively, with a loop and an explicit stack, without recursion.
For the tree [1, null, 2, 3] (1 has the right child 2, and 2 has the left child 3), the answer is [3, 2, 1].
Approach: simulate the recursive calls
The recursive solution does three things in each call: recurse left, recurse right, record the value. The program's call stack remembers which of the three things each pending call still has to do. To remove the recursion, this lesson builds that bookkeeping by hand.
Each entry on the stack is a State: a node plus a step number saying what that simulated call does next.
- Step 0 (
LEFT_CALL): go into the left child. - Step 1 (
RIGHT_CALL): go into the right child. - Step 2 (
APPEND_TO_TRAVERSAL): record the node's value. - Any step above 2: the call is finished.
The loop takes the top entry off the stack and pushes it back with the step advanced by one. Then it performs the action of the step it took off. If that action is a call, it pushes a new entry for the child, starting at step 0. The child sits on top of its parent, so it runs to completion first, and the parent resumes at its next step afterwards. This is exactly how the call stack behaves in the recursive version.
Simpler iterative solutions exist, for example a stack that produces node, right, left order and then reverses the list. The simulation is more code, but it is a general technique: it turns any recursive function into a loop, whatever it does between its calls.
Step by step
- Define
StateinsideSolutionwith a publicTreeNode node, a publicint stepand a constructor that stores both. Each instance is one simulated call. - Name the steps.
LEFT_CALL,RIGHT_CALLandAPPEND_TO_TRAVERSALare 0, 1 and 2. - Create the output list
traversal. - Create the stack as a
Stack<State>. - Push the first call: the root at step
LEFT_CALL. - Loop while the stack is not empty, using
stack.Count > 0. - Read the top with
Peek()intonodeandstep. - Remove it with
Pop(). It is pushed back right away, with the next step, unless the call is finished. - Skip finished calls. If
nodeisnullorstepis aboveAPPEND_TO_TRAVERSAL, callcontinue. The entry is already off the stack, so the call below it resumes on the next iteration. - Push the same call with
step + 1. This must happen before pushing a child. Then, when the child is finished and popped, this call resumes at its next action. - Step 0: push the left child at
LEFT_CALL. - Step 1: push the right child at
LEFT_CALLas well, because every call starts by going left. - Step 2: add
node.valtotraversal. Both children are finished by now, which is what post order requires. - Return
traversalafter the loop.
The solution in C#
class Solution {
class State {
public TreeNode node;
public int step;
public State(TreeNode node, int step) {
this.node = node;
this.step = step;
}
};
public List<int> PostOrderTraversal(TreeNode root) {
int LEFT_CALL = 0;
int RIGHT_CALL = 1;
int APPEND_TO_TRAVERSAL = 2;
List<int> traversal = new List<int>();
Stack<State> stack = new Stack<State>();
stack.Push(new State(root, LEFT_CALL));
while (stack.Count > 0) {
TreeNode node = stack.Peek().node;
int step = stack.Peek().step;
stack.Pop();
if (node == null || step > APPEND_TO_TRAVERSAL) {
continue;
}
stack.Push(new State(node, step + 1));
if (step == LEFT_CALL) {
stack.Push(new State(node.left, LEFT_CALL));
} else if (step == RIGHT_CALL) {
stack.Push(new State(node.right, LEFT_CALL));
} else if (step == APPEND_TO_TRAVERSAL) {
traversal.Add(node.val);
}
}
return traversal;
}
}
C# details:
- Nested class.
Stateis declared insideSolution. Without an access modifier, a nested class is private, so it is only visible insideSolution, which is all this solution needs. Its fields arepublicso the loop can read them. this.node = node. In the constructor, the parameter and the field have the same name, sothis.picks the field.Stack<T>comes fromSystem.Collections.Generic.Pushadds on top,Peekreads the top without removing it,Popremoves and returns it, andCountis the size. CallingPoporPeekon an empty stack throws anInvalidOperationException, which this loop avoids by checkingCountfirst.- The semicolon after a class. The closing
};ofStateis allowed in C#. The semicolon is optional there and changes nothing. - The three step values are plain local variables. They never change, so
const intwould also work.
Tracing the example
For [1, null, 2, 3], written as (node, step), the stack goes through these main stages:
- Start: (1, 0). Pop it, push (1, 1), then push (null, 0) for the missing left child.
- (null, 0) is popped and skipped. Then (1, 1) is popped: push (1, 2) and push (2, 0) for the right child.
- (2, 0) is popped: push (2, 1) and push (3, 0). Then (3, 0) leads to (3, 1) and (3, 2) in the same way, with two empty children skipped on the way. When (3, 2) is popped, 3 is added.
- (3, 3) is skipped as finished. Then (2, 1) leads to (2, 2) and 2 is added.
- Finally (1, 2) is popped and 1 is added, giving
[3, 2, 1].
Complexity
Time: O(n). Each real node is pushed four times: once at each of the steps 0, 1, 2 and the finished step 3, plus the entries for missing children, of which there are at most n + 1. The total number of pushes is proportional to n.
Space: O(h) for the stack, where h is the height of the tree, plus the output list. The stack lives on the heap, so a very deep tree does not overflow the call stack as recursion could.
Edge cases and common mistakes
- Empty tree. The first entry holds a
nullnode. It is popped and skipped, the stack is empty, and an empty list comes back. - Pushing the parent after the child. The parent has to go back on the stack first, with the advanced step. If the child went first, the child would run after its parent instead of before it.
- Starting children at the wrong step. A child is a new call, so it starts at
LEFT_CALL, not at the parent's step. - Not skipping
nullnodes. Readingnode.leftonnullthrows aNullReferenceException. - Peeking without popping. If the entry is never popped, the same state would be processed again forever.
- Appending at the wrong step. Adding the value at step 0 or 1 gives pre order or in order instead of post order.
Testing
[1, null, 2, 3]gives[3, 2, 1].- A single node
[5]gives[5]. - An empty tree gives
[]. - The tree
[1, 2, 3]gives[2, 3, 1].