Tree Traversal
Table of contents
Binary Tree Node
Assume our tree is implemented as such:
template<typename T>
class BinaryTreeNode {
public:
using Node = BinaryTreeNode<T>;
using NodePtr = shared_ptr<BinaryTreeNode<T>>;
T val_;
NodePtr left_;
NodePtr right_;
BinaryTreeNode(T val) : val_(val), left_(nullptr), right_(nullptr) {}
};
Pretty basic setup.
For further simplicity, we’ll assume T = int
.
Depth-First Traversal
There are three actions that should be taken at step:
- Traverse the left subtree
- Traverse the right subtree
- Visit the node
Depending on when we visit the node, we can have three different types of depth-first traversal:
- Pre-order traversal: Visit the node before traversing the subtrees
- In-order traversal: Visit the node in between traversing the subtrees
- Post-order traversal: Visit the node after traversing the subtrees
Recursion or Stack
Depth-first traversal is usually implemented using recursion. You could also implement it iteratively using a stack.
For each traversal, we’ll return the result of the traversal as a vector of node values.
Recursive DFT
Very simple to implement.
The difference between the three types of traversal really just boils down to where you put the push
.
void PreOrder(const NodePtr& node, vector<int>& res) {
if (!node) return;
res.push_back(node->val_); // Pre-order
PreOrder(node->left_, result);
// res.push_back(node->val_); // In-order
PreOrder(node->right_, result);
// res.push_back(node->val_); // Post-order
}
Iterative DFT
Pre and Inorder
Because recursion is basically a stack under the hood, you can implement DFT iteratively using a stack.
You’re gonna need two while loops:
- One loop to keep popping the stack (equivalent to returning from a recursive call)
- Another loop to keep going deeper to the left subtree
void PreOrder(const NodePtr& node, vector<int>& res) {
stack<NodePtr> s;
NodePtr curr = node;
while (curr || !s.empty()) {
while (curr) {
res.push_back(curr->val_); // Pre-order
s.push(curr);
curr = curr->left_;
}
curr = s.top();
s.pop();
// res.push_back(curr->val_); // In-order
curr = curr->right_;
}
}
Postorder
Iterative postorder traversal is a bit more complicated/different.
Why? Because preorder and inorder visits a node (the core action of the recursion) before visiting all left and right subtrees.
On the other hand, postorder needs to wait until right subtree is processed. But if you see our iterative implementation above, if we push to the res
vector after curr = curr->right_
, our original curr
node is lost.
There is a simple implementation using two stacks. The idea is to dump processed nodes from the first stack into the second stack.
Since stack is LIFO, popping from the second stack will give us the correct order.
There is also a way to do it with one stack.
void PostOrder(const NodePtr& node, vector<int>& res) {
stack <NodePtr> s1, s2;
if (!node) return;
s1.push(node);
NodePtr curr;
while (!s1.empty()) {
curr = s1.top();
s1.pop();
s2.push(curr); // Dump processed nodes to s2
if (curr->left_) s1.push(curr->left_);
if (curr->right_) s1.push(curr->right_);
}
while (!s2.empty()) {
res.push_back(s2.top()->val_);
s2.pop();
}
}
Breadth-First Traversal
Level-order traversal uses a queue. After processing yourself, add all children to the queue to be processed.
void BreadthFirstTraversal(const NodePtr& root, vector<int>& res) {
if (!root) return;
queue<NodePtr> q;
q.push(root);
while (!q.empty()) {
NodePtr curr = q.front();
q.pop();
res.push_back(curr->val_);
if (curr->left_) q.push(curr->left_);
if (curr->right_) q.push(curr->right_);
}
}