Directed Graph
A set of vertices connected by one-way edges: an edge u→v does not imply v→u.
Definition
A directed graph G = (V, E) has vertices V and a set of ordered pairs E ⊆ V × V. The edge (u, v) — written u → v — allows travel from u to v only. Each vertex has an in-degree (edges arriving) and an out-degree (edges leaving).
Direction changes almost everything: reachability is not symmetric, "connected" splits into weakly and strongly connected, a cycle must follow arrows, and a digraph without cycles is a DAG (Directed Acyclic Graph) that admits a Topological Sort. Algorithms such as Kahn's Algorithm, Tarjan's SCC Algorithm, and Kosaraju's Algorithm exist only for directed graphs.
Typical representations are the Adjacency List (adj[u] holds the out-neighbors of u), the Adjacency Matrix (M[u][v] = 1 iff u → v, not necessarily symmetric), and the Edge List. Reversing the graph (flip every edge) is a common preprocessing step to answer "who can reach v?" questions.
Intuition
A mental model before the formal terms.
Think of one-way streets. You may drive from A to B, but getting back may require a long detour — or be impossible. A neighborhood where every corner can reach every other corner by legal driving is a strongly connected component; the map of neighborhoods with the one-way roads between them never loops (it is a DAG).
Dependencies are the other mental model: "task B needs task A first" is the edge A → B. A cycle means an impossible schedule; no cycle means a valid ordering exists.
How it works
- Store vertices
0…V-1and out-neighbor listsadj[u]. Addingu → vappendsvtoadj[u]only (unlike an undirected graph, which appends both ways). - Track in-degrees in an array
indeg[v]when needed for Kahn's Algorithm or source detection (in-degree 0 = source, out-degree 0 = sink). - Traverse with Breadth-First Search (BFS) or Depth-First Search (DFS) following out-edges. A vertex
vis reachable fromuiff DFS fromuvisitsv. - Detect cycles with DFS three-colour marking: white (unvisited), grey (on the current recursion stack), black (finished). Meeting a grey vertex means a back edge → cycle.
- Build the reverse graph
radj[v]containingufor everyu → vto answer "which vertices can reachv?" or to run Kosaraju's Algorithm. - Condense strongly connected components into single nodes; the result is always a DAG (Directed Acyclic Graph).
Why it works
DFS on a directed graph classifies every edge as tree, back, forward, or cross. A back edge (to a grey ancestor) exists iff the graph has a cycle, because a cycle's "last discovered" vertex must have an edge back to an ancestor still on the stack.
Strong connectivity is an equivalence relation (reflexive, symmetric, transitive), so the vertices partition into components. Any cycle between components would merge them, so the condensation is acyclic.
Operations
| Operation | Description | Cost |
|---|---|---|
| addEdge(u, v) | Append v to adj[u]; increment indeg[v]. | O(1) |
| removeEdge(u, v) | Delete v from adj[u]. | O(deg⁺(u)) |
| outNeighbors(u) | Iterate adj[u]. | O(deg⁺(u)) |
| inNeighbors(v) | Requires the reverse graph, or a scan of all edges. | O(deg⁻(v)) with reverse list, O(V + E) without |
| hasEdge(u, v) | Scan adj[u] (or O(1) with a matrix / hash set). | O(deg⁺(u)) |
| reverse() | Build radj with all edges flipped. | O(V + E) |
| reachable(u, v) | DFS/BFS from u. | O(V + E) |
Recognition
How to tell a problem wants this.
- Words like "prerequisite", "depends on", "follows", "one-way", "links to", "flows to", "before/after".
- The input is a list of pairs
[a, b]meaning "a must come before b" or "a points to b". - Questions about ordering, deadlock, reachability from a source, or "can every page be reached from the home page".
- Web pages and hyperlinks, function call graphs, state machines, import graphs.
Interactive demo
Play, step, change the input. ← → and space work too.
Showing the closely related Depth-First Search (DFS) visualization.
1stack = [source]; visited = {}; order = []2while stack not empty:3 u = stack.pop()4 if u in visited: continue5 visited.add(u); order.append(u)6 for v in reversed(neighbors(u)):7 if v not in visited: stack.push(v)Pseudocode
1adj = [[] for _ in range(V)]; indeg = [0] * V2add_edge(u, v): adj[u].append(v); indeg[v] += 13has_cycle(): color = WHITE * V4 dfs(u): color[u] = GREY5 for v in adj[u]: if color[v] == GREY: return True; if WHITE and dfs(v): return True6 color[u] = BLACK; return False7reverse(): for u: for v in adj[u]: radj[v].append(u)Implementation
1class DirectedGraph:2 """A digraph: edges have direction, so u -> v does not imply v -> u.3 Out-degree and in-degree are different numbers, and reversal is4 meaningful."""5 61 · State: out[u] holds successors; in-degrees are derived, not stored7 def __init__(self, n: int) -> None:8 self.out: list[list[int]] = [[] for _ in range(n)]9 10 def add_edge(self, u: int, v: int) -> None:11 self.out[u].append(v)12 13 def __len__(self) -> int:14 return len(self.out)15 16 def successors(self, u: int) -> list[int]:17 return self.out[u]18 19 def out_degree(self, u: int) -> int:20 return len(self.out[u])21 222 · In-degree needs a full pass: nothing points backwards by default23 def in_degrees(self) -> list[int]:24 deg = [0] * len(self.out)25 for row in self.out:26 for v in row:27 deg[v] += 128 return deg29 303 · The reverse graph makes predecessors as cheap as successors31 def reversed(self) -> "DirectedGraph":32 r = DirectedGraph(len(self.out))33 for u, row in enumerate(self.out):34 for v in row:35 r.out[v].append(u)36 return r37 384 · Reachability follows edges forward only — asymmetric by nature39 def reachable_from(self, start: int) -> list[bool]:40 seen = [False] * len(self.out)41 seen[start] = True42 stack = [start]43 while stack:44 u = stack.pop()45 for v in self.out[u]:46 if not seen[v]:47 seen[v] = True48 stack.append(v)49 return seen50 515 · A cycle exists iff a DFS finds a back edge into the active path52 def has_cycle(self) -> bool:53 state = [0] * len(self.out) # 0 = new, 1 = on the path, 2 = done54 for s in range(len(self.out)):55 if state[s] != 0:56 continue57 stack = [[s, 0]] # [vertex, next neighbour index]58 state[s] = 159 while stack:60 frame = stack[-1]61 u, i = frame[0], frame[1]62 if i < len(self.out[u]):63 frame[1] += 164 v = self.out[u][i]65 if state[v] == 1:66 return True # back edge67 if state[v] == 0:68 state[v] = 169 stack.append([v, 0])70 else:71 state[u] = 272 stack.pop()73 return False[[] for _ in range(n)]creates distinct successor lists;[[]] * nwould alias them.in_degrees()is the derived quantity — the pass that Kahns topological sort begins with.reachable_frommarksseen[v] = Truebefore appending, which bounds the stack at O(V).has_cyclestores frames as two-element *lists* rather than tuples, becauseframe[1] += 1must mutate the stacked entry — a tuple would be immutable.while stack:uses list truthiness, the Pythonic empty test.
The iterative form avoids RecursionError, which CPython raises at about 1000 frames — reachable on a graph of only a few thousand vertices in a path.
- CPython caps recursion at
sys.getrecursionlimit()(1000 by default); raising it risks a genuine C-stack segfault, so an explicit stack is the safe answer. - Mutable list frames are needed for the in-place cursor increment; a
tupleframe would require popping and re-pushing a replacement. networkx.is_directed_acyclic_graph(G)andnetworkx.reverse(G)cover both of these directly for real work.graphlib.TopologicalSorterin the standard library raisesCycleError, which makes it an off-the-shelf cycle detector for DAG-shaped input.
- Writing the DFS recursively and hitting
RecursionErroron a long path. - Using a tuple for the DFS frame and then trying
frame[1] += 1, which raisesTypeError. - Reporting a cycle whenever a visited vertex is seen, which flags every DAG containing two paths to the same vertex.
- Mutating a DFS frame in place needs different mechanics per language: a C++ structured binding to a reference, a JavaScript/TypeScript object (references by nature), and a Python *list* rather than a tuple.
- Recursion depth is the practical reason all four use an explicit stack, but the limits differ sharply — CPython raises
RecursionErrornear 1000 frames, JavaScript engines throw near 10000, and C++ simply overflows the stack with no diagnostic. - Library escape hatches exist only in Python (
graphlib.TopologicalSorter,networkx) and, for some algorithms, C++ (Boost.Graph); JS/TS have neither in the standard library. - Dense flag arrays: C++
std::vector<bool>bit-packs automatically, JS/TS can opt intoInt8Array, and Pythonlist[bool]stores pointers to the two singleton bool objects — cheap in time, not in memory.
Complexity
| Operation | Average | Worst | Note |
|---|---|---|---|
| Access | O(1) | O(1) | Vertex by id. |
| Search | O(deg(u)) | O(V) | Edge (u, v) lookup by scanning u's list. |
| Insert | O(1) | O(1) | Append an edge. |
| Delete | O(deg(u)) | O(V) | Remove an edge from u's list. |
| Update | O(deg(u)) | O(V) | Find the edge, then change its weight. |
| Reverse | O(V + E) | O(V + E) | |
| Cycle check | O(V + E) | O(V + E) | |
| Reachability | O(V + E) | O(V + E) | |
| Space | O(V + E) | Adjacency-list representation; a matrix costs O(V²). | |
Advantages & disadvantages
- Models asymmetric relationships exactly: dependencies, links, flows, transitions.
- Enables ordering algorithms (topological sort) and dependency analysis.
- Storage is half that of the equivalent undirected adjacency list because each edge is stored once.
- Reachability is one-way, so "connected" is ambiguous and many problems require SCC decomposition first.
- In-neighbors are not directly available from an out-adjacency list — a second, reversed structure is needed.
- Cycles are easy to introduce accidentally in dependency data and must be checked explicitly.
Use cases
- Build systems and package managers: Topological Sort of the dependency graph.
- Course prerequisites (Course Schedule I/II).
- Web graph / PageRank, citation networks, social "follows".
- Compilers: control-flow graphs, call graphs, SSA dominator trees.
- State machines and game boards where moves are irreversible.
- The relationship is inherently one-way: prerequisites, links, flows, transitions.
- You need an ordering (topological sort) or dependency/deadlock analysis.
- Reachability questions are asymmetric ("can A reach B" differs from "can B reach A").
- The relationship is symmetric (friendship, road with two-way traffic) — an Undirected Graph halves the bookkeeping and enables Union-Find (Disjoint Set Union).
- You only need "are these in the same group" — undirected connectivity or Union-Find (Disjoint Set Union) is simpler than SCCs.
- The graph is a tree with a known root — a parent array or child lists suffice.
Alternatives
Common mistakes
- Adding the edge in both directions out of habit, silently turning the digraph undirected.
- Using the undirected cycle check (visited + parent) on a digraph — it reports false cycles on cross edges; use three colours.
- Running Union-Find (Disjoint Set Union) on a directed graph to test connectivity — DSU ignores direction.
- Forgetting that in-degree information must be maintained separately from out-adjacency.
- Assuming DFS from one source visits every vertex; a digraph may need multiple starts.
Interview patterns
- Course Schedule: detect a cycle, or produce an order with Kahn's Algorithm.
- Find the "celebrity" / a vertex with in-degree n-1 and out-degree 0.
- Reconstruct Itinerary: Eulerian path in a directed multigraph (Hierholzer).
- Alien Dictionary: build a digraph of character orderings, then topologically sort.
- Count SCCs, or find the minimum edges to make the graph strongly connected.
- Choosing between BFS, DFS, Dijkstra and DPAdvanced
- Stack versus queueBeginner
- Recursion versus iterationIntermediate
- When space complexity mattersIntermediate
- Course ScheduleIntermediate
- Network Delay TimeAdvanced
- Number of IslandsIntermediate