5 Best Ways to Implement Depth First Search Postorder Traversal in Python

Rate this post

πŸ’‘ Problem Formulation: Depth First Search (DFS) traversal is a fundamental algorithm used in tree and graph data structures. Specifically, the postorder traversal requires the nodes to be visited in the order of left child, right child, and then the parent. This article explores how to implement DFS postorder traversal in Python, with an example input of a tree represented as a graph structure and the expected output being a list containing the nodes in the required postorder sequence.

Method 1: Recursive Approach

Recursive techniques in programming allow functions to call themselves with modified parameters, leading to a clean and intuitive traversal implementation. A recursive DFS postorder function typically requires a helper function that accepts a node and the graph structure, and manages the state of visited nodes.

Here’s an example:

def postorder_traversal(graph, start):
    visited = set()
    result = []
    
    def dfs(node):
        if node not in visited:
            visited.add(node)
            for neighbour in graph[node]:
                dfs(neighbour)
            result.append(node)
    
    dfs(start)
    return result

graph = {
    'A' : ['B','C'],
    'B' : ['D', 'E'],
    'C' : ['F'],
    'D' : [],
    'E' : [],
    'F' : []
}

print(postorder_traversal(graph, 'A'))

Output: [‘D’, ‘E’, ‘B’, ‘F’, ‘C’, ‘A’]

This snippet defines a postorder_traversal function that accepts a graph and a starting node. Inside, a nested function dfs is defined, which recursively traverses the graph in postorder fashion. The list result collects the traversal order, which is returned at the end of the function.

Method 2: Iterative Approach with Stack

Iterative solutions often use a stack to manage traversal without recursion. For DFS postorder, we modify the usual stack mechanism slightly. Nodes get pushed to the stack in the opposite order, and a secondary stack or a list can be utilized to reverse the order of processed nodes into postorder.

Here’s an example:

def postorder_traversal_iterative(graph, start):
    visited = set()
    stack = [start]
    result = []
    
    while stack:
        node = stack.pop()
        if node not in visited:
            visited.add(node)
            result.insert(0, node)
            stack.extend(graph[node])

    return result

graph = {
    'A' : ['B','C'],
    'B' : ['D', 'E'],
    'C' : ['F'],
    'D' : [],
    'E' : [],
    'F' : []
}

print(postorder_traversal_iterative(graph, 'A'))

Output: [‘D’, ‘E’, ‘B’, ‘F’, ‘C’, ‘A’]

This code uses a stack to manage the nodes that need to be visited and a list to store the traversal order. Nodes are added to the start of the result list to ensure the final postorder. The method eschews recursion in favor of iterative logic, ideal for larger datasets where stack overflow might be a concern.

Method 3: Using Two Stacks

Although similar to the iterative stack approach, using two stacks can simplify the algorithm. The first stack is used for traversal, while the second is used to reverse the order of the nodes. Upon completion, the second stack is popped to produce the postorder traversal.

Here’s an example:

def postorder_traversal_two_stacks(graph, start):
    visited = set()
    stack = [start]
    result_stack = []
    
    while stack:
        node = stack.pop()
        if node not in visited:
            visited.add(node)
            result_stack.append(node)
            for neighbour in graph[node]:
                stack.append(neighbour)

    return result_stack[::-1]

graph = {
    'A' : ['B','C'],
    'B' : ['D', 'E'],
    'C' : ['F'],
    'D' : [],
    'E' : [],
    'F' : []
}

print(postorder_traversal_two_stacks(graph, 'A'))

Output: [‘D’, ‘E’, ‘B’, ‘F’, ‘C’, ‘A’]

The adjustment to use two stacks simplifies the process of reversing the order of node visits. The result_stack is reversed at the end using slicing, effectively giving us the postorder sequence. This approach avoids list insertions, which can be expensive operations.

Method 4: Color Marking Nodes

Color marking is a method for tracking traversal stages of nodes during DFS. A node can have two states: “white” being unvisited, and “gray” being processed. This method avoids repeated checks of visited state by directly controlling the traversal flow through these states.

Here’s an example:

def postorder_traversal_color(graph, start):
    stack = [(start, 'white')]
    result = []
    
    while stack:
        node, color = stack.pop()
        if color == 'white':
            stack.append((node, 'gray'))
            for neighbour in reversed(graph[node]):
                stack.append((neighbour, 'white'))
        else:
            result.append(node)

    return result

graph = {
    'A' : ['B','C'],
    'B' : ['D', 'E'],
    'C' : ['F'],
    'D' : [],
    'E' : [],
    'F' : []
}

print(postorder_traversal_color(graph, 'A'))

Output: [‘D’, ‘E’, ‘B’, ‘F’, ‘C’, ‘A’]

Maintaining state in the stack itself as a tuple of node and color, the above code snippet uses color marking to avoid revisiting nodes. When a white node is encountered, it is pushed back as gray, and its neighbors are inserted in white, ensuring that nodes aren’t processed until all descendants are handled.

Bonus One-Liner Method 5: Using NetworkX

NetworkX is a Python library for studying graphs and networks, which includes many common algorithms. It offers a straightforward way to perform DFS postorder traversal with a single function call.

Here’s an example:

import networkx as nx

G = nx.DiGraph([('A', 'B'), ('A', 'C'), ('B', 'D'), ('B', 'E'), ('C', 'F')])
result = list(nx.dfs_postorder_nodes(G, source='A'))
print(result)

Output: [‘D’, ‘E’, ‘B’, ‘F’, ‘C’, ‘A’]

The one-liner utilizes NetworkX’s built-in DFS postorder nodes function to produce the traversal. It is arguably the simplest implementation for those willing to include third-party libraries, offering powerful graph manipulation and analysis algorithms.

Summary/Discussion

  • Method 1: Recursive Approach. This is the most classic and straightforward method for depth first searches. However, it is not suitable for very large trees due to possible stack overflow errors.
  • Method 2: Iterative Approach with Stack. Converts recursion into an iterative form using a stack, which makes it more suitable for larger datasets but can be less intuitive than the recursive approach.
  • Method 3: Using Two Stacks. Separates the concerns of traversal and ordering into two different stacks, resulting in clearer logic and potentially more efficient reversal operations.
  • Method 4: Color Marking Nodes. This technique simplifies the handling of traversal states and reduces the complexity by avoiding excessive checks on whether a node has been visited.
  • Bonus Method 5: Using NetworkX. Serves as an extremely compact solution, excellent for quick implementations and utilizing the added efficiency of devoted graph libraries. However, it requires an external dependency on the NetworkX library.