The deep dive covers LeetCode 207 & 210 in full depth with Kahn's BFS, DFS 3-color marking, edge cases, real-world applications (npm, Airflow, Kubernetes), and interview simulation.
🔬 Saturday Deep Dive: 拓扑排序 — Course Schedule I & II (17 min read)
Topological sort produces a linear ordering of a DAG's nodes such that every dependency appears before the node depending on it. It is the backbone of package managers, build systems, task schedulers, and database migration tools.
If a cycle exists — A depends on B depends on A — no valid ordering exists. Detecting that is half the problem and where most interview candidates trip.
A DAG (Directed Acyclic Graph) has directed edges and no cycles.
Topological sort gives a linear ordering where for every directed edge u → v, u appears before v.
In-degree = number of incoming edges to a node. Zero in-degree means "no prerequisites."
Two canonical approaches:
Kahn's (BFS)
DFS (3-color)
Intuition
Peel off dependency-free nodes
Go deep; finish-order reversed = topo-order
Cycle detection
processed < n at the end
Back edge to VISITING node
Returns order?
Yes, directly
Yes, via post-order collection
Preferred for?
Interviews (transparent)
When DFS is already required
Time & Space: O(V + E) for both.
Part 2: Step-by-Step Implementation / 一步一步实现 (8 min)
Approach 1: Kahn's Algorithm (BFS) — Recommended
中文: Kahn's 算法直观、易调试,还能直接检测环路。面试时优先使用。
from collections import deque
from typing import List
def canFinish(numCourses: int, prerequisites: List[List[int]]) -> bool:
"""
LeetCode 207: Can you finish all courses?
Return True if the prerequisite graph has no cycle.
Example:
numCourses=4, prerequisites=[[1,0],[2,0],[3,1],[3,2]]
Edges: 0->1, 0->2, 1->3, 2->3 (valid DAG)
"""# Build adjacency list: adj[prereq] = [courses that need this prereq]
adj = [[] for _ in range(numCourses)]
in_degree = [0] * numCourses
for course, prereq in prerequisites:
adj[prereq].append(course) # prereq must come BEFORE course
in_degree[course] += 1# course has one more prerequisite# Start with all courses that have zero prerequisites
queue = deque(c for c in range(numCourses) if in_degree[c] == 0)
processed = 0while queue:
course = queue.popleft()
processed += 1for next_course in adj[course]:
in_degree[next_course] -= 1# one fewer prereq neededif in_degree[next_course] == 0:
queue.append(next_course) # now ready to take# If all courses processed → no cycle → can finishreturn processed == numCourses
def findOrder(numCourses: int, prerequisites: List[List[int]]) -> List[int]:
"""
LeetCode 210: Return the actual valid course order, or [] if impossible.
Identical to canFinish — just collect the processing order.
"""
adj = [[] for _ in range(numCourses)]
in_degree = [0] * numCourses
for course, prereq in prerequisites:
adj[prereq].append(course)
in_degree[course] += 1
queue = deque(c for c in range(numCourses) if in_degree[c] == 0)
order = []
while queue:
course = queue.popleft()
order.append(course) # record the order — only new line vs canFinishfor next_course in adj[course]:
in_degree[next_course] -= 1if in_degree[next_course] == 0:
queue.append(next_course)
# If we couldn't process all courses, a cycle existsreturn order if len(order) == numCourses else []
Approach 2: DFS with 3-Color Cycle Detection
中文: DFS 方法用三种状态标记每个节点。面试官要求 DFS 时用这套写法。
def findOrder_dfs(numCourses: int, prerequisites: List[List[int]]) -> List[int]:
"""
DFS-based topological sort with 3-color marking.
State encoding:
0 = UNVISITED (white) — not yet explored
1 = VISITING (gray) — on the current DFS stack; back edge here = CYCLE
2 = VISITED (black) — fully processed, safe
"""# Build: adj[course] = [prerequisites of this course]# (Direction: course → what it needs)
adj = [[] for _ in range(numCourses)]
for course, prereq in prerequisites:
adj[course].append(prereq)
state = [0] * numCourses # all start UNVISITED
result = []
def dfs(node: int) -> bool:
"""Returns False if a cycle is detected."""if state[node] == 1:
returnFalse# Back edge to current stack → CYCLEif state[node] == 2:
returnTrue# Already processed → skip safely
state[node] = 1# Mark VISITING (on current stack)for prereq in adj[node]:
ifnot dfs(prereq):
returnFalse# Propagate cycle detection up
state[node] = 2# Mark VISITED (done)
result.append(node) # Post-order: node added AFTER all its prerequisitesreturnTrue# Handle disconnected components — try every nodefor course in range(numCourses):
if state[course] == 0:
ifnot dfs(course):
return [] # Cycle found# result is already in valid topological order# (prereqs get added first because we go deep before adding current node)return result
Algorithm Trace / 算法追踪 🔍
Input:numCourses=3, prerequisites=[[1,0],[2,1]]
Meaning: to take 1, first take 0; to take 2, first take 1.
工作流调度的核心——只有当所有上游任务完成,下游任务才能启动。Airflow DAG 的命名就来自拓扑排序的基础数据结构。
English:
npm/pip install resolves a dependency graph via topological sort. Circular dependency → error. This is exactly LeetCode 207.
Make/Bazel/Gradle topologically order build targets so prerequisites compile first.
Alembic (SQLAlchemy migrations) sorts migration scripts by dependency to guarantee schema consistency.
Kubernetes operators apply resources in dependency order; ArgoCD's sync-wave is topo-sort made visible.
Airflow/Temporal DAGs — "DAG" literally stands for the same data structure. Task execution follows topological order; parallel tasks are those at the same BFS level.
The universal pattern: Any "X must happen before Y" constraint set across tasks → build a graph, topologically sort it.
Part 5: Interview Simulation / 面试模拟 (3 min)
Scenario: "You have n tasks and a list of [task, dependency] pairs. Return a valid execution order, or an empty list if impossible."
Q1: Walk me through your approach before writing any code.
→ "I'll model it as a directed graph where `dependency → task`. Then I'll use Kahn's BFS: initialize a queue with all tasks that have no dependencies (in-degree 0), process them one by one decrementing neighbors' in-degrees, and enqueue any that hit zero. If I process all `n` tasks, no cycle exists and the processing order is my answer. If fewer — there's a cycle, return empty."
Q2: What if there's a cycle?
→ "Kahn's handles it naturally. Nodes in a cycle always have at least one incoming edge from within the cycle, so their in-degree never reaches zero. They never get enqueued. After the BFS, len(order) < n tells me a cycle exists. No extra cycle-detection code needed — it falls out of the algorithm for free."
Q3: Which tasks can run in parallel?
→ "All tasks enqueued in the same BFS 'round' have no dependencies on each other — they can run concurrently. Process the queue level by level instead of node by node:"
def parallel_order(n: int, deps: List[List[int]]) -> List[List[int]]:
"""Returns execution waves; tasks in same wave run in parallel."""
adj = [[] for _ in range(n)]
in_degree = [0] * n
for task, prereq in deps:
adj[prereq].append(task)
in_degree[task] += 1
queue = deque(t for t in range(n) if in_degree[t] == 0)
waves = []
while queue:
wave_size = len(queue) # snapshot current level
wave = []
for _ in range(wave_size):
task = queue.popleft()
wave.append(task)
for nxt in adj[task]:
in_degree[nxt] -= 1if in_degree[nxt] == 0:
queue.append(nxt)
waves.append(wave)
return waves if sum(len(w) for w in waves) == n else []
# e.g. [[0,1], [2,3], [4]] — first two tasks run in parallel, etc.
Q4: How is this different from regular BFS/DFS on an undirected graph?
→ "Direction is fundamental. Regular BFS on an undirected graph finds shortest paths — edges have no inherent ordering. Topological sort only applies to directed graphs with no cycles. The BFS here isn't finding shortest paths; it's finding a processing order that respects dependency direction. You can't topologically sort a tree or undirected graph meaningfully."
Q5: What's the time/space complexity? Where does it break down in practice?
→ "O(V+E) time — each node and edge visited exactly once. O(V+E) space for adjacency list + O(V) for in-degree and queue. The algorithm itself is never the bottleneck. In practice: (1) building the dependency graph from a database or config system can be slow — that's the I/O bottleneck; (2) if the graph has millions of nodes, the adjacency list memory dominates; (3) for very wide DAGs (many parallel tasks), queue overhead matters. The algorithm is essentially optimal — you can't do better than O(V+E) since you must read every edge at least once."