Mastering Binary Tree Traversals: A Comprehensive Guide

Adam DeJans Jr.
Plain Simple Software
8 min readFeb 12, 2024

Binary trees are fundamental data structures in computer science, used extensively in database systems, search algorithms, and more. Traversing a binary tree is a core operation that involves visiting each node exactly once in a specific order. This article delves into the three primary traversal strategies: pre-order, in-order, and post-order. We will explore both recursive and iterative implementations in Python, providing intuitive explanations and detailed logic behind each approach.

Examples of binary tree traversals.

Understanding Tree Traversals

Tree traversal algorithms are classified based on the order in which nodes are visited:

  • Pre-order Traversal: Visit the root node first, then recursively visit the left subtree, followed by the right subtree.
  • In-order Traversal: Recursively visit the left subtree, visit the root node, and finally, visit the right subtree. This order is especially useful in binary search trees to retrieve elements in sorted order.
  • Post-order Traversal: Recursively visit the left and right subtrees before visiting the root node. This method is useful for operations that require processing children before their parents, such as tree deletions.

Pre-order Traversal (Root-Left-Right)

Intuition

Pre-order traversal prioritizes the root node, then proceeds to the left subtree, and finally the right subtree. It’s ideal for scenarios where you need to explore roots before inspecting leaves, such as when exporting a tree structure into a file that can be easily reconstructed.

Recursive Implementation

def preorderTraversal(root):
# Base case: if the current node is None, stop recursion
if root is None:
return

# First, visit the root node
print(root.val)
# Then, recursively visit the left subtree
preorderTraversal(root.left)
# Finally, recursively visit the right subtree
preorderTraversal(root.right)

Iterative Implementation

def preorderTraversalIterative(root):
if root is None: # If the tree is empty, return an empty list.
return []
stack = [root] # Initialize the stack with the root node.
output = []
while stack: # Continue until there are no nodes left to process.
node = stack.pop() # Remove the last element from the stack to process it.
if node:
output.append(node.val) # Visit the node by adding its value to the output list.
# Push right child first so that the left child is processed first, maintaining pre-order.
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
return output

Why?

Pre-order traversal is implemented this way to ensure the root node is processed before its subtrees, which is essential for operations where the root needs to be visited first (e.g., tree cloning).

  • Terminating Condition Explanation: The loop continues as long as the stack is not empty. This condition ensures that the traversal only stops when there are no more nodes left to visit. Initially, the stack contains the root node, and nodes are added to or removed from the stack as the traversal progresses. When the stack is finally empty, it indicates that all nodes have been visited in the pre-order sequence.
  • Iterative Logic: The stack facilitates backtracking to the parent node, ensuring nodes are visited in pre-order. Pushing the right child before the left ensures left nodes are processed first, adhering to the pre-order sequence.
  • Stack Usage: A stack is used to manually simulate the recursive call stack. It helps in backtracking to the parent node once we’re done with the left subtree and need to traverse the right subtree.
  • Order of Operations: Pushing the right child before the left ensures that nodes are processed in the correct pre-order sequence (node-left-right) because the last-in (left child) is processed first.

Applications

Pre-order traversal is useful in scenarios where you need to work with the root before the leaves. Specific applications include:

  • Tree Copying: When cloning a binary tree, pre-order traversal allows you to create and copy nodes in the same structure as the original tree.
  • Expression Trees: For prefix expression evaluation, pre-order traversal provides the correct order of operations.
  • Creating Prefix Notation: When converting a binary tree into an expression using prefix notation, pre-order achieves this naturally.
  • Directory Structure: If you’re representing a filesystem as a tree, pre-order traversal can help list directories before files within each directory, reflecting how content is structured.

In-order Traversal (Left-Root-Right)

Intuition

In-order traversal is particularly significant in binary search trees (BST) because it retrieves elements in their natural order. The process involves visiting the left subtree, then the root node, and finally the right subtree.

Recursive Implementation

def inorderTraversal(root):
# Base case: if the current node is None, stop recursion
if root is None:
return

# First, recursively visit the left subtree
inorderTraversal(root.left)
# Visit the root node
print(root.val)
# Finally, recursively visit the right subtree
inorderTraversal(root.right)

Iterative Implementation

def inorderTraversalIterative(root):
stack = [] # Stack to track the nodes for backtracking.
current = root
output = []
while current or stack: # Continue as long as there is a current node to process or nodes in the stack.
while current: # Dive left until reaching the leftmost node.
stack.append(current)
current = current.left
current = stack.pop() # Start with the leftmost node.
output.append(current.val) # Visit the node by adding its value to the output list.
current = current.right # Move to the right subtree.
return output

Why?

The recursive approach naturally follows the definition of in-order traversal. The iterative approach, using a stack, mimics the call stack of recursive execution, providing a way to traverse without recursion.

  • Terminating Condition Explanation: The loop continues as long as current is not None or the stack is not empty. This condition is crucial because the traversal dives deep into the leftmost node before starting to visit nodes. When current becomes None, and the stack is empty, it means that all nodes have been visited. This condition ensures comprehensive traversal by allowing the algorithm to backtrack to previously visited nodes and proceed to their right subtrees.
  • Iterative Logic: The iterative approach mimics the recursive call stack with a manual stack, ensuring nodes are visited in the correct in-order sequence by “diving” left, then processing nodes, and finally moving right.
  • Stack and Current Pointer: The stack keeps track of nodes for backtracking, and the current pointer helps in reaching the leftmost node of any subtree.
  • Processing Nodes: Nodes are pushed onto the stack as the algorithm dives deep into the left children. Popping from the stack returns to the most recently visited node, ensuring in-order (left-node-right) processing.
  • Moving to Right Subtrees: After visiting a node (and its left subtree), the algorithm moves to its right subtree, mimicking the recursive in-order pattern without actual recursion.

Applications

In-order traversal is particularly beneficial in binary search trees (BSTs) where it has unique advantages:

  • Sorted Output: In-order traversal of a BST yields elements in their natural, ascending order, making it ideal for sorting.
  • Binary Search Tree Validation: To check if a binary tree is a BST, in-order traversal can be used to verify that values are retrieved in non-decreasing order.
  • Finding Kth Smallest/Largest Element: In-order traversal allows you to count nodes as you traverse, facilitating efficient searches for the kth smallest or largest element in a BST.

Post-order Traversal (Left-Right-Root)

Intuition

Post-order traversal is used for operations that require visiting child nodes before their corresponding parent nodes, such as when performing a safe tree deletion or evaluating postfix expressions.

Recursive Implementation

def postorderTraversal(root):
# Base case: if the current node is None, stop recursion
if root is None:
return

# First, recursively visit the left subtree
postorderTraversal(root.left)
# Then, recursively visit the right subtree
postorderTraversal(root.right)
# Finally, visit the root node
print(root.val)

Iterative Implementation

def postorderTraversalIterative(root):
if root is None: # If the tree is empty, there's nothing to traverse.
return []

stack1 = [root] # Initialize stack1 with the root node.
stack2 = [] # stack2 will store nodes in reverse post-order.

# Continue until stack1 is empty.
while stack1:
node = stack1.pop() # Pop the top node from stack1.
stack2.append(node) # Push it onto stack2.

# Push left and right children of the popped node to stack1.
# Left child is pushed before the right child to ensure that
# the right child is processed first, maintaining post-order.
if node.left:
stack1.append(node.left)
if node.right:
stack1.append(node.right)

# Once stack1 is empty, pop all nodes from stack2 and print/collect their values.
# This will give us the nodes in post-order.
output = [node.val for node in reversed(stack2)]
return output

Why?

The key to this method is the use of two stacks, which allows us to reverse the order of traversal twice, effectively resulting in the correct post-order sequence. The first stack (stack1) is used for the initial traversal, while the second stack (stack2) serves to reverse the order of the nodes.

  • Traversing the Tree: Initially, the root node is pushed onto stack1. As we iterate, each node popped from stack1 is pushed onto stack2, ensuring that we process nodes in the reverse of their intended post-order sequence. When pushing children of the currently processed node onto stack1, the left child is pushed before the right child. This ensures that upon reversal, we visit the right child before the left child, adhering to the post-order traversal rule (left-right-root).
  • Termination Condition: The loop continues as long as stack1 is not empty. This condition ensures that every node in the tree is visited exactly once. The process ends when stack1 is emptied, indicating that all nodes have been transferred to stack2 in the reverse post-order sequence.
  • Efficiency of Two Stacks: Using two stacks is efficient because it avoids the costly operation of inserting elements at the beginning of the output list. Instead, the reversal of the traversal order is elegantly handled by the nature of stack operations (LIFO — Last In, First Out). This technique leverages the stacks’ inherent characteristics to achieve the desired traversal order without additional computational overhead.
  • Final Output Generation: The final post-order traversal output is obtained by reversing stack2 and collecting the values of the nodes. This step is crucial as it transforms the reverse post-order sequence in stack2 back into the correct post-order sequence.

Applications

Post-order traversal is preferred in scenarios where children nodes must be processed before their parent node:

  • Tree Deletion: To safely delete or free a tree from memory, post-order traversal ensures that child nodes are dealt with before deleting the parent node, preventing dangling references.
  • Expression Trees: For postfix expression evaluation, post-order traversal yields the operands in the correct order for evaluation.
  • Dependency Resolution: In scenarios where dependencies must be resolved before a task (node) can be executed, such as build systems or package managers, post-order traversal ensures dependencies are handled first.
  • Space Evaluation: When calculating space used by nodes in a filesystem represented as a tree, post-order can accumulate and return the total size from the bottom up.

Conclusion

Understanding and implementing binary tree traversals are foundational skills in computer science, crucial for a wide range of applications including expression tree evaluations, BST operations, and tree modifications. Each traversal method, from in-order for processing nodes in sorted order, pre-order for cloning trees, to post-order for safely deleting nodes, serves specific purposes tailored to enhance algorithm design and data structure manipulation. Both recursive and iterative implementations offer distinct advantages: recursion simplifies code, while iterative approaches optimize for environments with limited stack space or when facing stack overflow risks. Through comprehensive explanations and code examples, this guide not only aims to equip readers with a deep understanding of binary tree traversal methods but also highlights the importance of selecting the appropriate approach based on specific requirements and constraints, thereby enhancing their problem-solving toolkit.

Did you like what you read? Please follow me and share this post to help others along their data science journey!

Let’s connected on LinkedIn as well, I’d love to hear from you! Make sure to follow Plain Simple Software for more software articles too!

Other Suggested Reading:

--

--