Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Graph Algorithms

Builds on Level-Order BFS, which you haven’t finished yet.

P1MediumBuilt on Level-Order BFS~6 min speedrun

Topological Sort (Kahn's Algorithm)

Built on
Level-Order BFSAn in-degree count per node decides when a node may enter queue.
The problem

Course Schedule II (LC 210)(opens leetcode.com)

You have numCourses courses to complete, numbered 0 through numCourses - 1, and a list prerequisites where each entry [ai, bi] means course bi must be completed before course ai.

  • So an entry [0, 1] means course 1 has to be finished before course 0.

Produce one valid order in which all courses can be taken; any correct order is acceptable. If no order can complete every course, return an empty list instead.

Example 1
Input:numCourses = 2, prerequisites = [[1,0]]
Output:[0,1]
Course 1 depends on course 0, so 0 must come first, giving the order [0, 1].

Topological Sort

in-degree 0 vertices ➔ drain dependencies ➔ linear topological order
Spot it when: Tasks have prerequisites and you need an order, want to know if everything can finish, or need the number of rounds to finish.
Not this one if…
  • You must check where every path from one start ends, as in All Paths from Source Lead to Destination (LC 1059), not put all the nodes in an order. See Cycle Detection
Topological Sort
Python
1from collections import deque, defaultdict
2from typing import List
3 
4def topological_sort(num_courses: int, prerequisites: List[List[int]]) -> List[int]:
5 """Course Schedule II with Kahn's algorithm: an order to take every course, or [] if impossible.
6 Time: O(V + E) | Space: O(V + E)
7 """
8 adj = defaultdict(list)
9 in_degree = [0] * num_courses
10 
11 for dest, src in prerequisites: # [a, b] means b before a: edge b -> a
12 adj[src].append(dest)
13 in_degree[dest] += 1
14 
15 queue = deque([i for i in range(num_courses) if in_degree[i] == 0]) # No prerequisites left
16 order = []
17 
18 while queue:
19 node = queue.popleft()
20 order.append(node)
21 for neighbor in adj[node]:
22 in_degree[neighbor] -= 1 # One prerequisite of neighbor is done
23 if in_degree[neighbor] == 0:
24 queue.append(neighbor)
25 
26 return order if len(order) == num_courses else [] # Nodes left over: a cycle
Invariant: Every node in order or in queue comes after all of its prerequisites, and in_degree[v] counts the prerequisites of v not yet in order.

A course can be taken once all its prerequisites are done, so count each node's in_degree, start queue with every node whose count is 0, and each time you pop node, lower its neighbours' counts and add any that reach 0. A node enters queue only after every edge into it has been removed, so it lands in order after all its prerequisites. The trap is the cycle check: nodes on a cycle never reach 0, so return order only when len(order) == num_courses, never a partial order.

See it run

Topological Sort (Kahn's algorithm), step by step

queue [0] · order [] · in_degree [0, 1, 1, 2]

queue [0] · order [] · in_degree [0, 1, 1, 2]

queue [] · order [0] · in_degree [0, 1, 1, 2]

queue [1] · order [0] · in_degree [0, 0, 1, 2]

queue [1, 2] · order [0] · in_degree [0, 0, 0, 2]

queue [2] · order [0, 1] · in_degree [0, 0, 0, 2]

queue [2] · order [0, 1] · in_degree [0, 0, 0, 1]

queue [] · order [0, 1, 2] · in_degree [0, 0, 0, 1]

queue [3] · order [0, 1, 2] · in_degree [0, 0, 0, 0]

queue [] · order [0, 1, 2, 3] · in_degree [0, 0, 0, 0]

queue [] · order [0, 1, 2, 3] · in_degree [0, 0, 0, 0]

queue [] · order [0, 1, 2, 3] · in_degree [0, 0, 0, 0] · answer [0, 1, 2, 3]

Each column is a course number. The rows are in_degree, queue (front on the left) and order.

  • node the course just popped from queue and added to order
  • neighbor a course that depends on node; its count drops by 1
  • pushed a course with count 0, added to queue

Each pair [a, b] adds an edge b -> a and raises in_degree[a]. The counts are [0, 1, 1, 2].

queue starts with every course whose count is 0: [0]. order is empty.

Pop node = 0 and append it to order. order is now [0].

Edge 0 -> 1: in_degree[1] drops to 0, so every prerequisite of 1 is in order. 1 joins queue.

Edge 0 -> 2: in_degree[2] drops to 0, so every prerequisite of 2 is in order. 2 joins queue.

Pop node = 1 and append it to order. order is now [0, 1].

Edge 1 -> 3: in_degree[3] drops to 1. 3 still waits for another prerequisite, so it stays out of queue.

Pop node = 2 and append it to order. order is now [0, 1, 2].

Edge 2 -> 3: in_degree[3] drops to 0, so every prerequisite of 3 is in order. 3 joins queue.

Pop node = 3 and append it to order. order is now [0, 1, 2, 3].

Check: len(order) = 4 equals num_courses = 4, so no course was left behind.

Done: the order is [0, 1, 2, 3].

in_degree
00
11
21
32
queue
0
order
1 / 12
Gotchas1 of 3

Gotcha 1 of 3

  1. 1
    The trap

    Gotcha 1: Returning order without checking len(order) == num_courses

    Mistake
    return order
    Fix
    return order if len(order) == num_courses else []

    What happens: A course on a cycle waits for another course on the same cycle. Its in_degree never reaches 0, so it never enters queue. order then holds only the courses outside the cycle. Returning that partial order says the schedule is possible when it is not.

    Example: Course Schedule II (LC 210): numCourses = 3, prerequisites = [[1,0],[2,1],[1,2]]
    Mistake: returns [0]Fix: returns []
  2. 2

    Gotcha 2: Adding the edge as a -> b instead of b -> a

    Mistake
    adj[dest].append(src)
    in_degree[src] += 1
    Fix
    adj[src].append(dest)
    in_degree[dest] += 1

    What happens: A pair [a, b] means that course b comes before course a. So the edge goes from b to a, and a's count goes up. With the edge reversed, every course comes before its prerequisites. The result is the order read backwards.

    Example: numCourses = 2, prerequisites = [[1,0]]
    Mistake: returns [1, 0]Fix: returns [0, 1]
  3. 3

    Gotcha 3: Starting queue with node 0 only

    Mistake
    queue = deque([0])
    Fix
    queue = deque([i for i in range(num_courses) if in_degree[i] == 0])

    What happens: Every course whose in_degree is 0 can be taken at once. Courses can sit in separate groups with no edge between them. A group that is never seeded never enters order. The final check then sees a short order and returns [].

    Example: numCourses = 2, prerequisites = []
    Mistake: returns []Fix: returns [0, 1]

Deep Dive

What the in-degree counts, and why it works

A prerequisite pair [a, b] means that course b must come before course a. The card turns it into an edge b -> a. The in_degree of a course is the number of edges that point into it, which is the number of prerequisites it still waits for.

A course with in_degree 0 waits for nothing, so it can be taken now. Taking it finishes one prerequisite for each course that depends on it. So in_degree[neighbor] -= 1 records that one prerequisite is done.

A course enters queue only when its count reaches 0. At that moment every prerequisite of it has already been taken. This is the invariant: every node in order or in queue comes after all of its prerequisites.

A trace on four courses

Take Course Schedule II (LC 210) with num_courses = 4 and prerequisites = [[1,0],[2,0],[3,1],[3,2]]. The edges are 0 -> 1, 0 -> 2, 1 -> 3 and 2 -> 3. The counts are in_degree = [0, 1, 1, 2].

Only course 0 has count 0, so queue = [0]. Pop 0 and put it in order. The edge 0 -> 1 lowers in_degree[1] to 0, so 1 joins queue. The edge 0 -> 2 does the same for 2.

Pop 1. The edge 1 -> 3 lowers in_degree[3] from 2 to 1. Course 3 still waits for course 2, so it does not join queue. Pop 2. The edge 2 -> 3 lowers in_degree[3] to 0, so 3 joins queue. Pop 3, which has no edges.

The result is order = [0, 1, 2, 3]. Its length is 4, which equals num_courses, so the answer is valid.

The cycle check

If courses depend on each other in a loop, no course in the loop can go first. Each of them waits for another course of the loop, so its count never reaches 0 and it never enters queue.

Example: num_courses = 3 and prerequisites = [[1,0],[2,1],[1,2]]. Course 0 has count 0 and is taken. After that, course 1 waits for course 2, and course 2 waits for course 1. Both counts stay at 1. order is [0], which is shorter than 3.

So the card ends with return order if len(order) == num_courses else []. Without it, the function would return [0] and claim that a schedule exists. The same check answers Course Schedule (LC 207), which only asks whether all courses can finish.

The cost and what the order means

Building adj and in_degree reads each prerequisite pair once. The loop pops each course at most once and follows each edge once. The time is O(V + E), where V is num_courses and E is the number of pairs. The space is O(V + E) for adj, in_degree, queue and order.

Many correct orders can exist. In the trace, [0, 2, 1, 3] is also valid. LeetCode accepts any valid order, and the card returns the one that follows the queue.

The same loop solves Alien Dictionary (LC 269). Letters are the nodes, and each pair of neighbouring words gives one edge between the first letters that differ.

60-Second Pitch

"I treat each prerequisite pair as a directed edge and count the incoming edges of every course in in_degree. A course with count 0 has no unfinished prerequisite, so I put it in a queue. When I pop a course, I add it to order and lower the count of each course that depends on it. A course whose count reaches 0 joins the queue. If order has all num_courses courses, it is a valid schedule. If it is shorter, the remaining courses are on a cycle and I return an empty list. Each course and each edge is handled once, so the time is O(V + E)."

Common Questions

How does the algorithm detect a cycle?

Every course on a cycle waits for another course of the same cycle, so its in_degree never reaches 0. It never enters queue and never enters order. After the loop, len(order) is smaller than num_courses, and the function returns [].

What are the time and space complexity?

O(V + E) time and O(V + E) space. The code reads each pair once to build adj and in_degree. The loop pops each course once and follows each edge once. The space holds adj, in_degree, queue and order.

Is the order unique?

No. Whenever queue holds more than one course, any of them can go first. In the four-course example, [0, 1, 2, 3] and [0, 2, 1, 3] are both valid. The problems that need one exact order add a rule for ties, for example the smallest letter first, and use a heap instead of a plain queue.

How is this different from a depth-first search for topological order?

Both give a valid order in O(V + E). The depth-first version uses three states per node and puts a node in the answer after all its descendants, then reverses the list. Kahn's algorithm needs no recursion, so it has no stack-depth problem, and its cycle check is the single test len(order) == num_courses.

How do you handle the course-schedule input format?

A pair [a, b] means b before a, so the edge is b -> a and in_degree[a] goes up. The loop for dest, src in prerequisites names the two values by their role. Reading the pair in the other direction produces the order backwards.

SWE Reasoning

Spot it

  • Tasks, courses or build steps have prerequisites, and the question asks for an order that respects all of them.
  • The question asks whether all tasks can finish. The answer is no when the prerequisites contain a cycle.
  • The relations are directed: b before a does not mean a before b.
  • Several tasks may be valid at once, and any valid order is accepted.

The invariant, step by step

  1. For each pair [dest, src], add dest to adj[src] and add 1 to in_degree[dest]. Now in_degree[v] counts the prerequisites of v.
  2. queue starts with every course whose count is 0. These courses wait for nothing, so they are safe to take first.
  3. node = queue.popleft() takes a safe course and appends it to order. Everything it needs is already in order.
  4. For each neighbor of node, in_degree[neighbor] -= 1 records that one prerequisite is done.
  5. When a count reaches 0, all prerequisites of neighbor are in order, so neighbor joins queue.
  6. When queue is empty, every course that could be placed has been placed. If len(order) == num_courses, order is a valid order. If not, the missing courses wait on a cycle.

Traps

  • No length check. Returning order after a cycle gives a partial order. For [[1,0],[2,1],[1,2]] it returns [0] instead of [].
  • Edge in the wrong direction. Using adj[dest].append(src) makes every course come before its prerequisites. For [[1,0]] it returns [1, 0] instead of [0, 1].
  • Seeding queue with course 0 only. Courses with count 0 can be in separate groups. A group that is never seeded never enters order, and the length check wrongly returns [].
  • Checking the count before lowering it. Test in_degree[neighbor] == 0 after the -= 1, not before. A course would otherwise enter queue one step late or never.

Practice

Canonical LeetCode:Course Schedule II (LC 210)Course Schedule (LC 207)Alien Dictionary (LC 269)Course Schedule IV (LC 1462)Parallel Courses (LC 1136)Parallel Courses III (LC 2050)Sequence Reconstruction (LC 444)Largest Color Value in a Directed Graph (LC 1857)Minimum Height Trees (LC 310)
Theory Context•Graph Algorithms

Builds on Level-Order BFS, which you haven’t finished yet.

P1MediumBuilt on Level-Order BFS~6 min speedrun

Topological Sort (Kahn's Algorithm)

Built on
Level-Order BFSAn in-degree count per node decides when a node may enter queue.
The problem

Course Schedule II (LC 210)(opens leetcode.com)

You have numCourses courses to complete, numbered 0 through numCourses - 1, and a list prerequisites where each entry [ai, bi] means course bi must be completed before course ai.

  • So an entry [0, 1] means course 1 has to be finished before course 0.

Produce one valid order in which all courses can be taken; any correct order is acceptable. If no order can complete every course, return an empty list instead.

Example 1
Input:numCourses = 2, prerequisites = [[1,0]]
Output:[0,1]
Course 1 depends on course 0, so 0 must come first, giving the order [0, 1].

Topological Sort

in-degree 0 vertices ➔ drain dependencies ➔ linear topological order
Spot it when: Tasks have prerequisites and you need an order, want to know if everything can finish, or need the number of rounds to finish.
Not this one if…
  • You must check where every path from one start ends, as in All Paths from Source Lead to Destination (LC 1059), not put all the nodes in an order. See Cycle Detection
Topological Sort
Python
1from collections import deque, defaultdict
2from typing import List
3 
4def topological_sort(num_courses: int, prerequisites: List[List[int]]) -> List[int]:
5 """Course Schedule II with Kahn's algorithm: an order to take every course, or [] if impossible.
6 Time: O(V + E) | Space: O(V + E)
7 """
8 adj = defaultdict(list)
9 in_degree = [0] * num_courses
10 
11 for dest, src in prerequisites: # [a, b] means b before a: edge b -> a
12 adj[src].append(dest)
13 in_degree[dest] += 1
14 
15 queue = deque([i for i in range(num_courses) if in_degree[i] == 0]) # No prerequisites left
16 order = []
17 
18 while queue:
19 node = queue.popleft()
20 order.append(node)
21 for neighbor in adj[node]:
22 in_degree[neighbor] -= 1 # One prerequisite of neighbor is done
23 if in_degree[neighbor] == 0:
24 queue.append(neighbor)
25 
26 return order if len(order) == num_courses else [] # Nodes left over: a cycle
Invariant: Every node in order or in queue comes after all of its prerequisites, and in_degree[v] counts the prerequisites of v not yet in order.

A course can be taken once all its prerequisites are done, so count each node's in_degree, start queue with every node whose count is 0, and each time you pop node, lower its neighbours' counts and add any that reach 0. A node enters queue only after every edge into it has been removed, so it lands in order after all its prerequisites. The trap is the cycle check: nodes on a cycle never reach 0, so return order only when len(order) == num_courses, never a partial order.

See it run

Topological Sort (Kahn's algorithm), step by step

queue [0] · order [] · in_degree [0, 1, 1, 2]

queue [0] · order [] · in_degree [0, 1, 1, 2]

queue [] · order [0] · in_degree [0, 1, 1, 2]

queue [1] · order [0] · in_degree [0, 0, 1, 2]

queue [1, 2] · order [0] · in_degree [0, 0, 0, 2]

queue [2] · order [0, 1] · in_degree [0, 0, 0, 2]

queue [2] · order [0, 1] · in_degree [0, 0, 0, 1]

queue [] · order [0, 1, 2] · in_degree [0, 0, 0, 1]

queue [3] · order [0, 1, 2] · in_degree [0, 0, 0, 0]

queue [] · order [0, 1, 2, 3] · in_degree [0, 0, 0, 0]

queue [] · order [0, 1, 2, 3] · in_degree [0, 0, 0, 0]

queue [] · order [0, 1, 2, 3] · in_degree [0, 0, 0, 0] · answer [0, 1, 2, 3]

Each column is a course number. The rows are in_degree, queue (front on the left) and order.

  • node the course just popped from queue and added to order
  • neighbor a course that depends on node; its count drops by 1
  • pushed a course with count 0, added to queue

Each pair [a, b] adds an edge b -> a and raises in_degree[a]. The counts are [0, 1, 1, 2].

queue starts with every course whose count is 0: [0]. order is empty.

Pop node = 0 and append it to order. order is now [0].

Edge 0 -> 1: in_degree[1] drops to 0, so every prerequisite of 1 is in order. 1 joins queue.

Edge 0 -> 2: in_degree[2] drops to 0, so every prerequisite of 2 is in order. 2 joins queue.

Pop node = 1 and append it to order. order is now [0, 1].

Edge 1 -> 3: in_degree[3] drops to 1. 3 still waits for another prerequisite, so it stays out of queue.

Pop node = 2 and append it to order. order is now [0, 1, 2].

Edge 2 -> 3: in_degree[3] drops to 0, so every prerequisite of 3 is in order. 3 joins queue.

Pop node = 3 and append it to order. order is now [0, 1, 2, 3].

Check: len(order) = 4 equals num_courses = 4, so no course was left behind.

Done: the order is [0, 1, 2, 3].

in_degree
00
11
21
32
queue
0
order
1 / 12
Gotchas1 of 3

Gotcha 1 of 3

  1. 1
    The trap

    Gotcha 1: Returning order without checking len(order) == num_courses

    Mistake
    return order
    Fix
    return order if len(order) == num_courses else []

    What happens: A course on a cycle waits for another course on the same cycle. Its in_degree never reaches 0, so it never enters queue. order then holds only the courses outside the cycle. Returning that partial order says the schedule is possible when it is not.

    Example: Course Schedule II (LC 210): numCourses = 3, prerequisites = [[1,0],[2,1],[1,2]]
    Mistake: returns [0]Fix: returns []
  2. 2

    Gotcha 2: Adding the edge as a -> b instead of b -> a

    Mistake
    adj[dest].append(src)
    in_degree[src] += 1
    Fix
    adj[src].append(dest)
    in_degree[dest] += 1

    What happens: A pair [a, b] means that course b comes before course a. So the edge goes from b to a, and a's count goes up. With the edge reversed, every course comes before its prerequisites. The result is the order read backwards.

    Example: numCourses = 2, prerequisites = [[1,0]]
    Mistake: returns [1, 0]Fix: returns [0, 1]
  3. 3

    Gotcha 3: Starting queue with node 0 only

    Mistake
    queue = deque([0])
    Fix
    queue = deque([i for i in range(num_courses) if in_degree[i] == 0])

    What happens: Every course whose in_degree is 0 can be taken at once. Courses can sit in separate groups with no edge between them. A group that is never seeded never enters order. The final check then sees a short order and returns [].

    Example: numCourses = 2, prerequisites = []
    Mistake: returns []Fix: returns [0, 1]

Deep Dive

What the in-degree counts, and why it works

A prerequisite pair [a, b] means that course b must come before course a. The card turns it into an edge b -> a. The in_degree of a course is the number of edges that point into it, which is the number of prerequisites it still waits for.

A course with in_degree 0 waits for nothing, so it can be taken now. Taking it finishes one prerequisite for each course that depends on it. So in_degree[neighbor] -= 1 records that one prerequisite is done.

A course enters queue only when its count reaches 0. At that moment every prerequisite of it has already been taken. This is the invariant: every node in order or in queue comes after all of its prerequisites.

A trace on four courses

Take Course Schedule II (LC 210) with num_courses = 4 and prerequisites = [[1,0],[2,0],[3,1],[3,2]]. The edges are 0 -> 1, 0 -> 2, 1 -> 3 and 2 -> 3. The counts are in_degree = [0, 1, 1, 2].

Only course 0 has count 0, so queue = [0]. Pop 0 and put it in order. The edge 0 -> 1 lowers in_degree[1] to 0, so 1 joins queue. The edge 0 -> 2 does the same for 2.

Pop 1. The edge 1 -> 3 lowers in_degree[3] from 2 to 1. Course 3 still waits for course 2, so it does not join queue. Pop 2. The edge 2 -> 3 lowers in_degree[3] to 0, so 3 joins queue. Pop 3, which has no edges.

The result is order = [0, 1, 2, 3]. Its length is 4, which equals num_courses, so the answer is valid.

The cycle check

If courses depend on each other in a loop, no course in the loop can go first. Each of them waits for another course of the loop, so its count never reaches 0 and it never enters queue.

Example: num_courses = 3 and prerequisites = [[1,0],[2,1],[1,2]]. Course 0 has count 0 and is taken. After that, course 1 waits for course 2, and course 2 waits for course 1. Both counts stay at 1. order is [0], which is shorter than 3.

So the card ends with return order if len(order) == num_courses else []. Without it, the function would return [0] and claim that a schedule exists. The same check answers Course Schedule (LC 207), which only asks whether all courses can finish.

The cost and what the order means

Building adj and in_degree reads each prerequisite pair once. The loop pops each course at most once and follows each edge once. The time is O(V + E), where V is num_courses and E is the number of pairs. The space is O(V + E) for adj, in_degree, queue and order.

Many correct orders can exist. In the trace, [0, 2, 1, 3] is also valid. LeetCode accepts any valid order, and the card returns the one that follows the queue.

The same loop solves Alien Dictionary (LC 269). Letters are the nodes, and each pair of neighbouring words gives one edge between the first letters that differ.

60-Second Pitch

"I treat each prerequisite pair as a directed edge and count the incoming edges of every course in in_degree. A course with count 0 has no unfinished prerequisite, so I put it in a queue. When I pop a course, I add it to order and lower the count of each course that depends on it. A course whose count reaches 0 joins the queue. If order has all num_courses courses, it is a valid schedule. If it is shorter, the remaining courses are on a cycle and I return an empty list. Each course and each edge is handled once, so the time is O(V + E)."

Common Questions

How does the algorithm detect a cycle?

Every course on a cycle waits for another course of the same cycle, so its in_degree never reaches 0. It never enters queue and never enters order. After the loop, len(order) is smaller than num_courses, and the function returns [].

What are the time and space complexity?

O(V + E) time and O(V + E) space. The code reads each pair once to build adj and in_degree. The loop pops each course once and follows each edge once. The space holds adj, in_degree, queue and order.

Is the order unique?

No. Whenever queue holds more than one course, any of them can go first. In the four-course example, [0, 1, 2, 3] and [0, 2, 1, 3] are both valid. The problems that need one exact order add a rule for ties, for example the smallest letter first, and use a heap instead of a plain queue.

How is this different from a depth-first search for topological order?

Both give a valid order in O(V + E). The depth-first version uses three states per node and puts a node in the answer after all its descendants, then reverses the list. Kahn's algorithm needs no recursion, so it has no stack-depth problem, and its cycle check is the single test len(order) == num_courses.

How do you handle the course-schedule input format?

A pair [a, b] means b before a, so the edge is b -> a and in_degree[a] goes up. The loop for dest, src in prerequisites names the two values by their role. Reading the pair in the other direction produces the order backwards.

SWE Reasoning

Spot it

  • Tasks, courses or build steps have prerequisites, and the question asks for an order that respects all of them.
  • The question asks whether all tasks can finish. The answer is no when the prerequisites contain a cycle.
  • The relations are directed: b before a does not mean a before b.
  • Several tasks may be valid at once, and any valid order is accepted.

The invariant, step by step

  1. For each pair [dest, src], add dest to adj[src] and add 1 to in_degree[dest]. Now in_degree[v] counts the prerequisites of v.
  2. queue starts with every course whose count is 0. These courses wait for nothing, so they are safe to take first.
  3. node = queue.popleft() takes a safe course and appends it to order. Everything it needs is already in order.
  4. For each neighbor of node, in_degree[neighbor] -= 1 records that one prerequisite is done.
  5. When a count reaches 0, all prerequisites of neighbor are in order, so neighbor joins queue.
  6. When queue is empty, every course that could be placed has been placed. If len(order) == num_courses, order is a valid order. If not, the missing courses wait on a cycle.

Traps

  • No length check. Returning order after a cycle gives a partial order. For [[1,0],[2,1],[1,2]] it returns [0] instead of [].
  • Edge in the wrong direction. Using adj[dest].append(src) makes every course come before its prerequisites. For [[1,0]] it returns [1, 0] instead of [0, 1].
  • Seeding queue with course 0 only. Courses with count 0 can be in separate groups. A group that is never seeded never enters order, and the length check wrongly returns [].
  • Checking the count before lowering it. Test in_degree[neighbor] == 0 after the -= 1, not before. A course would otherwise enter queue one step late or never.

Practice

Canonical LeetCode:Course Schedule II (LC 210)Course Schedule (LC 207)Alien Dictionary (LC 269)Course Schedule IV (LC 1462)Parallel Courses (LC 1136)Parallel Courses III (LC 2050)Sequence Reconstruction (LC 444)Largest Color Value in a Directed Graph (LC 1857)Minimum Height Trees (LC 310)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•