Overview
Recently, I’ve been thinking about some ideas and corresponding implementations for non-recursive tree traversal. I’m writing them down here for future reference.
Author: Muniao’s Notes https://www.qtmuniao.com. Please indicate the source when reposting.
Direct Iterative Approach
The basic idea is to keep going left until hitting a wall (encountering NULL), then turn back; move to the right child (the root of the right subtree), and continue going left (in a loop).
The basic code is as follows:
1 | /** |
The corresponding complete traversal path is shown below:
1 | (1) root (5?) |
For any node root, there are three opportunities to visit it:
- Coming from the parent node (1)
- Returning after visiting the left subtree (3)
- Returning after visiting the right subtree (5)
Correspondingly, according to the definition of binary tree traversal:
- If the node is visited at (1), it is preorder traversal: root first (1), then left subtree (2), and finally right subtree (4).
- If the node is visited at (3), it is inorder traversal: left subtree first (2), then root (3), and finally right subtree (4).
- If the node is visited at (5), it is postorder traversal: left subtree first (2), then right subtree (4), and finally root (5).
Since we need to continue visiting the right subtree after finishing the left subtree, we need to save the pointer to root, which is the role of the stack here. As the traversal goes deeper, nodes are continuously pushed onto the stack, and the maximum length is the depth of the tree. It is worth emphasizing that as we return from visiting the left subtree, the stack will continuously pop, because after obtaining the pointer to the right subtree, there is no longer a need to save the pointer to root. Therefore, without special handling, step (5) does not exist.
Therefore, the non-recursive code for binary tree preorder and inorder traversal can be directly given as:
1 | // preorder traversal: |
The code for postorder traversal is slightly more complex. The main modification required is: we cannot pop the stack after the second visit (i.e., (3) in the figure), but need to pop at stage (5). This raises the question of how to identify stage (5). My approach here is to introduce a prev pointer to mark the previous binary tree node in the visit sequence. If root->right is prev, or root->right is NULL, we can determine that we have returned from visiting the right subtree, which corresponds to stage (5).
1 | void postorderTraversal(TreeNode* root) { |
Simulating Recursion with a Stack
We know that a stack can be used to simulate function calls, or rather, function calls are essentially push and pop operations of function stack frames. Since binary tree traversal is simple enough, our simulation becomes relatively easy to implement.
For example, for preorder traversal, since the stack reverses the visiting order, we push the right subtree first.
1 | void preorderTraversal(TreeNode* root) { |
If we push the left subtree first, it becomes an inverse postorder traversal. By adding another stack, we can obtain postorder traversal.
1 | void inversePostorderTraversal(TreeNode* root) { |
Morris Inorder Traversal
For the traversal methods above, whether recursive or non-recursive, their extra space complexity is O(h), i.e., O(lg n), because the maximum stack overhead is the depth of the tree. So, is there a way to traverse a tree without using extra space? The astute reader might think of threaded binary trees. Bingo! This is the Morris inorder traversal.
Without further ado, here is the code:
1 | void inorderTraversal(TreeNode* root) { |
The basic idea is:
1 | 1. Initialize current as root |
Well, this is actually pseudocode; see the source here. The core idea is that when we rush headlong into a dead end, in order to return painlessly, we need to punch a hole in the wall leading back to the original path, and fill the hole after passing through it. However, punching holes takes time; this is a typical trade-off of time for space—each time we have to waste a bit more time finding the rightmost node in the left subtree.
Summary
The essence of using a stack for binary tree traversal is: using the stack to save the access path (i.e., the path from the root node to the current node), so that when we can no longer proceed in the left subtree, we can backtrack and visit the right subtree.
