Graphs

Medium-Hard
Time: O(V + E) for DFS/BFS, O((V+E)logV) for Dijkstra, O(E log E) for KruskalSpace: O(V + E)
0/33
problems solved
0%
1.

What is a Graph?

A graph is a collection of nodes (vertices) connected by edges. Unlike trees, graphs can have cycles, multiple paths between nodes, and nodes with any number of connections.

Graphs model relationships: social networks (friends), maps (cities and roads), dependencies (course prerequisites), and many more real-world scenarios.

Two main representations:

Adjacency List - Store neighbors for each node. Space efficient for sparse graphs.

Adjacency Matrix - 2D array where matrix[i][j] = 1 if edge exists. Fast edge lookup but O(V^2) space.

For most interview problems, adjacency list is preferred because graphs are usually sparse.

Code
Java
2.

DFS vs BFS: When to Use Which

Both DFS and BFS explore all reachable nodes, but they do it in different orders with different trade-offs.

DFS (Depth-First Search)

  • Goes deep before going wide
  • Uses stack (or recursion)
  • Good for: exploring all paths, detecting cycles, topological sort, backtracking
  • Memory: O(h) where h is max depth

BFS (Breadth-First Search)

  • Explores level by level
  • Uses queue
  • Good for: shortest path in unweighted graph, level-order traversal
  • Memory: O(w) where w is max width

Key insight: BFS guarantees shortest path in unweighted graphs because it explores all nodes at distance k before any node at distance k+1. DFS does NOT give shortest path - it gives A path.

Code
Java

Interactive Grid Traversal

Grid Traversal: Number of Islands

DFS/BFS to find connected components in a grid

Speed:
Queue (FIFO)
Empty
Islands Found
0
Click Play to start finding islands
Unvisited Land
Currently Exploring
Visited (Island)
Water

BFS: Uses a queue (FIFO) - explores level by level, spreading outward like ripples.

3.

Grid as a Graph: Number of Islands

A 2D grid is really a graph where each cell is a node, and adjacent cells (up/down/left/right) are connected by edges.

The classic 'Number of Islands' problem: count connected components of '1's in a grid.

Approach:

  1. Iterate through every cell
  2. When you find an unvisited '1', that's a new island - increment count
  3. Use DFS/BFS to mark all connected '1's as visited
  4. Continue scanning for more islands

The key insight is that finding connected components in a grid is the same as counting how many times you start a new DFS/BFS traversal.

Code
Java

Interactive Grid Traversal

Grid Traversal: Number of Islands

DFS/BFS to find connected components in a grid

Speed:
Queue (FIFO)
Empty
Islands Found
0
Click Play to start finding islands
Unvisited Land
Currently Exploring
Visited (Island)
Water

BFS: Uses a queue (FIFO) - explores level by level, spreading outward like ripples.

4.

Multi-Source BFS: Rotting Oranges

Standard BFS starts from one source. Multi-source BFS starts from ALL sources simultaneously - they spread outward in parallel.

This is perfect for problems like 'Rotting Oranges' where multiple things spread at the same time, or 'Walls and Gates' where you want distance from ANY gate.

The trick: instead of adding one node to the queue initially, add ALL source nodes. Then BFS as usual. Each 'level' of BFS represents one time unit passing.

Time to rot all oranges = number of BFS levels needed to reach all fresh oranges.

Code
Java
5.

Topological Sort: Course Schedule

Topological sort orders nodes so that for every directed edge A -> B, A comes before B. It only works on DAGs (Directed Acyclic Graphs).

Two main approaches:

Kahn's Algorithm (BFS)

  1. Calculate in-degree (incoming edges) for each node
  2. Add all nodes with in-degree 0 to queue
  3. Process queue: for each node, decrement in-degree of neighbors
  4. When neighbor's in-degree becomes 0, add to queue
  5. If processed count != total nodes, there's a cycle

DFS approach

  1. Use 3 states: unvisited, visiting (in current path), visited
  2. If we reach a 'visiting' node, we found a cycle
  3. Add nodes to result in post-order (after all descendants processed)
  4. Reverse the result

Kahn's is often preferred - easier to detect cycles and track order.

Code
Java

Interactive Topological Sort

Topological Sort: Course Schedule

Kahn's Algorithm - Process courses with no remaining prerequisites

Speed:
Queue (indegree = 0)
Empty
Completed Order
None yet
0
Total Courses
0
Completed
0
Ready
Click Play to start topological sort (Kahn's Algorithm)
Has Prerequisites
Ready (indegree=0)
Processing
Completed
6.

Dijkstra: Weighted Shortest Path

BFS works for unweighted graphs (all edges cost 1). For weighted graphs, we need Dijkstra's algorithm.

The idea: always process the node with smallest known distance first. Use a min-heap (priority queue) to efficiently get this node.

Steps:

  1. Initialize all distances to infinity, except source = 0
  2. Add source to min-heap with distance 0
  3. Pop node with smallest distance
  4. For each neighbor, if going through current node is shorter, update distance and add to heap
  5. Skip nodes we've already processed with a shorter distance

Time: O((V + E) log V) with binary heap.

Note: Dijkstra doesn't work with negative edge weights. For that, use Bellman-Ford.

Code
Java

Interactive Dijkstra

Dijkstra's Algorithm

Find shortest path in weighted graph using priority queue

Speed:
Priority Queue (min-heap by distance)
Empty
Distance Table
Click Play to find shortest path from A to F
Source/Target
In Queue
Processing
Visited

Dijkstra: Always process the node with smallest known distance. Update neighbors if going through current node is shorter. Time: O((V+E) log V) with min-heap.

7.

Clone Graph: Deep Copy with HashMap

Cloning a graph requires creating new nodes while preserving the exact same structure. The challenge: handling cycles and ensuring each node is cloned exactly once.

The solution: use a HashMap to track original -> clone mappings.

Approach (DFS):

  1. If node is null, return null
  2. If we've already cloned this node, return the clone
  3. Create a new clone node
  4. Store in HashMap immediately (before recursing - handles cycles!)
  5. Recursively clone all neighbors
  6. Return the clone

This same pattern works for 'Copy List with Random Pointer' and similar problems.

Code
Java
8.

Implicit Graphs: Word Ladder

Sometimes the graph isn't given explicitly - you have to recognize it's there. In Word Ladder, each word is a node, and two words are connected if they differ by exactly one character.

Approach:

  1. Recognize this is a shortest path problem (minimum transformations)
  2. Use BFS since it's unweighted
  3. Generate neighbors by trying all possible single-character changes
  4. Use a word set for O(1) valid word lookup

Optimization: Instead of generating all neighbors, you can preprocess words into patterns. 'hot' -> 'ot', 'ht', 'ho*'. Words with same pattern are neighbors.

Code
Java
9.

Union-Find: Dynamic Connectivity

Union-Find (Disjoint Set Union) efficiently tracks connected components as edges are added. Two operations:

Find(x): Which component does x belong to? (Returns root/representative) Union(x, y): Merge the components containing x and y

Optimizations that make it nearly O(1) per operation:

  • Path Compression: During find, make all nodes point directly to root
  • Union by Rank/Size: Attach smaller tree under larger tree

Use Union-Find when:

  • Need to track/count connected components dynamically
  • Checking if two nodes are connected
  • Kruskal's MST algorithm
  • Problems like 'Number of Provinces', 'Redundant Connection'
Code
Java
10.

Cycle Detection: The Three-Color Method

Detecting cycles in directed graphs requires tracking whether a node is currently being explored (in the recursion stack).

Three Colors/States:

  • WHITE (0): Unvisited
  • GRAY (1): Currently being explored (in recursion stack)
  • BLACK (2): Fully explored (all descendants processed)

Cycle exists if: We reach a GRAY node during DFS. This means we found a back edge to an ancestor in the current path.

Example:

0 → 1 → 2
↑ ↓
└───3
DFS from 0: 0(gray) → 1(gray) → 2(gray) → 3(gray)
3 → 1: 1 is GRAY = cycle found!

Note: For undirected graphs, just track parent - if we see a visited node that's not the parent, there's a cycle.

Code
Java
11.

Bipartite Graph Check

A graph is bipartite if nodes can be colored with two colors such that no two adjacent nodes have the same color. Equivalent to checking if graph has no odd-length cycles.

Algorithm (BFS or DFS):

  1. Try to 2-color the graph
  2. Start from any uncolored node, color it 0
  3. Color all neighbors with opposite color (1)
  4. If any neighbor already has the same color, NOT bipartite
  5. Repeat for disconnected components

Use Cases:

  • Is graph bipartite?
  • Can we divide students into two groups with no conflicts?
  • Two-coloring problems

Example:

Bipartite: Not Bipartite:
0 - 1 0 - 1
| | | |
3 - 2 2 - 3
\ /
4 (triangle = odd cycle)
Code
Java
12.

Minimum Spanning Tree: Kruskal's Algorithm

A Minimum Spanning Tree connects all nodes with minimum total edge weight. It has exactly V-1 edges for V nodes.

Kruskal's Algorithm:

  1. Sort all edges by weight (ascending)
  2. Process edges from smallest to largest
  3. Add edge if it connects two different components (using Union-Find)
  4. Stop when we have V-1 edges

Why Union-Find? We need to quickly check if two nodes are already connected. Union-Find does this in nearly O(1).

Prim's vs Kruskal's:

  • Kruskal: Sort edges, use Union-Find. Better for sparse graphs.
  • Prim: Grow tree from one node, use min-heap. Better for dense graphs.

Time: O(E log E) for sorting + O(E × α(V)) for Union-Find ≈ O(E log E)

Code
Java
13.

Common Edge Cases

Edge Cases to Handle:

1. Empty Graph / Single Node

if (n == 0) return 0;
if (n == 1) return 0; // No edges needed

2. Disconnected Graph

// Check all components, not just one
for (int i = 0; i < n; i++) {
if (!visited[i]) {
dfs(i); // Start new traversal
}
}

3. Self-Loops

if (u == v) continue; // Skip self-loops if not allowed

4. Multiple Edges Between Same Nodes

// Usually keep all for weighted, or dedupe for unweighted
Set<String> seen = new HashSet<>();
if (seen.add(u + "," + v)) { /* process */ }

5. Source/Destination Same

if (source == destination) return true; // Or 0 for distance

Common Mistakes:

MistakeConsequence
Mark visited after adding to queueProcess same node multiple times
Forget to handle disconnectedMiss some components
DFS for shortest pathGets A path, not shortest
Dijkstra with negative weightsIncorrect results
Topo sort on cyclic graphInfinite loop or wrong order
14.

Choosing the Right Approach

Quick reference for common graph problem types:

Problem TypeAlgorithmTime Complexity
Shortest path (unweighted)BFSO(V + E)
Shortest path (weighted, positive)DijkstraO((V+E) log V)
Shortest path (negative weights)Bellman-FordO(V × E)
Connected componentsDFS/BFS or Union-FindO(V + E)
Cycle detection (directed)DFS 3-colorO(V + E)
Cycle detection (undirected)DFS with parent or Union-FindO(V + E)
Topological orderKahn's BFS or DFSO(V + E)
MSTKruskal's or Prim'sO(E log E)
Bipartite checkBFS/DFS 2-coloringO(V + E)
All paths/backtrackingDFSVaries
Level-order/distanceBFSO(V + E)

Decision Framework:

Need shortest path?
├── Unweighted → BFS
├── Weighted (positive) → Dijkstra
└── Weighted (negative) → Bellman-Ford
Need to detect cycle?
├── Directed → 3-color DFS or Kahn's
└── Undirected → DFS with parent or Union-Find
Need ordering with dependencies?
└── Topological Sort (Kahn's preferred)
Need to track connectivity dynamically?
└── Union-Find
15.

Template Summary

DFS Template:

function dfs(node, visited):
visited.add(node)
// Process node
for neighbor in graph[node]:
if neighbor not in visited:
dfs(neighbor, visited)

BFS Template:

function bfs(start):
queue = [start]
visited = {start}
level = 0
while queue not empty:
size = queue.length
for i in 0..size:
node = queue.dequeue()
// Process node
for neighbor in graph[node]:
if neighbor not in visited:
visited.add(neighbor) // Mark BEFORE adding
queue.enqueue(neighbor)
level++

Topological Sort (Kahn's):

function topoSort(n, edges):
build graph and indegree array
queue = all nodes with indegree 0
order = []
while queue not empty:
node = queue.dequeue()
order.append(node)
for neighbor in graph[node]:
indegree[neighbor]--
if indegree[neighbor] == 0:
queue.enqueue(neighbor)
return order if len(order) == n else [] // [] = cycle

Dijkstra:

function dijkstra(graph, start):
dist = [Infinity] × n
dist[start] = 0
pq = MinHeap([(0, start)])
while pq not empty:
d, u = pq.pop()
if d > dist[u]: continue // Skip stale
for (v, w) in graph[u]:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
pq.push((dist[v], v))
return dist

Interview Tips:

  • Always clarify: directed vs undirected?
  • Draw the graph when debugging
  • Check for disconnected components
  • Remember: BFS = shortest path (unweighted)

Test Your Knowledge

Take a quick 15question quiz to test what you've learned.