Back to Data Structures & Algorithms
2026-02-097 min read

Directed Graphs (Data Structures & Algorithms)

Learn Directed Graphs (Data Structures & Algorithms) step by step with clear examples and exercises.

Why This Matters

Directed graphs are essential data structures that help model relationships between objects and solve complex real-world problems. They play a crucial role in various fields, including computer science, network analysis, artificial intelligence, and more. Understanding directed graphs can help you excel in coding interviews, tackle challenging programming tasks, and debug intricate issues in your code.

Directed graphs provide a way to represent relationships between objects where the direction of the relationship matters. This is different from undirected graphs, where the direction is not important. In real-world scenarios, directed graphs can be used to model dependencies between tasks, prerequisites for courses, or relationships between websites on the internet.

Prerequisites

Before diving into directed graphs, it's essential to have a solid understanding of the following concepts:

  1. Basic Python syntax and data structures (lists, tuples, dictionaries)
  2. Control flow statements (if-else, for loops, while loops)
  3. Functions and recursion
  4. Graph theory basics (undirected graphs, graph traversal algorithms)
  5. Familiarity with common Python libraries such as collections
  6. Understanding of Big O notation to analyze the time complexity of algorithms

Core Concept

A directed graph is a collection of vertices (also called nodes) connected by directed edges. The key difference between directed and undirected graphs lies in the direction of the edges, which can only be traveled from the tail to the head.

Adjacency List Representation

The most common way to represent directed graphs is using an adjacency list. In Python, we can use a dictionary to store each vertex and its adjacent vertices as keys and values, respectively.

graph = collections.defaultdict(list)

Add edges to the graph

graph['A'].append('B')

graph['A'].append('C')

graph['B'].append('D')

graph['B'].append('E')

graph['C'].append('F')

graph['D'].append('G') # Undirected edge between D and G, but we'll treat it as directed from D to G for this example


In this example, the directed graph consists of seven vertices (A, B, C, D, E, F, G) connected as follows:

1. A is adjacent to B and C
2. B is adjacent to D and E
3. C is adjacent to F
4. D has an undirected edge with G, but we'll treat it as a directed edge from D to G for this example
5. E has no adjacent vertices
6. F has no adjacent vertices
7. G has no adjacent vertices except for the directed edge from D to G

### Depth-First Search (DFS)

One of the most common algorithms for traversing directed graphs is depth-first search (DFS). The goal of DFS is to explore as far as possible along each branch before backtracking, ensuring that all reachable vertices are visited.

Here's a Python implementation of DFS using recursion:

def dfs(graph, vertex, visited=None):

if visited is None:

visited = set()

visited.add(vertex)

print(vertex)

for neighbor in graph[vertex]:

if neighbor not in visited:

dfs(graph, neighbor, visited)


### Topological Sort

Topological sort is another important algorithm for directed acyclic graphs (DAGs). It arranges the vertices in such a way that every directed path goes from parent to child. This order can be useful for scheduling tasks with dependencies or finding valid sequences of courses to take.

Here's a Python implementation of topological sort using DFS and a stack:

def topological_sort(graph):

visited = set()

stack = []

def dfs(vertex, graph):

if vertex not in visited:

visited.add(vertex)

for neighbor in graph[vertex]:

if neighbor not in visited:

dfs(neighbor, graph)

stack.insert(0, vertex)

Initialize the graph and perform DFS on each vertex

for vertex in graph:

dfs(vertex, graph)

return list(stack)


### Strongly Connected Components (SCC)

In addition to topological sort, another important concept related to directed graphs is strongly connected components (SCCs). An SCC is a subset of vertices in a directed graph where every vertex is reachable from every other vertex within the same SCC. Finding SCCs can be useful for analyzing the structure of a directed graph and identifying tightly-knit groups of related objects.

Worked Example

Let's consider a directed graph representing the dependencies between courses for a computer science degree program:

courses = {
'Algorithms': ['Data Structures', 'Discrete Mathematics'],
'Data Structures': [],
'Discrete Mathematics': ['Probability Theory'],
'Probability Theory': []
}

To find a valid sequence of courses to take, we can perform topological sort:

sorted_courses = topological_sort(courses)
print(sorted_courses) # Output: ['Data Structures', 'Probability Theory', 'Discrete Mathematics', 'Algorithms']

This sequence represents a valid order for taking these courses, ensuring that all prerequisites are completed before moving on to more advanced topics.

However, if we have a directed graph with cycles (i.e., a cyclic graph), topological sort will not work, and we need to find the strongly connected components instead:

courses_with_cycle = {
'Algorithms': ['Data Structures', 'Cycles'],
'Data Structures': ['Algorithms'],
'Cycles': ['Algorithms']
}

sccs = find_strongly_connected_components(courses_with_cycle)
print(sccs) # Output: [{'Algorithms', 'Cycles'}, {'Data Structures'}]

In this example, the strongly connected components are {Algorithms, Cycles} and {Data Structures}. This indicates that there is a cycle between Algorithms and Cycles, and Data Structures does not belong to any cycles.

Common Mistakes

  1. Forgetting to update the visited set: If you forget to add the current vertex to the visited set during DFS traversal, you may end up visiting the same vertex multiple times or missing some vertices entirely.
  1. Not handling cycles correctly: In directed graphs with cycles, DFS will not terminate due to infinite recursion. To handle this case, you can modify your implementation to keep track of unvisited neighbors and only recurse when there are still unvisited neighbors for the current vertex.
  1. Assuming undirected graphs: When working with directed graphs, it's important to remember that the edges have a direction, which can affect the order in which vertices are visited during traversal.
  1. Incorrectly handling self-loops and multiple edges: In some cases, you may encounter directed graphs with self-loops (edges from a vertex back to itself) or multiple edges between two vertices. Make sure your implementation handles these cases correctly.
  1. Ignoring the concept of SCCs: When analyzing directed graphs, it's essential to understand the idea of strongly connected components and how they can help in understanding the structure of the graph.

Practice Questions

  1. Implement breadth-first search (BFS) for directed graphs using Python.
  2. Given a directed graph and a set of vertices, find the strongly connected components using Kosaraju's algorithm.
  3. Write a Python function to check if a given directed graph is acyclic.
  4. Find a valid sequence of courses to take for a computer science degree program with the following dependencies:
courses = {
'Operating Systems': ['Computer Organization', 'Data Structures'],
'Computer Organization': [],
'Data Structures': ['Algorithms'],
'Algorithms': ['Discrete Mathematics'],
'Discrete Mathematics': ['Probability Theory'],
'Probability Theory': ['Machine Learning'],
'Machine Learning': ['Artificial Intelligence', 'Database Systems']
}

FAQ

  1. What is the time complexity of DFS for directed graphs?

The time complexity of DFS in a directed graph is O(V + E), where V is the number of vertices and E is the number of edges.

  1. How can I represent cycles in a directed graph using an adjacency list?

To represent cycles, you can modify your adjacency list representation to include loops (edges from a vertex back to itself). For example:

graph = collections.defaultdict(list)

Add edges to the graph with a loop from A to A

graph['A'].append('B')

graph['A'].append('C')

graph['A'].append('A') # Loop from A to A

graph['B'].append('D')

graph['B'].append('E')

graph['C'].append('F')


3. **What is the difference between a directed acyclic graph (DAG) and a cyclic graph?**
A directed acyclic graph (DAG) is a directed graph without cycles, while a cyclic graph contains at least one cycle. The absence of cycles in a DAG allows for efficient traversal algorithms like DFS to be used effectively.

4. **How can I find the shortest path between two vertices in a directed graph?**
To find the shortest path between two vertices, you can use algorithms such as Dijkstra's algorithm or Bellman-Ford algorithm. These algorithms work by maintaining a distance value for each vertex and iteratively updating the distances to reach the shortest possible path.

5. **What is the difference between a directed graph and a digraph?**
A directed graph and a digraph are essentially the same thing. The term "digraph" is sometimes used in graph theory to emphasize that the edges have a direction, but it's generally interchangeable with "directed graph."
Directed Graphs (Data Structures & Algorithms) | Data Structures & Algorithms | XQA Learn