Problem
Implement an iterator that performs a Postorder Traversal on a binary tree. In postorder traversal, the nodes of a binary tree are visited in the following order:
- Left subtree
- Right subtree
- Root node
Iterator Design Pattern
The iterator pattern facilitates traversal over a collection or dataset. Java’s Iterator provides three key methods:
- hasNext(): Returns
true
if there are remaining elements to iterate, otherwisefalse
. - next(): Retrieves the next element in the collection.
- remove(): Deletes the most recently returned element from the collection.
Examples
Example 1
|
|
Example 2
|
|
Solution
Method 1 - Using Stack
Postorder traversal is a depth-first traversal that focuses on visiting a node only after visiting its children. To implement an iterator for postorder traversal:
- We need to ensure that we traverse the left and right subtree before visiting the root node.
- Unlike recursive traversal, the iterator is implemented using a stack (since recursion internally uses a stack).
- The idea is to keep track of already visited nodes and process the current node only once its children are processed.
The next()
method retrieves the subsequent element in the collection, raising the question of how the next element is determined. Specifically, for this problem, the next element corresponds to the postorder successor, which is the tree node visited immediately after the current node in postorder traversal.
A formal definition of the postorder successor is as follows:
- If the node is the left child of its parent:
- When the parent has a right subtree ( T’ ), the successor is the leftmost leaf of ( T’ ).
- If the parent lacks a right subtree, the successor is the parent itself.
- If the node is the right child of its parent, then the successor is the parent.
graph TD; A(1) B(2) C(3) D(4):::orangeFill E(5) F(6):::greenFill G(7):::greenBorder H(8):::blueBorder I(9) J(10):::blueFill K(11):::orangeBorder L(12) A --- B & C B --- D & E C --- F & G D --- H D ~~~ N1:::hidden H --- I & J E ~~~ N2:::hidden E --- K F ~~~ N3:::hidden F --- L classDef hidden display:none classDef blueFill fill:#1E90FF,stroke:#000,stroke-width:1px,color:#fff; classDef blueBorder fill:none,stroke:#1E90FF,stroke-width:2px,color:#000; classDef greenFill fill:#32CD32,stroke:#000,stroke-width:1px,color:#fff; classDef greenBorder fill:none,stroke:#32CD32,stroke-width:2px,color:#000; classDef orangeFill fill:#FFA500,stroke:#000,stroke-width:1px,color:#fff; classDef orangeBorder fill:none,stroke:#FFA500,stroke-width:2px,color:#000;
In the accompanying diagram, each solid-coloured node’s postorder successor is marked with the same solid border colour. For instance, node 8 is the successor of 10, 11 follows 4, and 7 is the successor of 6.
Intuition
In a basic binary tree without parent pointers, we require a stack to maintain references to the parent as well as ancestors up to the root. This enables traversal while keeping hierarchy intact. Additionally, the iterator needs to track the last node that was returned, making it important to have an instance-level variable for this. The state of the stack must persist across multiple calls to the next()
method, ensuring traversal continuity. For practice, readers can implement the remove()
method on their own. Meanwhile, let’s focus on implementing the hasNext()
and next()
methods.
Approach
hasNext() Method
In postorder traversal, the root node is the final node visited. Consequently, the iterator guarantees a next element as long as the last returned node is not null
.
next() Method
To fetch the postorder successor of the previously returned node, several scenarios must be addressed:
- If the previously returned node is
null
: This means thenext()
method is invoked for the first time. Start by pushing the root node onto the stack.- Check if the top node has a left child. If it does, push the left child onto the stack and repeat.
- If no left child is found, check for a right child. If present, push it onto the stack and repeat step 1.
- If there is a previously set node:
- Determine if the node is the left child of its parent (the parent is at the top of the stack). If true, check if the parent has a right child. If the right child exists, follow the previous sequence of steps. If no right child exists, pop the parent from the stack, mark it as visited, and return it.
- If the node is the right child of its parent, simply pop the parent from the stack, mark it as visited, and return it.
The logic is visualised through the accompanying code and animation. The diagram demonstrates how the next()
method processes each invocation, updating the “previous node” appropriately.
Here is a video if you want to prefer pausing to see dry run:
- In some cases, multiple nodes are pushed onto the stack, while in others, nodes are simply popped off and returned directly.
- If the previous node was the right child, the method pops one node and returns it immediately. Similarly, if the parent of the previous node lacks a right child, it pops and returns the node.
- When the previous node was the left child, the method proceeds to find the leftmost leaf of the parent’s right child (e.g., when the previous node is 4, the next call returns 11).
This behaviour aligns perfectly with the formal definition of the postorder successor and ensures correctness during traversal.
Code
|
|
|
|
Complexity
- ⏰ Time complexity:
O(n)
. Each node is visited exactly once. - 🧺 Space complexity:
O(h)
, whereh
is the height of the tree (due to stack space for recursion simulation).