← Back to DevBytes

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

Introduction to Course Schedule II

Course Schedule II is one of the most popular graph-based algorithmic problems you'll encounter in coding interviews and competitive programming. It is a classic extension of the original Course Schedule problem (LeetCode 207) and appears as problem 210 on LeetCode. The problem asks you to not only determine whether a set of courses can be completed, but also to return a valid ordering in which to take them.

At its core, this problem tests your understanding of directed graphs, topological sorting, and cycle detection. Mastering it will sharpen your ability to model real-world dependency relationships and solve scheduling problems efficiently.

What Is Course Schedule II?

Given a total of numCourses labeled from 0 to numCourses - 1, and an array prerequisites where each element is a pair [a, b] indicating that you must take course b before course a, your task is to return a valid ordering of courses. If no valid ordering exists (because of a cycle in the prerequisites), you should return an empty array.

For example, given numCourses = 4 and prerequisites = [[1,0],[2,0],[3,1],[3,2]], one valid ordering is [0,1,2,3] or [0,2,1,3]. Both are correct because they respect every prerequisite constraint.

Modeling the Problem as a Graph

Each course is a node, and each prerequisite pair [a, b] is a directed edge from b to a. This means b must come before a in the ordering. The goal is to find a topological ordering of this directed graph — a linear sequence of nodes where every directed edge points from an earlier node to a later node.

A topological ordering is only possible if the graph is a Directed Acyclic Graph (DAG). If the graph contains a cycle, no valid ordering exists, and we return an empty array.

Why It Matters

Course Schedule II is more than just an interview question. It represents a broad category of real-world problems involving dependencies and ordering. Understanding how to solve it equips you to handle:

From an interview perspective, this problem is a favorite because it combines multiple concepts — graph construction, traversal, cycle detection, and ordering — into a single, elegant challenge. Solving it cleanly demonstrates strong algorithmic thinking.

Approach 1: Topological Sort Using Kahn's Algorithm (BFS)

The most intuitive way to solve Course Schedule II is Kahn's Algorithm, which uses Breadth-First Search (BFS) and an in-degree array. The in-degree of a node is the number of edges pointing to it — in other words, the number of prerequisites that course still has.

How Kahn's Algorithm Works

The algorithm proceeds in the following steps:

JavaScript Implementation Using BFS

function findOrder(numCourses, prerequisites) {
  // Build adjacency list and in-degree array
  const adjacencyList = new Array(numCourses).fill(0).map(() => []);
  const inDegree = new Array(numCourses).fill(0);

  for (const [course, prereq] of prerequisites) {
    adjacencyList[prereq].push(course);
    inDegree[course]++;
  }

  // Initialize queue with courses that have no prerequisites
  const queue = [];
  for (let i = 0; i < numCourses; i++) {
    if (inDegree[i] === 0) {
      queue.push(i);
    }
  }

  const order = [];
  let count = 0;

  while (queue.length > 0) {
    const current = queue.shift();
    order.push(current);
    count++;

    for (const neighbor of adjacencyList[current]) {
      inDegree[neighbor]--;
      if (inDegree[neighbor] === 0) {
        queue.push(neighbor);
      }
    }
  }

  // If we processed all courses, return the order; otherwise return []
  return count === numCourses ? order : [];
}

// Example usage
const numCourses = 4;
const prerequisites = [[1, 0], [2, 0], [3, 1], [3, 2]];
console.log(findOrder(numCourses, prerequisites)); // [0, 1, 2, 3] or [0, 2, 1, 3]

This implementation runs in O(V + E) time, where V is the number of courses and E is the number of prerequisite pairs. The space complexity is also O(V + E) due to the adjacency list and in-degree array.

Optimizing the Queue With a Deque

Using queue.shift() in JavaScript is O(n) because it reindexes the array. For larger inputs, you can simulate a more efficient queue using an index pointer:

function findOrderOptimized(numCourses, prerequisites) {
  const adjacencyList = new Array(numCourses).fill(0).map(() => []);
  const inDegree = new Array(numCourses).fill(0);

  for (const [course, prereq] of prerequisites) {
    adjacencyList[prereq].push(course);
    inDegree[course]++;
  }

  const queue = [];
  for (let i = 0; i < numCourses; i++) {
    if (inDegree[i] === 0) queue.push(i);
  }

  const order = [];
  let head = 0;

  while (head < queue.length) {
    const current = queue[head++];
    order.push(current);

    for (const neighbor of adjacencyList[current]) {
      inDegree[neighbor]--;
      if (inDegree[neighbor] === 0) {
        queue.push(neighbor);
      }
    }
  }

  return order.length === numCourses ? order : [];
}

By using a head pointer instead of shift(), we avoid the costly reindexing and keep each dequeue operation at O(1).

Approach 2: Topological Sort Using DFS

An alternative approach uses Depth-First Search (DFS) to perform topological sorting. The idea is to recursively visit each node, explore all its neighbors first, and then add the node to the front of the result. Cycle detection is handled using a three-state marking system: unvisited, visiting, and visited.

How DFS Topological Sort Works

JavaScript Implementation Using DFS

function findOrderDFS(numCourses, prerequisites) {
  const adjacencyList = new Array(numCourses).fill(0).map(() => []);
  for (const [course, prereq] of prerequisites) {
    adjacencyList[prereq].push(course);
  }

  // 0 = unvisited, 1 = visiting, 2 = visited
  const state = new Array(numCourses).fill(0);
  const order = [];
  let hasCycle = false;

  function dfs(node) {
    if (hasCycle) return;
    if (state[node] === 1) {
      hasCycle = true;
      return;
    }
    if (state[node] === 2) return;

    state[node] = 1;
    for (const neighbor of adjacencyList[node]) {
      dfs(neighbor);
    }
    state[node] = 2;
    order.push(node);
  }

  for (let i = 0; i < numCourses; i++) {
    if (state[i] === 0) {
      dfs(i);
    }
  }

  if (hasCycle) return [];
  // Reverse because nodes are added after their dependents
  return order.reverse();
}

// Example usage
console.log(findOrderDFS(4, [[1, 0], [2, 0], [3, 1], [3, 2]])); // [0, 2, 1, 3]

Because nodes are added to the result only after all their dependents have been processed, the result is built in reverse topological order. We reverse it at the end to get the correct sequence.

The DFS approach also runs in O(V + E) time and uses O(V + E) space. However, it relies on the call stack for recursion, which may cause stack overflow for very deep graphs. In such cases, the BFS approach is safer.

Comparing the Two Approaches

Both BFS and DFS produce valid topological orderings, but they have different characteristics that may influence your choice:

For interview settings, Kahn's Algorithm is often preferred because it is iterative, easy to explain, and the cycle detection logic is straightforward — if the result doesn't contain all nodes, there's a cycle.

Best Practices

Always Validate Input

Before building your graph, consider edge cases. If prerequisites is empty, any ordering of courses is valid. If numCourses is 0, return an empty array. Handling these upfront prevents unnecessary computation.

function findOrderSafe(numCourses, prerequisites) {
  if (numCourses === 0) return [];
  if (prerequisites.length === 0) {
    return Array.from({ length: numCourses }, (_, i) => i);
  }
  // ... continue with main algorithm
}

Choose the Right Data Structures

Use arrays for the adjacency list and in-degree array when the number of courses is known upfront. This is more efficient than using Maps or Objects for integer-indexed nodes. For sparse graphs, you might consider Maps, but for this problem, arrays are ideal.

Avoid Expensive Array Operations

As shown earlier, avoid Array.prototype.shift() for queue operations. Use an index pointer or a dedicated queue implementation to keep operations at O(1).

Test With Multiple Scenarios

Always test your solution against several cases to ensure correctness:

Consider Readability Over Micro-Optimizations

In an interview, clarity matters more than squeezing out every last millisecond. Write code that is easy to follow, with meaningful variable names and comments explaining non-obvious steps. You can always mention potential optimizations verbally.

Common Pitfalls to Avoid

Conclusion

Solving Course Schedule II in JavaScript is a rewarding exercise that deepens your understanding of graphs, topological sorting, and cycle detection. Whether you choose Kahn's BFS-based algorithm or the DFS approach, the key is to model the problem correctly as a directed graph and handle dependencies with care. By following the implementations and best practices outlined in this guide, you'll be well-equipped to tackle not only this problem but also the many real-world dependency-ordering challenges that share its structure. Practice with varied inputs, internalize the trade-offs between BFS and DFS, and you'll approach similar problems with confidence in any interview or production scenario.

— Ad —

Google AdSense will appear here after approval

← Back to all articles