← Back to DevBytes

Solving Course Schedule II in Python: Step-by-Step Guide

Introduction to Course Schedule II

Course Schedule II is one of the most popular graph problems on LeetCode (Problem 210) and a frequent interview question at top tech companies. The problem asks you to determine a valid ordering of courses given their prerequisites, returning an empty array if no such ordering exists. At its core, this is a topological sorting problem — a fundamental algorithm every developer should master.

In this tutorial, we'll walk through the problem step by step, build intuition about the underlying graph structure, and implement two distinct solutions in Python: one using Kahn's algorithm (BFS-based) and another using depth-first search (DFS). By the end, you'll understand not only how to solve this specific problem but also how to apply these techniques to any dependency-resolution scenario.

Understanding the Problem

Let's start by formally defining the problem. You are given:

Your task is to return any valid ordering of courses that satisfies all prerequisite constraints. If it is impossible to complete all courses (because of a cycle in the dependency graph), return an empty array.

Example Walkthrough

Consider numCourses = 4 and prerequisites = [[1,0],[2,0],[3,1],[3,2]]. This means:

One valid ordering is [0, 1, 2, 3] or [0, 2, 1, 3]. Both are correct because in each, every prerequisite appears before the course that depends on it.

Why This Problem Matters

Topological sorting is not just an academic exercise. It appears in countless real-world systems:

Understanding how to detect cycles and produce valid orderings is essential for any developer working with dependency graphs.

Modeling the Problem as a Graph

The first step is recognizing that courses and prerequisites form a directed graph. Each course is a node, and each prerequisite pair [a, b] creates a directed edge from b to a (because b must come before a).

A valid course ordering is exactly a topological order of this directed graph — a linear arrangement of nodes where for every directed edge u → v, node u appears before node v.

Two critical observations:

Solution 1: Kahn's Algorithm (BFS Approach)

Kahn's algorithm is the most intuitive topological sort method. The idea is simple: repeatedly remove nodes with no incoming edges (in-degree of zero), since they have no unmet prerequisites and can be taken immediately.

How Kahn's Algorithm Works

  1. Compute the in-degree (number of incoming edges) for every node.
  2. Initialize a queue with all nodes that have in-degree zero.
  3. While the queue is not empty, remove a node, append it to the result, and decrement the in-degree of its neighbors. If any neighbor's in-degree becomes zero, add it to the queue.
  4. If the result contains all nodes, return it. Otherwise, a cycle exists, so return an empty array.

Python Implementation

from collections import deque, defaultdict

def findOrder(numCourses, prerequisites):
    # Build adjacency list and in-degree array
    graph = defaultdict(list)
    in_degree = [0] * numCourses
    
    for course, prereq in prerequisites:
        graph[prereq].append(course)
        in_degree[course] += 1
    
    # Start with all courses that have no prerequisites
    queue = deque([i for i in range(numCourses) if in_degree[i] == 0])
    order = []
    
    while queue:
        current = queue.popleft()
        order.append(current)
        
        # Reduce in-degree of neighbors
        for neighbor in graph[current]:
            in_degree[neighbor] -= 1
            if in_degree[neighbor] == 0:
                queue.append(neighbor)
    
    # If we processed all courses, return the order; otherwise cycle exists
    return order if len(order) == numCourses else []

# Example usage
numCourses = 4
prerequisites = [[1, 0], [2, 0], [3, 1], [3, 2]]
print(findOrder(numCourses, prerequisites))  # Output: [0, 1, 2, 3] or [0, 2, 1, 3]

Complexity Analysis

Solution 2: DFS-Based Topological Sort

The DFS approach offers an alternative that some developers find more elegant. The idea is to perform a depth-first traversal and add nodes to the result in reverse post-order — meaning a node is added only after all nodes depending on it have been fully explored.

How DFS Topological Sort Works

  1. Maintain a visited state for each node: unvisited (0), currently in the recursion stack (1), or fully processed (2).
  2. For each unvisited node, start a DFS.
  3. Mark the node as "in progress" (1) when entering it.
  4. If you encounter a node that is currently "in progress," you've found a cycle — return failure.
  5. After exploring all neighbors, mark the node as "processed" (2) and prepend it to the result.

The three-state marking is crucial: it distinguishes between nodes we've never seen and nodes currently on our recursion path, which is what allows cycle detection.

Python Implementation

def findOrder(numCourses, prerequisites):
    # Build adjacency list
    graph = {i: [] for i in range(numCourses)}
    for course, prereq in prerequisites:
        graph[prereq].append(course)
    
    # State: 0 = unvisited, 1 = visiting, 2 = visited
    state = [0] * numCourses
    order = []
    
    def dfs(node):
        if state[node] == 1:
            # Cycle detected
            return False
        if state[node] == 2:
            # Already processed
            return True
        
        # Mark as visiting
        state[node] = 1
        
        for neighbor in graph[node]:
            if not dfs(neighbor):
                return False
        
        # Mark as visited and add to order
        state[node] = 2
        order.append(node)
        return True
    
    # Try DFS from every unvisited node
    for course in range(numCourses):
        if state[course] == 0:
            if not dfs(course):
                return []
    
    # Reverse because we added nodes in post-order
    return order[::-1]

# Example usage
numCourses = 4
prerequisites = [[1, 0], [2, 0], [3, 1], [3, 2]]
print(findOrder(numCourses, prerequisites))  # Output: [0, 2, 1, 3] or similar valid order

Complexity Analysis

Comparing the Two Approaches

Both algorithms solve the problem optimally, but they have different strengths:

For Course Schedule II specifically, Kahn's algorithm tends to be cleaner and easier to explain. However, knowing both gives you flexibility during interviews and in production code.

Best Practices

Choose the Right Data Structures

Use collections.defaultdict(list) for adjacency lists to avoid manual key initialization. For the in-degree array, a simple list is sufficient and more cache-friendly than a dictionary when node labels are contiguous integers.

Handle Edge Cases Explicitly

Always consider these edge cases in your solution:

Validate Input

In production code, validate that prerequisite pairs reference valid course indices:

def findOrder(numCourses, prerequisites):
    # Input validation
    for course, prereq in prerequisites:
        if not (0 <= course < numCourses and 0 <= prereq < numCourses):
            raise ValueError(f"Invalid course index in pair [{course}, {prereq}]")
    
    # ... rest of the algorithm

Avoid Recursion for Large Graphs

Python's default recursion limit is around 1000. For graphs with more than a few thousand nodes, prefer Kahn's iterative BFS approach or increase the recursion limit with sys.setrecursionlimit(). However, increasing the limit is a band-aid; iterative solutions are safer for unbounded input sizes.

Write Testable Code

Separate the graph construction from the algorithm logic so each component can be tested independently:

def build_graph(numCourses, prerequisites):
    graph = defaultdict(list)
    in_degree = [0] * numCourses
    for course, prereq in prerequisites:
        graph[prereq].append(course)
        in_degree[course] += 1
    return graph, in_degree

def topological_sort(graph, in_degree, numCourses):
    queue = deque([i for i in range(numCourses) if in_degree[i] == 0])
    order = []
    while queue:
        node = queue.popleft()
        order.append(node)
        for neighbor in graph[node]:
            in_degree[neighbor] -= 1
            if in_degree[neighbor] == 0:
                queue.append(neighbor)
    return order if len(order) == numCourses else []

def findOrder(numCourses, prerequisites):
    graph, in_degree = build_graph(numCourses, prerequisites)
    return topological_sort(graph, in_degree, numCourses)

Testing Your Solution

Thorough testing is essential. Here's a test suite covering the main scenarios:

def test_findOrder():
    # Test 1: Normal case
    result = findOrder(4, [[1, 0], [2, 0], [3, 1], [3, 2]])
    assert len(result) == 4
    assert is_valid_order(result, [[1, 0], [2, 0], [3, 1], [3, 2]])
    
    # Test 2: Cycle exists
    assert findOrder(2, [[0, 1], [1, 0]]) == []
    
    # Test 3: No prerequisites
    result = findOrder(3, [])
    assert sorted(result) == [0, 1, 2]
    
    # Test 4: Single course
    assert findOrder(1, []) == [0]
    
    # Test 5: Linear chain
    result = findOrder(3, [[1, 0], [2, 1]])
    assert result == [0, 1, 2]
    
    # Test 6: Self-loop (cycle)
    assert findOrder(2, [[0, 0]]) == []
    
    print("All tests passed!")

def is_valid_order(order, prerequisites):
    position = {course: idx for idx, course in enumerate(order)}
    for course, prereq in prerequisites:
        if position[prereq] >= position[course]:
            return False
    return True

test_findOrder()

Common Pitfalls to Avoid

Conclusion

Course Schedule II is a gateway problem that teaches topological sorting — a technique with broad applications in build systems, package managers, and task schedulers. Both Kahn's algorithm and the DFS-based approach solve the problem in optimal O(V + E) time, and choosing between them comes down to personal preference and specific problem constraints. Kahn's algorithm is generally cleaner for this problem due to its iterative nature and straightforward cycle detection, while the DFS approach shines when you need deeper traversal insights. By mastering both methods, understanding the graph modeling step, and following the best practices outlined above, you'll be well-equipped to tackle not only Course Schedule II but any dependency-ordering challenge you encounter in your development career.

— Ad —

Google AdSense will appear here after approval

← Back to all articles