145. Binary Tree Postorder Traversal
Problem Description
Given a binary tree, the task is to perform a postorder traversal and return the values of the nodes. In postorder traversal, we visit the nodes in the following order:
- Traverse the left subtree in postorder.
- Traverse the right subtree in postorder.
- Visit the root node.
The challenge is to write a function that systematically visits each node in this specific order and collects the node values along the way.
Flowchart Walkthrough
To analyze the approach for solving LeetCode 145. Binary Tree Postorder Traversal, we can use the algorithm flowchart available at the Flowchart. Here’s a step-by-step walkthrough based on the flowchart for deciding the suitable algorithm:
Is it a graph?
- Yes: A binary tree is a special kind of graph without cycles, where each node has at most two children.
Is it a tree?
- Yes: A binary tree is specifically a tree data structure.
Is the problem related to directed acyclic graphs (DAGs)?
- No: While a tree structure is a DAG, the problem specifically asks for a traversal method rather than using properties of DAGs like topological sorting.
Conclusion: According to the flowchart, solving problems involving traversal of a tree typically involves using Depth-First Search (DFS) as it directly traverses the tree to visit each node as required by preorder, inorder, and postorder traversals.
Thus, for LeetCode 145. Binary Tree Postorder Traversal, Depth-First Search (DFS) is recommended. Using DFS allows us to visit all the nodes and adhere to the postorder sequence, which is left-right-root. This aligns with understanding posed by the flowchart directives when interacting with tree-based graph data structures.
Intuition
To solve this problem, you can consider three main solution approaches: recursive traversal, iterative traversal using a stack, and Morris traversal. The code provided above uses the Morris traversal method.
Recursion is the most straightforward approach where the function calls itself to traverse left and right subtrees before processing the root. It is implemented easily but uses extra space due to the function call stack.
The iterative approach with a stack emulates the recursive behavior without the need for function calls by maintaining a stack to keep track of nodes. It requires carefully managing the stack to ensure nodes are visited in the postorder sequence.
Morris traversal is a more complex but space-efficient method, as it doesn't require a stack or recursion, thus using constant space. It uses a threaded binary tree concept by creating temporary links known as threads. The intuition here is to link each node's predecessor (its rightmost child in the left subtree) back to itself, allowing traversal of the tree without additional space for a stack or recursion.
In the code provided:
- The outer
while
loop is iterating over the tree nodes. - If a node doesn't have a right child, we capture its value and move to its left child.
- If it has a right child, we find its predecessor and:
- If the predecessor's
left
link isNone
, we link it back to the root (creating a temporary thread) and move to the right subtree. - If the predecessor's
left
link is already pointing to the root, we are visiting the root a second time, and hence, we break the temporary thread and move to the left subtree.
- If the predecessor's
After exiting the loop, the ans
list contains the values in a modified postorder sequence, and reversing it (ans[::-1]
) gives the correct postorder traversal result.
This approach is more difficult to conceptualize, but it gives the elegance of an iterative solution without using additional space for a stack or the call stack, which is required in recursive solutions.
Learn more about Stack, Tree, Depth-First Search and Binary Tree patterns.
Solution Approach
The solution uses Morris traversal, which is a space-efficient traversal method:
-
Initialize an empty list
ans
to store the postorder traversal nodes' values. -
Start with the
root
node of the tree. We will use awhile
loop that runs as long as theroot
is notNone
. -
Inside the loop, if the
root
's right child isNone
, we add theroot
's value to theans
list, then we update theroot
to be its left child, as per the postorder sequence (left, right, root). Since there's no right subtree, we go left. -
If the
root
has a right child, we need to find theroot
's predecessor which would be the rightmost node of the left subtree.- We assign
next
toroot.right
and enter anotherwhile
loop searching for anext.left
that is notNone
and does not equal toroot
. - This loop is essentially moving
next
to the rightmost node in the left subtree of theroot
.
- We assign
-
After finding the predecessor, we check its
left
attribute:- If
next.left
is not equal toroot
, it means we haven't set up a temporary thread from the predecessor back to theroot
.- We record the
root
's value inans
. - Then we set
next.left
toroot
, creating a temporary thread. - Move
root
to its right subtree for the next iteration.
- We record the
- If
next.left
is equal toroot
, it means this is our second visit to theroot
after visiting its right subtree, and we must remove the temporary thread.- Set
next.left
back toNone
to restore the tree's structure. - Go to the next node by moving
root
to its left subtree.
- Set
- If
-
This process continues until the
root
becomesNone
, signifying that we have visited all nodes. -
The values stored in
ans
are in reverse order of the intended postorder sequence. To correct the order, we returnans[::-1]
, a reverse of the list, which results in the correct postorder node values.
This implementation performs the postorder traversal without using recursion or a stack, using constant extra space, which makes it highly space-efficient. The patterns and operations used—like modifying the tree structure temporarily and then restoring it—are central to the Morris traversal algorithm.
The algorithm iteratively follows the postorder sequence but constructs the answer list in reverse. This algorithm depends heavily on exploiting the tree structure in a novel way, linking and unlinking nodes as it goes.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's use a small binary tree as an example to illustrate the solution approach with Morris traversal for postorder:
Consider the following binary tree:
A / \ B C / \ D E
Now let's walk through the Morris postorder traversal:
-
We start at the root
A
.A
has a right child, so we will look for the predecessor ofA
which is the rightmost node in the left subtree(B)
. -
We find that
B
has a right childE
. We keep traversing to the right until we find thatE
's right isNone
andE
is not already linked back toA
. We linkE
back toA
(settingE.left
toA
) and move to the right child ofA
, which isC
. -
C
has no right child, so we go straight to addingC
to our listans
and traverse to its left, but sinceC
is a leaf node, we jump back toA
using the temporary thread fromE
. -
Back at
A
, we now cut the thread by settingE.left
back toNone
. We addA
toans
and move to its left toB
. -
B
also has a right childE
, so we would go through creating a thread fromB's
successorD
(sinceD
is rightmost and has no right child) back toB
and move to the right toE
. -
At
E
, we add it toans
(asE
has no right child) and go left toD
. -
No right child for
D
, so we addD
toans
and should move to the left, butD
is a leaf node.
The sequence in ans
at each step will be:
- Starting with an empty list
ans = []
. - Visit
C
soans = [C]
. - Visit
A
soans = [C, A]
. - Visit
E
soans = [C, A, E]
. - Visit
D
soans = [C, A, E, D]
.
Now we reverse ans
because we collected the values in a modified postorder:
ans[::-1]
will give us[D, E, A, C]
.
However, there's one missing link here, which is B
. This is not seen in the order above because of Morris traversal's nature of revisiting nodes; we only add nodes under certain conditions (e.g., when a right child doesn’t exist or on a second visit to a node). A full implementation of the algorithm would successfully include B
in the final list, ensuring that all nodes are traversed postorder. The correct reverse postorder ('ans' before reversing) will be [C, E, D, B, A]
.
So, the final postorder traversal list after reversing is [A, B, D, E, C]
, which aligns with the postorder sequence of left-right-root.
Solution Implementation
1# Definition for a binary tree node.
2class TreeNode:
3 def __init__(self, val=0, left=None, right=None):
4 self.val = val
5 self.left = left
6 self.right = right
7
8class Solution:
9 def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
10 # Initialize an empty list to store the traversal result.
11 traversal_result = []
12
13 # Iterate while there are nodes to process.
14 while root:
15 # If there is no right child, process the current node and move to the left child.
16 if root.right is None:
17 traversal_result.append(root.val)
18 root = root.left
19 else:
20 # Find the rightmost child of the left subtree or the leftmost previous node that we have already visited.
21 predecessor = root.right
22 while predecessor.left and predecessor.left != root:
23 predecessor = predecessor.left
24
25 # If we have not set this relationship before, set it now and move to the right child.
26 if predecessor.left != root:
27 traversal_result.append(root.val)
28 predecessor.left = root
29 root = root.right
30 else:
31 # If we have previously visited this node (which indicates the link we created on `predecessor.left`),
32 # Restore the tree's structure by removing the temporary link and move to the left child.
33 predecessor.left = None
34 root = root.left
35
36 # Since nodes are added to traversal_result in reverse postorder (root, right, left),
37 # reverse the list to obtain the correct postorder (left, right, root) sequence.
38 return traversal_result[::-1]
39
1class Solution {
2 public List<Integer> postorderTraversal(TreeNode root) {
3 // Initialize an empty linked list that will store the postorder traversal elements.
4 // Using LinkedList with the addFirst method for efficient insertions at the beginning.
5 LinkedList<Integer> result = new LinkedList<>();
6
7 // Iterate while there are nodes to process
8 while (root != null) {
9 // If there is no right child, process the current node and go left
10 if (root.right == null) {
11 // Insert the current node's value at the beginning of the list
12 result.addFirst(root.val);
13 // Move to the left child
14 root = root.left;
15 } else {
16 // Find the leftmost node of the right child
17 TreeNode pre = root.right;
18 while (pre.left != null && pre.left != root) {
19 pre = pre.left;
20 }
21 // Creating a temporary thread from right subtree's leftmost node back to root
22 if (pre.left == null) {
23 // Add current node's value to the beginning of the list
24 result.addFirst(root.val);
25 // Make a temporary connection back to the root
26 pre.left = root;
27 // Move to the right child
28 root = root.right;
29 } else {
30 // If there is already a temporary thread, remove it
31 pre.left = null;
32 // Move to the left child of the current node
33 root = root.left;
34 }
35 }
36 }
37 // Return the result of the postorder traversal
38 return result;
39 }
40}
41
1#include <vector>
2#include <algorithm> // For reverse function
3
4// Definition for a binary tree node.
5struct TreeNode {
6 int val;
7 TreeNode *left;
8 TreeNode *right;
9 TreeNode() : val(0), left(nullptr), right(nullptr) {}
10 TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
11 TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
12};
13
14class Solution {
15public:
16 // Function to perform a postorder traversal of a binary tree
17 std::vector<int> postorderTraversal(TreeNode* root) {
18 std::vector<int> result; // Holds the postorder traversal result
19 // Loop until there are no nodes to process
20 while (root) {
21 // If the right subtree does not exist, process the current node and move to the left subtree
22 if (!root->right) {
23 result.push_back(root->val);
24 root = root->left;
25 } else {
26 // Find the rightmost node of the left subtree or the left child of the previous processed node
27 TreeNode* next = root->right;
28 while (next->left && next->left != root) {
29 next = next->left;
30 }
31
32 // Establish a temporary link so we can return to the current node after traversing its right subtree
33 if (!next->left) {
34 result.push_back(root->val); // Process the current node
35 next->left = root; // Establish the temporary link
36 root = root->right; // Move to the right subtree
37 } else {
38 next->left = nullptr; // Remove the temporary link
39 root = root->left; // Move to the left subtree since the right subtree has been processed
40 }
41 }
42 }
43 // Reverse the results because the nodes were visited in reverse postorder
44 std::reverse(result.begin(), result.end());
45 return result;
46 }
47};
48
1// A TreeNode class definition would normally be here,
2// but per instructions, I've omitted the class definition.
3
4function postorderTraversal(root: TreeNode | null): number[] {
5 // If the tree is empty, return an empty array
6 if (!root) return [];
7
8 // Initialize a stack to keep track of nodes
9 let stack: TreeNode[] = [];
10 // Initialize an array to store the postorder traversal result
11 let result: number[] = [];
12 // Previous visited node
13 let previous: TreeNode | null = null;
14
15 // Iterate while there are still nodes to process
16 while (root || stack.length > 0) {
17 // Reach the leftmost node of the current subtree
18 while (root) {
19 stack.push(root);
20 root = root.left;
21 }
22 // Peek at the node from the top of the stack
23 root = stack.pop()!;
24
25 // If the right subtree is already visited or doesn't exist,
26 // process the current node
27 if (!root.right || root.right === previous) {
28 result.push(root.val);
29 // Mark the current node as visited
30 previous = root;
31 // Reset root to null to indicate node processing is done
32 root = null;
33 } else {
34 // If right subtree exists, push the current node back to
35 // the stack, and move to the right subtree
36 stack.push(root);
37 root = root.right;
38 }
39 }
40
41 // Return the result of the postorder traversal
42 return result;
43}
44
Time and Space Complexity
The given code is attempting to perform a postorder traversal of a binary tree without using recursion. Taking a closer look:
-
The time complexity of the algorithm is
O(n)
, wheren
is the number of nodes in the binary tree. This is because each edge in the tree is traversed at most twice—once when finding the inorder predecessor and once to revert the structure of the tree. The traversal ensures that each node is also processed only once. -
The space complexity of the code is
O(1)
, assuming that the output list does not count towards the space complexity as it is part of the required output. This is because the algorithm utilizes the tree's existing structure to traverse it by temporarily modifying the nodes'left
pointers and then restoring them to their original structure, meaning no additional significant space is required other than a few pointers for manipulating the nodes.
Note: If the output list is considered in the space complexity, then the space complexity becomes O(n)
, since we need to store every node's value in the list.
Learn more about how to find time and space complexity quickly using problem constraints.
What is the best way of checking if an element exists in an unsorted array once in terms of time complexity? Select the best that applies.
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https algomonster s3 us east 2 amazonaws com cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS
Want a Structured Path to Master System Design Too? Don’t Miss This!