Module 6.1

Graph Basics & Traversals

Graphs are everywhere - social networks connecting friends, Google Maps finding your route, the internet linking web pages, and even your Netflix recommendations. Master graph fundamentals including representations, DFS, BFS traversals, connected components, and cycle detection. These are the building blocks for solving real-world network, pathfinding, and connectivity problems!

50 min read
Intermediate
High Frequency
What You'll Learn
  • Graph terminology
  • Adjacency list/matrix
  • DFS traversal
  • BFS traversal
  • Cycle detection
Contents
01

Introduction to Graphs

Graph

A graph is a non-linear data structure consisting of vertices (nodes) connected by edges. Unlike trees which have a hierarchical parent-child relationship, graphs can have any arbitrary connections between nodes, including cycles and multiple paths between the same pair of nodes. Graphs are incredibly versatile and used to model real-world networks like social connections (Facebook friends), transportation systems (roads between cities), computer networks (internet routing), and dependencies (course prerequisites). Every tree is a graph, but not every graph is a tree!

Undirected

Edges have no direction - if A connects to B, then B connects to A. Think: friendships on Facebook (mutual), roads that allow travel in both directions.

Directed

Edges have direction - A→B doesn't mean B→A. Think: Twitter follows (you can follow someone without them following you), one-way streets.

Weighted

Each edge has a cost, distance, or weight associated with it. Think: road distances between cities, flight ticket prices, or bandwidth capacity between network nodes.

Cyclic

Contains at least one cycle (path that returns to start). Acyclic graphs have no cycles - trees and DAGs (Directed Acyclic Graphs) are acyclic.

Term Definition Example
Vertex (Node) The fundamental unit of a graph, representing an entity such as a person, city, or data point. Vertices are typically labeled with numbers (0, 1, 2...) or names. In a social network: each user is a vertex
Edge A connection or link between two vertices. Edges can be directed (one-way) or undirected (two-way), and may carry weights representing cost, distance, or capacity. A friendship between two users is an edge
Degree The number of edges connected to a vertex. In directed graphs, we distinguish between in-degree (incoming edges) and out-degree (outgoing edges). A user with 5 friends has degree 5
Path A sequence of vertices where each adjacent pair is connected by an edge. A simple path visits each vertex at most once. NYC → Chicago → Denver → LA is a path
Cycle A path that starts and ends at the same vertex, forming a closed loop. A graph without cycles is called acyclic. A → B → C → A is a cycle of length 3
Connected A graph is connected if a path exists between every pair of vertices. If not connected, the graph has multiple connected components (isolated groups). Internet: most computers are connected

Practice Questions: Graph Fundamentals

Test your understanding with these hands-on exercises.

Problem: Given an edge list, write a method to count the number of unique vertices and edges in an undirected graph.

// Input: edges = [[0,1], [1,2], [2,0], [1,3]]
// Output: Vertices = 4, Edges = 4

public static int[] countVerticesAndEdges(int[][] edges) {
    // Your code here
}
Show Solution
public static int[] countVerticesAndEdges(int[][] edges) {
    Set vertices = new HashSet<>();
    for (int[] edge : edges) {
        vertices.add(edge[0]);
        vertices.add(edge[1]);
    }
    return new int[]{vertices.size(), edges.length};
}

// Test: countVerticesAndEdges(new int[][]{{0,1},{1,2},{2,0},{1,3}})
// Output: [4, 4]

Problem: Given an adjacency list, calculate the degree (number of edges) for each vertex.

// Input: adj = [[1,2], [0,2,3], [0,1], [1]]
// Output: [2, 3, 2, 1] (degrees for vertices 0,1,2,3)

public static int[] calculateDegrees(List> adj) {
    // Your code here
}
Show Solution
public static int[] calculateDegrees(List> adj) {
    int[] degrees = new int[adj.size()];
    for (int i = 0; i < adj.size(); i++) {
        degrees[i] = adj.get(i).size();
    }
    return degrees;
}

// For undirected: degree = size of neighbor list
// For directed: in-degree requires counting incoming edges
02

Graph Representation

What is Graph Representation?

Graph Representation is how we store a graph in computer memory so we can efficiently perform operations like finding neighbors, checking if an edge exists, or traversing the entire graph. Just like you can write the same number as "5", "five", or "V", a graph can be represented in multiple ways - each with different strengths for different use cases.

Choosing the right representation is crucial for algorithm efficiency. Think of it like choosing between a phone book (alphabetical lookup) vs. a contact list (scroll through) - both store the same information but work better for different tasks. The two most common representations are Adjacency List (memory-efficient, great for sparse graphs) and Adjacency Matrix (fast edge lookup, great for dense graphs).

Adjacency List

Each vertex maintains a list of its neighbors. Like a contact list where each person has a list of their friends' names. Memory efficient - only stores edges that actually exist.

Best for: Sparse graphs (few edges), traversal algorithms, most real-world graphs (social networks, road maps).

Adjacency Matrix

A 2D table (V×V) where cell [i][j] tells if there's an edge from vertex i to j. Like a spreadsheet where rows and columns are vertices - check any intersection instantly!

Best for: Dense graphs (many edges), when you need O(1) edge lookup, weighted graphs, small graphs.

How to Choose?

Use Adjacency List when: E << V² (sparse graph), you need to iterate over neighbors frequently, memory is a concern.

Use Adjacency Matrix when: E ≈ V² (dense graph), you need instant edge existence check, the graph is small (under 1000 vertices).

Adjacency List (Preferred)

Basic Graph Structure
class Graph {
    int V;  // Number of vertices
    List> adj;
    
    Graph(int V) {
        this.V = V;
        adj = new ArrayList<>();
        for (int i = 0; i < V; i++) {
            adj.add(new ArrayList<>());
        }
    }
}
What's happening here?

We store the graph as a list of lists, where V holds how many vertices (nodes) our graph has, and adj is our "adjacency list" - think of it as a contact list for each vertex. In the constructor, we create V empty lists (one for each vertex from 0 to V-1), and later we'll fill these lists with each vertex's neighbors.

Real-world analogy: Imagine a phone book where each person has a list of their friends' phone numbers!

Adding Edges
// Add edge for undirected graph
void addEdge(int u, int v) {
    adj.get(u).add(v);
    adj.get(v).add(u);  // Remove this line for directed graph
}

// Get neighbors of a vertex
List getNeighbors(int v) {
    return adj.get(v);
}
What's happening here?

The addEdge(u, v) method creates a connection between vertex u and vertex v. When we call adj.get(u).add(v), we're saying "go to vertex u's friend list and add v", and adj.get(v).add(u) does the reverse. We add both directions because in an undirected graph, if A knows B, then B also knows A! For a directed graph (like one-way streets), you'd remove the second line to only add u→v, not v→u.

Example: Calling addEdge(0, 1) makes 0 and 1 neighbors. Now adj.get(0) contains [1] and adj.get(1) contains [0].

HashMap-Based Graph (Non-Consecutive Vertices)
Map> graph = new HashMap<>();

void addEdge(int u, int v) {
    graph.computeIfAbsent(u, k -> new ArrayList<>()).add(v);
    graph.computeIfAbsent(v, k -> new ArrayList<>()).add(u);
}
When to use HashMap instead of ArrayList?

ArrayList requires indices 0, 1, 2, 3... but what if your vertices are 5, 100, or 999? That's where HashMap comes in! It maps any key (vertex ID) to its neighbor list, so you're not limited to consecutive numbers. The computeIfAbsent(u, k -> new ArrayList<>()) method means "if u doesn't exist, create an empty list for it, then add v". This approach is also useful when vertices are strings (like city names) or objects (like user IDs).

Example: Cities like "NYC", "LA", "Chicago" can't be used as array indices, but they work perfectly as HashMap keys!

Weighted Graph Adjacency List
List> weightedAdj = new ArrayList<>();
// Each int[] is {neighbor, weight}
weightedAdj.get(u).add(new int[]{v, weight});
Understanding Weighted Graphs

A weight is a cost, distance, or value associated with an edge (like road distance or ticket price). Instead of just storing the neighbor ID, we store int[]{neighbor, weight} - an array with 2 values. The expression new int[]{v, weight} creates a pair containing the destination vertex and the cost to reach it. To read it back, you'd do: int[] edge = weightedAdj.get(u).get(0); int neighbor = edge[0]; int weight = edge[1];

Real-world example: Flight routes! NYC→LA might cost $300, but NYC→Chicago only $150. We need to store these costs along with the connections.

Visual: Adjacency List Representation Preferred for Sparse Graphs
Undirected Graph
0 1 2 3
Adjacency List
0 [1, 2]
1 [0, 2, 3]
2 [0, 1]
3 [1]
Space: O(V + E) - stores only existing edges!

Adjacency Matrix

Matrix Structure & Initialization
class GraphMatrix {
    int V;
    int[][] matrix;
    
    GraphMatrix(int V) {
        this.V = V;
        matrix = new int[V][V];  // All zeros initially
    }
}
What's happening here?

We create a 2D array (table) of size V rows x V columns, where each cell matrix[i][j] represents "Is there an edge from vertex i to vertex j?". A value of 0 means no connection, while 1 means connected (or you can use the actual weight for weighted graphs). Initially all cells are 0 (no edges yet) since Java automatically initializes int arrays to 0.

Think of it as: A spreadsheet where rows and columns are vertices - put a 1 where two vertices are connected!

Adding and Checking Edges
// Add edge (use 1 for unweighted, actual weight for weighted)
void addEdge(int u, int v) {
    matrix[u][v] = 1;
    matrix[v][u] = 1;  // Remove for directed graph
}

// Check if edge exists - O(1) lookup!
boolean hasEdge(int u, int v) {
    return matrix[u][v] != 0;
}
What's happening here?

The addEdge(u, v) method sets matrix[u][v] = 1 to mark "edge exists from u to v". For undirected graphs, we also set matrix[v][u] = 1 since the connection works both ways. The hasEdge(u, v) method simply checks if matrix[u][v] is not 0 - this gives us instant O(1) lookup because accessing an array by index is constant time with no searching needed.

Trade-off: Super fast edge lookup, but uses V² memory even if the graph is sparse (few edges). For 1000 vertices, that's 1 million cells!

Comparison: List vs Matrix
OperationAdjacency ListAdjacency Matrix
SpaceO(V + E)O(V²)
Add EdgeO(1)O(1)
Check EdgeO(degree)O(1)
Best ForSparse graphsDense graphs
Visual: Adjacency Matrix Representation O(1) Edge Lookup
Same Graph
0 1 2 3
Adjacency Matrix
0 1 2 3
0 0 1 1 0
1 1 0 1 1
2 1 1 0 0
3 0 1 0 0
Space: O(V²) - matrix[i][j] = 1 means edge exists between i and j

Practice Questions: Graph Representation

Test your understanding with these hands-on exercises.

Problem: Convert an edge list to an adjacency list representation.

// Input: n = 4, edges = [[0,1], [0,2], [1,2], [1,3]]
// Output: [[1,2], [0,2,3], [0,1], [1]]

public static List> buildAdjList(int n, int[][] edges) {
    // Your code here
}
Show Solution
public static List> buildAdjList(int n, int[][] edges) {
    List> adj = new ArrayList<>();
    for (int i = 0; i < n; i++) {
        adj.add(new ArrayList<>());
    }
    for (int[] edge : edges) {
        adj.get(edge[0]).add(edge[1]);
        adj.get(edge[1]).add(edge[0]);  // Undirected
    }
    return adj;
}

Problem: Convert an adjacency list to an adjacency matrix.

// Input: adj = [[1,2], [0,2], [0,1]]
// Output: [[0,1,1], [1,0,1], [1,1,0]]

public static int[][] listToMatrix(List> adj) {
    // Your code here
}
Show Solution
public static int[][] listToMatrix(List> adj) {
    int n = adj.size();
    int[][] matrix = new int[n][n];
    
    for (int i = 0; i < n; i++) {
        for (int neighbor : adj.get(i)) {
            matrix[i][neighbor] = 1;
        }
    }
    return matrix;
}

Problem: Build a weighted graph using HashMap where vertices can be any integer (not necessarily 0 to n-1).

// Input: edges = [[100,200,5], [200,300,10], [100,300,15]]
// (format: [from, to, weight])
// Output: HashMap with weighted adjacency list

public static Map> buildWeightedGraph(int[][] edges) {
    // Your code here
}
Show Solution
public static Map> buildWeightedGraph(int[][] edges) {
    Map> graph = new HashMap<>();
    
    for (int[] edge : edges) {
        int from = edge[0], to = edge[1], weight = edge[2];
        
        graph.computeIfAbsent(from, k -> new ArrayList<>())
             .add(new int[]{to, weight});
        graph.computeIfAbsent(to, k -> new ArrayList<>())
             .add(new int[]{from, weight});  // Undirected
    }
    return graph;
}

// Access: graph.get(100) returns [[200,5], [300,15]]
03

DFS (Depth-First Search)

What is Depth-First Search (DFS)?

Depth-First Search (DFS) is a graph traversal algorithm that explores as deep as possible along each branch before backtracking. Starting from a source node, DFS goes to a neighbor, then to that neighbor's neighbor, and keeps going deeper until it hits a dead end (a node with no unvisited neighbors). Only then does it backtrack to explore other paths. It uses a Stack data structure - either implicitly through recursion's call stack, or explicitly with a Stack object.

Think of DFS like exploring a maze: you pick a path and keep walking until you hit a wall, then go back to the last intersection and try a different path. This "go deep first" strategy makes DFS perfect for finding paths, detecting cycles, topological sorting, solving puzzles, and exploring all possibilities. Time Complexity: O(V + E) where V = vertices and E = edges.

Uses Stack (LIFO)

Last In, First Out structure. The most recently discovered node is explored first. Recursion automatically uses the call stack, so DFS is naturally recursive!

Go Deep First

When you find a neighbor, immediately explore it before looking at other neighbors. It's like following one complete path in a maze before trying alternatives.

Backtracking

When you hit a dead end (no unvisited neighbors), go back to the previous node and try other paths. This is how DFS eventually explores the entire graph.

Real-World Analogy

Imagine you're in a library looking for a specific book. DFS approach: Pick an aisle, walk to the very end checking each shelf, then backtrack and try the next aisle. You fully explore one path before starting another. This is efficient when the book might be anywhere and you need to search thoroughly!

Recursive DFS (Most Common)
void dfs(int start) {
    boolean[] visited = new boolean[adj.size()];
    dfsHelper(start, visited);
}

void dfsHelper(int node, boolean[] visited) {
    visited[node] = true;
    System.out.print(node + " ");
    
    for (int neighbor : adj.get(node)) {
        if (!visited[neighbor]) {
            dfsHelper(neighbor, visited);
        }
    }
}
Understanding Recursive DFS

The dfs(start) method initializes a visited array to track which nodes we've seen, then calls the helper. In dfsHelper(node, visited), we first mark the current node as visited (so we don't revisit it later), then process it (here we just print, but you might collect data or check conditions). Next, we loop through all neighbors of this node, and for each unvisited neighbor, we recursively call dfsHelper on it.

Why "depth-first"? When we find an unvisited neighbor, we immediately dive into it (go deeper) before checking other neighbors. It's like exploring a maze by always taking the first path until you hit a dead end, then backtracking!

Iterative DFS with Explicit Stack
void dfsIterative(int start) {
    boolean[] visited = new boolean[adj.size()];
    Stack stack = new Stack<>();
    
    stack.push(start);
    
    while (!stack.isEmpty()) {
        int node = stack.pop();
        
        if (!visited[node]) {
            visited[node] = true;
            System.out.print(node + " ");
            
            // Add neighbors in reverse for same order as recursive
            List neighbors = adj.get(node);
            for (int i = neighbors.size() - 1; i >= 0; i--) {
                if (!visited[neighbors.get(i)]) {
                    stack.push(neighbors.get(i));
                }
            }
        }
    }
}
Why use iterative instead of recursive?

Each recursive call uses memory on the "call stack", and very deep graphs can cause StackOverflowError! The solution is to use our own Stack data structure instead of relying on recursion. The algorithm works by pushing the starting node onto the stack, then repeatedly popping a node, marking it visited, processing it, and pushing all its unvisited neighbors onto the stack until the stack is empty. We add neighbors backwards (reverse order) so they come out in the same order as recursive DFS.

When to use: Very deep graphs (like a long chain of 10,000 nodes) or when you need explicit control over the traversal stack.

DFS on 2D Grid (Island Problems)
void dfsGrid(char[][] grid, int r, int c, boolean[][] visited) {
    int rows = grid.length, cols = grid[0].length;
    
    // Boundary and visited check
    if (r < 0 || r >= rows || c < 0 || c >= cols || visited[r][c]) {
        return;
    }
    
    visited[r][c] = true;
    
    // 4 directions: up, down, left, right
    int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
    for (int[] dir : dirs) {
        dfsGrid(grid, r + dir[0], c + dir[1], visited);
    }
}
Grid DFS - A Very Common Pattern!

A 2D grid IS a graph! Each cell is a node, and adjacent cells (up/down/left/right) are its neighbors. The boundary check r < 0 || r >= rows || c < 0 || c >= cols prevents going off the grid. The directions array {{-1,0}, {1,0}, {0,-1}, {0,1}} represents: (-1,0) = up (row decreases), (1,0) = down (row increases), (0,-1) = left (column decreases), and (0,1) = right (column increases). For 8-directional movement including diagonals, add: {-1,-1}, {-1,1}, {1,-1}, {1,1}.

Common problems using this pattern: Number of Islands, Flood Fill, Surrounded Regions, Shortest Path in Binary Matrix.

Visual: DFS Traversal Order Depth-First = Stack
DFS from Node 0
0 1 2 3
Stack Progression
Step 1 Visit 0, push [1, 2] Stack: [2, 1]
Step 2 Visit 1, push [3] Stack: [2, 3]
Step 3 Visit 3, push [2] Stack: [2, 2]
Step 4 Visit 2, done! Stack: []
Key Insight: DFS explores as deep as possible before backtracking - like a maze explorer taking the first path until hitting a dead end!

Practice Questions: DFS Traversal

Test your understanding with these hands-on exercises.

Problem: Given a graph and a starting node, return all nodes reachable from the start using DFS.

// Input: adj = [[1,2], [0,3], [0], [1]], start = 0
// Output: [0, 1, 2, 3] (all nodes reachable from 0)

public static List findReachable(List> adj, int start) {
    // Your code here
}
Show Solution
public static List findReachable(List> adj, int start) {
    List result = new ArrayList<>();
    boolean[] visited = new boolean[adj.size()];
    dfs(start, adj, visited, result);
    return result;
}

private static void dfs(int node, List> adj, 
                        boolean[] visited, List result) {
    visited[node] = true;
    result.add(node);
    for (int neighbor : adj.get(node)) {
        if (!visited[neighbor]) {
            dfs(neighbor, adj, visited, result);
        }
    }
}

Problem: Use DFS to find if a path exists between source and destination. Return the path if it exists.

// Input: adj = [[1,2], [0,3], [0,3], [1,2]], src = 0, dest = 3
// Output: [0, 1, 3] or [0, 2, 3] (any valid path)

public static List findPath(List> adj, int src, int dest) {
    // Your code here
}
Show Solution
public static List findPath(List> adj, int src, int dest) {
    List path = new ArrayList<>();
    boolean[] visited = new boolean[adj.size()];
    if (dfsPath(src, dest, adj, visited, path)) {
        return path;
    }
    return new ArrayList<>();  // No path found
}

private static boolean dfsPath(int node, int dest, List> adj,
                                boolean[] visited, List path) {
    visited[node] = true;
    path.add(node);
    
    if (node == dest) return true;
    
    for (int neighbor : adj.get(node)) {
        if (!visited[neighbor]) {
            if (dfsPath(neighbor, dest, adj, visited, path)) {
                return true;
            }
        }
    }
    path.remove(path.size() - 1);  // Backtrack
    return false;
}

Problem: Find ALL possible paths from source to destination (no repeated vertices).

// Input: adj = [[1,2], [0,2,3], [0,1,3], [1,2]], src = 0, dest = 3
// Output: [[0,1,3], [0,1,2,3], [0,2,3], [0,2,1,3]]

public static List> findAllPaths(List> adj, int src, int dest) {
    // Your code here
}
Show Solution
public static List> findAllPaths(List> adj, int src, int dest) {
    List> result = new ArrayList<>();
    List path = new ArrayList<>();
    boolean[] visited = new boolean[adj.size()];
    
    dfsAllPaths(src, dest, adj, visited, path, result);
    return result;
}

private static void dfsAllPaths(int node, int dest, List> adj,
                                 boolean[] visited, List path,
                                 List> result) {
    visited[node] = true;
    path.add(node);
    
    if (node == dest) {
        result.add(new ArrayList<>(path));  // Found a path!
    } else {
        for (int neighbor : adj.get(node)) {
            if (!visited[neighbor]) {
                dfsAllPaths(neighbor, dest, adj, visited, path, result);
            }
        }
    }
    
    // Backtrack to explore other paths
    path.remove(path.size() - 1);
    visited[node] = false;
}
04

BFS (Breadth-First Search)

What is Breadth-First Search (BFS)?

Breadth-First Search (BFS) is a graph traversal algorithm that explores all neighbors at the current depth level before moving to nodes at the next depth level. Starting from a source node, BFS first visits all nodes 1 step away, then all nodes 2 steps away, and so on - spreading outward like ripples in water. It uses a Queue data structure (FIFO - First In, First Out) to process nodes in the order they were discovered.

BFS is the go-to algorithm for finding the shortest path in unweighted graphs because it explores nodes level-by-level, guaranteeing that the first time you reach any node, you've found the shortest path to it! Think of it like ripples spreading in water when you drop a stone - they expand equally in all directions. Time Complexity: O(V + E) where V = vertices and E = edges.

Uses Queue (FIFO)

First In, First Out structure. Nodes discovered first are explored first. This ensures we process all nodes at distance 1 before any at distance 2, and so on.

Level-by-Level

BFS explores nodes in "waves" - first all neighbors (level 1), then all neighbors' neighbors (level 2), etc. Like counting degrees of separation in social networks!

Shortest Path

In unweighted graphs, BFS guarantees the shortest path! Since it explores level-by-level, the first time you reach the destination is via the minimum number of edges.

Real-World Analogy

Imagine you're looking for a friend at a party. BFS approach: First ask everyone in your immediate circle (distance 1), then ask everyone they know (distance 2), etc. You spread your search equally in all directions rather than following one chain of people. This is perfect when you want to find someone with the fewest introductions!

Basic BFS Implementation
void bfs(int start) {
    boolean[] visited = new boolean[adj.size()];
    Queue queue = new LinkedList<>();
    
    visited[start] = true;
    queue.offer(start);
    
    while (!queue.isEmpty()) {
        int node = queue.poll();
        System.out.print(node + " ");
        
        for (int neighbor : adj.get(node)) {
            if (!visited[neighbor]) {
                visited[neighbor] = true;  // Mark BEFORE adding to queue!
                queue.offer(neighbor);
            }
        }
    }
}
Understanding BFS

First, we create a visited array and a queue, mark the start node as visited, and add it to the queue. In the main loop, while the queue is not empty, we poll (remove from front) the next node and process it. Then we explore all neighbors of the current node - for each unvisited neighbor, we mark it visited AND add it to the queue.

Common Mistake: Mark visited WHEN ADDING to queue, not when polling! Otherwise, the same node might be added multiple times by different neighbors, wasting time and memory.
BFS with Level Tracking
List> bfsLevels(int start) {
    List> levels = new ArrayList<>();
    boolean[] visited = new boolean[adj.size()];
    Queue queue = new LinkedList<>();
    
    visited[start] = true;
    queue.offer(start);
    
    while (!queue.isEmpty()) {
        int size = queue.size();  // Nodes at current level
        List level = new ArrayList<>();
        
        for (int i = 0; i < size; i++) {
            int node = queue.poll();
            level.add(node);
            
            for (int neighbor : adj.get(node)) {
                if (!visited[neighbor]) {
                    visited[neighbor] = true;
                    queue.offer(neighbor);
                }
            }
        }
        levels.add(level);
    }
    return levels;
}
The "Level-by-Level" Trick

The key insight is that int size = queue.size() captures how many nodes are at the CURRENT level. This works because at the start of each iteration, the queue contains ONLY nodes at the current distance from the source. The inner for-loop then processes exactly 'size' nodes (the current level) and adds their neighbors (which form the next level). After the loop completes, the queue contains only next-level nodes, ready for the next iteration.

Use cases: Tree level-order traversal, finding shortest distance, processing graph in layers (like social network "degrees of separation").

Shortest Path (Unweighted Graph)
int shortestPath(int start, int end) {
    if (start == end) return 0;
    
    boolean[] visited = new boolean[adj.size()];
    Queue queue = new LinkedList<>();
    
    visited[start] = true;
    queue.offer(start);
    int distance = 0;
    
    while (!queue.isEmpty()) {
        int size = queue.size();
        distance++;  // Moving to next level = +1 distance
        
        for (int i = 0; i < size; i++) {
            int node = queue.poll();
            
            for (int neighbor : adj.get(node)) {
                if (neighbor == end) return distance;  // Found!
                
                if (!visited[neighbor]) {
                    visited[neighbor] = true;
                    queue.offer(neighbor);
                }
            }
        }
    }
    return -1;  // No path exists
}
Why BFS Finds Shortest Path

BFS explores level-by-level, visiting all nodes at distance 1 first, then all at distance 2, and so on. This means the first time we encounter the target node, we've found it via the shortest possible path! We track distance by incrementing it after processing each complete level. As soon as we find the 'end' node, we return immediately since we've found the shortest path. If the queue empties without finding the target, no path exists and we return -1.

Important: This only works for unweighted graphs (all edges cost 1). For weighted graphs, use Dijkstra's algorithm instead!
Visual: BFS Traversal (Level-by-Level) Breadth-First = Queue
BFS from Node 0
Level 0 Level 1 Level 2 0 1 2 3
Queue Progression
Level 0 Process 0 Queue: [0]
Level 1 Process 1, 2 Queue: [1, 2]
Level 2 Process 3 Queue: [3]
Done! All nodes visited Queue: []
Key Insight: BFS explores like ripples in water - all nodes at distance 1, then distance 2, etc. This guarantees shortest path in unweighted graphs!

Practice Questions: BFS Traversal

Test your understanding with these hands-on exercises.

Problem: Return nodes grouped by their level (distance from start).

// Input: adj = [[1,2], [0,3], [0,3], [1,2]], start = 0
// Output: [[0], [1,2], [3]] (level 0, level 1, level 2)

public static List> bfsLevels(List> adj, int start) {
    // Your code here
}
Show Solution
public static List> bfsLevels(List> adj, int start) {
    List> levels = new ArrayList<>();
    boolean[] visited = new boolean[adj.size()];
    Queue queue = new LinkedList<>();
    
    visited[start] = true;
    queue.offer(start);
    
    while (!queue.isEmpty()) {
        int size = queue.size();
        List level = new ArrayList<>();
        
        for (int i = 0; i < size; i++) {
            int node = queue.poll();
            level.add(node);
            
            for (int neighbor : adj.get(node)) {
                if (!visited[neighbor]) {
                    visited[neighbor] = true;
                    queue.offer(neighbor);
                }
            }
        }
        levels.add(level);
    }
    return levels;
}

Problem: Find the shortest path between two nodes and return the actual path (not just distance).

// Input: adj = [[1,2], [0,3], [0,3], [1,2]], src = 0, dest = 3
// Output: [0, 1, 3] (shortest path)

public static List shortestPath(List> adj, int src, int dest) {
    // Your code here
}
Show Solution
public static List shortestPath(List> adj, int src, int dest) {
    int n = adj.size();
    boolean[] visited = new boolean[n];
    int[] parent = new int[n];
    Arrays.fill(parent, -1);
    
    Queue queue = new LinkedList<>();
    visited[src] = true;
    queue.offer(src);
    
    while (!queue.isEmpty()) {
        int node = queue.poll();
        
        if (node == dest) break;
        
        for (int neighbor : adj.get(node)) {
            if (!visited[neighbor]) {
                visited[neighbor] = true;
                parent[neighbor] = node;
                queue.offer(neighbor);
            }
        }
    }
    
    // Reconstruct path
    List path = new ArrayList<>();
    if (!visited[dest]) return path;  // No path
    
    for (int node = dest; node != -1; node = parent[node]) {
        path.add(0, node);
    }
    return path;
}

Problem: Transform beginWord to endWord, changing one letter at a time. Each intermediate word must be in wordList. Return minimum transformations.

// Input: beginWord = "hit", endWord = "cog", 
//        wordList = ["hot","dot","dog","lot","log","cog"]
// Output: 5 (hit -> hot -> dot -> dog -> cog)

public static int ladderLength(String beginWord, String endWord, List wordList) {
    // Your code here - use BFS!
}
Show Solution
public static int ladderLength(String beginWord, String endWord, List wordList) {
    Set wordSet = new HashSet<>(wordList);
    if (!wordSet.contains(endWord)) return 0;
    
    Queue queue = new LinkedList<>();
    queue.offer(beginWord);
    int level = 1;
    
    while (!queue.isEmpty()) {
        int size = queue.size();
        
        for (int i = 0; i < size; i++) {
            String word = queue.poll();
            char[] chars = word.toCharArray();
            
            for (int j = 0; j < chars.length; j++) {
                char original = chars[j];
                
                for (char c = 'a'; c <= 'z'; c++) {
                    if (c == original) continue;
                    chars[j] = c;
                    String newWord = new String(chars);
                    
                    if (newWord.equals(endWord)) return level + 1;
                    
                    if (wordSet.contains(newWord)) {
                        queue.offer(newWord);
                        wordSet.remove(newWord);  // Mark visited
                    }
                }
                chars[j] = original;
            }
        }
        level++;
    }
    return 0;
}
05

Connected Components

What is a Connected Component?

A Connected Component is a maximal group of vertices in an undirected graph where there exists a path between every pair of vertices within that group. Think of it as an "island" of connected nodes - you can travel from any node to any other node within the same component, but you cannot reach nodes in other components without adding new edges. A graph can have one component (fully connected) or multiple components (disconnected graph).

Finding connected components is a fundamental graph operation used everywhere: social network analysis (finding friend groups that don't overlap), image processing (identifying separate objects in an image), network reliability (detecting isolated segments), and recommendation systems (grouping similar items). The algorithm is simple: keep running DFS/BFS from unvisited nodes until everyone is visited!

Single Component

All nodes are reachable from any other node - the entire graph is connected. Think: a fully connected social network where everyone knows everyone through some chain of friends. One DFS from any node visits everyone!

Multiple Components

Graph has isolated groups with no path between them. Think: separate friend circles that don't overlap, or islands in an ocean. Each DFS from a new unvisited node discovers a new component!

Finding Algorithm

Use DFS/BFS from each unvisited node. Each complete traversal discovers one component. Count how many times you start a new traversal = number of components!

Count Connected Components
int countComponents(int n, int[][] edges) {
    // Step 1: Build adjacency list
    List> adj = new ArrayList<>();
    for (int i = 0; i < n; i++) {
        adj.add(new ArrayList<>());
    }
    for (int[] edge : edges) {
        adj.get(edge[0]).add(edge[1]);
        adj.get(edge[1]).add(edge[0]);
    }
    
    // Step 2: DFS from each unvisited node
    boolean[] visited = new boolean[n];
    int count = 0;
    
    for (int i = 0; i < n; i++) {
        if (!visited[i]) {
            dfs(i, adj, visited);  // Explore entire component
            count++;  // Found a new component!
        }
    }
    
    return count;
}
The Connected Components Algorithm

First, we build the graph by converting the edge list to adjacency list format. Then we loop through all vertices from 0 to n-1. Whenever we find an unvisited vertex, we've discovered a NEW component! We run DFS from this vertex, which marks ALL nodes in this component as visited, then increment our count. We continue the loop, skipping already-visited nodes and starting DFS only from unvisited ones.

Real-world analogy: Counting friend groups! Start with any person, mark everyone they can reach through friendships - that's one group. Find the next unvisited person and repeat.

DFS Helper for Components
void dfs(int node, List> adj, boolean[] visited) {
    visited[node] = true;
    for (int neighbor : adj.get(node)) {
        if (!visited[neighbor]) {
            dfs(neighbor, adj, visited);
        }
    }
}
Simple but Powerful

This is the simplest form of DFS - just mark and explore. We mark the current node as visited with visited[node] = true, then for each unvisited neighbor, we recursively call DFS. When this function returns, ALL nodes reachable from 'node' are marked as visited. It's like a flood - once you pour water on one cell, it spreads to all connected cells.

Number of Islands (Classic Grid Problem)
int numIslands(char[][] grid) {
    if (grid == null || grid.length == 0) return 0;
    
    int rows = grid.length, cols = grid[0].length;
    int count = 0;
    
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            if (grid[i][j] == '1') {
                dfsIsland(grid, i, j);  // Sink the island
                count++;  // Found a new island!
            }
        }
    }
    return count;
}
Number of Islands - Interview Favorite!

In this problem, the grid contains '1' for land and '0' for water. An island is a group of '1's connected horizontally or vertically (not diagonally). The algorithm works by scanning the grid cell by cell using nested loops. When we find a '1' (land), we've discovered a new island, so we run DFS to "sink" the entire island by changing all connected '1's to '0's, then increment our island count. As we continue scanning, the sunken island won't be counted again because all its cells are now '0'.

Why "sink"? By changing '1' to '0', we mark it as visited without needing a separate visited array!

Island DFS (4-Directional)
void dfsIsland(char[][] grid, int r, int c) {
    // Boundary check + water check
    if (r < 0 || r >= grid.length || c < 0 || c >= grid[0].length 
        || grid[r][c] != '1') {
        return;
    }
    
    grid[r][c] = '0';  // Mark as visited (sink the land)
    
    // Explore all 4 directions
    dfsIsland(grid, r + 1, c);  // down
    dfsIsland(grid, r - 1, c);  // up
    dfsIsland(grid, r, c + 1);  // right
    dfsIsland(grid, r, c - 1);  // left
}
The "Flood Fill" Pattern

The base case handles three stopping conditions: r < 0 || r >= grid.length checks for vertical out-of-bounds, c < 0 || c >= grid[0].length checks for horizontal out-of-bounds, and grid[r][c] != '1' stops at water or already visited cells. We mark cells as visited by setting grid[r][c] = '0' (sinking the land), then explore all 4 directions by recursively calling DFS on up, down, left, and right cells. This approach saves O(rows x cols) space by modifying the input grid instead of using a separate visited array.

Note: If you can't modify the input, use visited[][] array or change '1' to '2' and restore later.

Practice Questions: Connected Components

Test your understanding with these hands-on exercises.

Problem: Given n nodes and edges, count the number of connected components.

// Input: n = 5, edges = [[0,1], [1,2], [3,4]]
// Output: 2 (components: {0,1,2} and {3,4})

public static int countComponents(int n, int[][] edges) {
    // Your code here
}
Show Solution
public static int countComponents(int n, int[][] edges) {
    List> adj = new ArrayList<>();
    for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
    
    for (int[] edge : edges) {
        adj.get(edge[0]).add(edge[1]);
        adj.get(edge[1]).add(edge[0]);
    }
    
    boolean[] visited = new boolean[n];
    int count = 0;
    
    for (int i = 0; i < n; i++) {
        if (!visited[i]) {
            dfs(i, adj, visited);
            count++;
        }
    }
    return count;
}

private static void dfs(int node, List> adj, boolean[] visited) {
    visited[node] = true;
    for (int neighbor : adj.get(node)) {
        if (!visited[neighbor]) dfs(neighbor, adj, visited);
    }
}

Problem: Count the number of islands in a 2D grid ('1' = land, '0' = water).

// Input: grid = [['1','1','0','0'],
//                ['1','1','0','0'],
//                ['0','0','1','0'],
//                ['0','0','0','1']]
// Output: 3

public static int numIslands(char[][] grid) {
    // Your code here
}
Show Solution
public static int numIslands(char[][] grid) {
    if (grid == null || grid.length == 0) return 0;
    
    int count = 0;
    for (int i = 0; i < grid.length; i++) {
        for (int j = 0; j < grid[0].length; j++) {
            if (grid[i][j] == '1') {
                sinkIsland(grid, i, j);
                count++;
            }
        }
    }
    return count;
}

private static void sinkIsland(char[][] grid, int r, int c) {
    if (r < 0 || r >= grid.length || c < 0 || c >= grid[0].length 
        || grid[r][c] != '1') return;
    
    grid[r][c] = '0';  // Sink the land
    sinkIsland(grid, r + 1, c);
    sinkIsland(grid, r - 1, c);
    sinkIsland(grid, r, c + 1);
    sinkIsland(grid, r, c - 1);
}

Problem: Find the maximum area of an island in a 2D grid.

// Input: grid = [[0,0,1,0,0],
//                [0,1,1,1,0],
//                [0,0,1,0,0]]
// Output: 5 (the cross-shaped island)

public static int maxAreaOfIsland(int[][] grid) {
    // Your code here
}
Show Solution
public static int maxAreaOfIsland(int[][] grid) {
    int maxArea = 0;
    for (int i = 0; i < grid.length; i++) {
        for (int j = 0; j < grid[0].length; j++) {
            if (grid[i][j] == 1) {
                maxArea = Math.max(maxArea, calculateArea(grid, i, j));
            }
        }
    }
    return maxArea;
}

private static int calculateArea(int[][] grid, int r, int c) {
    if (r < 0 || r >= grid.length || c < 0 || c >= grid[0].length 
        || grid[r][c] != 1) return 0;
    
    grid[r][c] = 0;  // Mark visited
    
    return 1 + calculateArea(grid, r + 1, c)
             + calculateArea(grid, r - 1, c)
             + calculateArea(grid, r, c + 1)
             + calculateArea(grid, r, c - 1);
}
06

Cycle Detection

What is a Cycle in a Graph?

A cycle is a path in a graph that starts and ends at the same vertex, passing through at least one edge without repeating any edge. In simple terms, it's a "closed loop" where you can start at a node, travel through a series of edges, and return to where you started. Detecting cycles is crucial because many algorithms (like finding shortest paths or topological sorting) require graphs without cycles, and cycles can indicate problems like deadlocks or circular dependencies.

Cycle detection is essential across many domains: validating data structures (trees must be acyclic), deadlock detection in operating systems (processes waiting in a circle), dependency resolution (detecting circular imports/dependencies), course prerequisite validation (can't have A require B and B require A), and detecting infinite loops in workflows. The detection approach differs significantly between undirected and directed graphs!

Why Different Approaches?

In undirected graphs, every edge A-B is bidirectional (A→B and B→A). When traversing from A to B, we'll see A in B's neighbors - but that's NOT a cycle! We need to track the parent to distinguish between "came from here" vs "found a back edge." In directed graphs, edges are one-way, so we use a three-color system (White/Gray/Black) to detect if we're visiting a node that's still being processed.

Undirected Graph Cycles

Track the parent node during DFS. When we find a visited neighbor that is NOT our parent, we've found a cycle! The parent check prevents false positives from the bidirectional nature of undirected edges.

Example: A-B-C-A forms a cycle. Traversing A→B→C, when at C we see A is visited and A ≠ parent (B). Cycle detected!

Directed Graph Cycles

Use three-color marking: WHITE (unvisited), GRAY (currently in DFS stack), BLACK (completely done). If during exploration we encounter a GRAY node, we've found a back edge - that's a cycle!

Example: A→B→C→A. Processing A (gray)→B (gray)→C (gray). C sees A is GRAY = cycle!

Undirected Graph - Main Function
boolean hasCycleUndirected(int n, List> adj) {
    boolean[] visited = new boolean[n];
    
    for (int i = 0; i < n; i++) {
        if (!visited[i]) {
            if (dfsCycle(i, -1, adj, visited)) {
                return true;  // Cycle found!
            }
        }
    }
    return false;  // No cycle
}
The Setup for Cycle Detection

We need to check all vertices because the graph might be disconnected, meaning a cycle could exist in any component. The parent parameter starts as -1 because the starting node has no parent in the DFS tree. We use an early return strategy: as soon as we find a cycle, we return true immediately without checking the rest. If the loop completes without finding any cycles, we've checked every component and can safely return false.

Undirected Graph - DFS with Parent Tracking
boolean dfsCycle(int node, int parent, List> adj, boolean[] visited) {
    visited[node] = true;
    
    for (int neighbor : adj.get(node)) {
        if (!visited[neighbor]) {
            if (dfsCycle(neighbor, node, adj, visited)) {
                return true;
            }
        } else if (neighbor != parent) {
            // Visited neighbor that's NOT our parent = CYCLE!
            return true;
        }
    }
    return false;
}
Why Track the Parent?

In undirected graphs, if node A connects to node B, then B also connects back to A. This creates a false positive risk: when we're at B (having come from A), we'll see A as a "visited neighbor" - but that's not actually a cycle! The solution is to track which node we came from (the parent) and ignore it when checking for cycles. The cycle detection logic is: if a neighbor is unvisited, we explore it normally with DFS. If a neighbor is visited AND it's our parent, we ignore it since that's just the edge we came from. But if a neighbor is visited AND it's NOT our parent, we've found a real cycle!

Example: A-B-C-A is a cycle. When at C, we see A is visited and A is not our parent (B is). Cycle detected!

Directed Graph - Three-Color Algorithm
// WHITE = 0 (unvisited), GRAY = 1 (in progress), BLACK = 2 (done)
boolean hasCycleDirected(int n, List> adj) {
    int[] color = new int[n];  // All WHITE (0) initially
    
    for (int i = 0; i < n; i++) {
        if (color[i] == 0) {  // WHITE - not visited
            if (dfsCycleDirected(i, adj, color)) {
                return true;
            }
        }
    }
    return false;
}
Why Three Colors Instead of Two?

The problem with just visited/unvisited is that in directed graphs, visiting a node twice doesn't always indicate a cycle! For example, consider A->B->C and A->C. When exploring A->B->C, then A->C, we see C is already visited, but that's NOT a cycle! The solution is to use three states: WHITE (0) means we haven't visited the node yet, GRAY (1) means the node is currently being explored and is in our current DFS path, and BLACK (2) means the node is completely finished with all descendants visited. A cycle exists only when we find a GRAY node, which means we've encountered a node that's already in our current path!

Directed Graph - Color-Based DFS
boolean dfsCycleDirected(int node, List> adj, int[] color) {
    color[node] = 1;  // GRAY - currently being processed
    
    for (int neighbor : adj.get(node)) {
        if (color[neighbor] == 1) {
            // Back edge to GRAY node = CYCLE!
            return true;
        }
        if (color[neighbor] == 0) {  // WHITE - unvisited
            if (dfsCycleDirected(neighbor, adj, color)) {
                return true;
            }
        }
        // BLACK nodes are already done - skip them
    }
    
    color[node] = 2;  // BLACK - completely processed
    return false;
}
The Three-Color Algorithm Step by Step

When we enter a node, we color it GRAY to indicate we're currently exploring it. Then we check each neighbor: if a neighbor is GRAY, we've found a node already in our current path, which means we have a cycle! If a neighbor is WHITE (unvisited), we explore it recursively. If a neighbor is BLACK (already fully explored), we skip it since it's safe and there's no cycle through this path. Once all neighbors are processed, we color the current node BLACK to mark it as fully explored, then return false to indicate no cycle was found through this node.

Think of it as: GRAY = "I'm still on this path". Finding another GRAY node means we've circled back to where we came from!

Understanding the Three-Color Algorithm
  1. WHITE (0): Node not yet visited
  2. GRAY (1): Node is currently in the DFS stack (being processed)
  3. BLACK (2): Node completely processed, all descendants visited

Finding a GRAY node while exploring means we've found a back edge → Cycle detected!

Practice Questions: Cycle Detection

Test your understanding with these hands-on exercises.

Problem: Detect if an undirected graph contains a cycle using DFS with parent tracking.

// Input: n = 4, edges = [[0,1], [1,2], [2,0], [2,3]]
// Output: true (cycle: 0 -> 1 -> 2 -> 0)

public static boolean hasCycle(int n, int[][] edges) {
    // Your code here
}
Show Solution
public static boolean hasCycle(int n, int[][] edges) {
    List> adj = new ArrayList<>();
    for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
    
    for (int[] edge : edges) {
        adj.get(edge[0]).add(edge[1]);
        adj.get(edge[1]).add(edge[0]);
    }
    
    boolean[] visited = new boolean[n];
    
    for (int i = 0; i < n; i++) {
        if (!visited[i]) {
            if (dfsCycle(i, -1, adj, visited)) return true;
        }
    }
    return false;
}

private static boolean dfsCycle(int node, int parent, 
                                 List> adj, boolean[] visited) {
    visited[node] = true;
    
    for (int neighbor : adj.get(node)) {
        if (!visited[neighbor]) {
            if (dfsCycle(neighbor, node, adj, visited)) return true;
        } else if (neighbor != parent) {
            return true;  // Cycle found!
        }
    }
    return false;
}

Problem: Detect if a directed graph contains a cycle using the three-color algorithm.

// Input: n = 4, edges = [[0,1], [1,2], [2,0], [2,3]]
// Output: true (cycle: 0 -> 1 -> 2 -> 0)

public static boolean hasCycleDirected(int n, int[][] edges) {
    // Your code here - use WHITE/GRAY/BLACK coloring
}
Show Solution
public static boolean hasCycleDirected(int n, int[][] edges) {
    List> adj = new ArrayList<>();
    for (int i = 0; i < n; i++) adj.add(new ArrayList<>());
    
    for (int[] edge : edges) {
        adj.get(edge[0]).add(edge[1]);  // Directed: only one direction
    }
    
    int[] color = new int[n];  // 0=WHITE, 1=GRAY, 2=BLACK
    
    for (int i = 0; i < n; i++) {
        if (color[i] == 0) {
            if (dfsCycleDirected(i, adj, color)) return true;
        }
    }
    return false;
}

private static boolean dfsCycleDirected(int node, List> adj, int[] color) {
    color[node] = 1;  // GRAY - in progress
    
    for (int neighbor : adj.get(node)) {
        if (color[neighbor] == 1) return true;  // Back edge = cycle!
        if (color[neighbor] == 0) {
            if (dfsCycleDirected(neighbor, adj, color)) return true;
        }
    }
    
    color[node] = 2;  // BLACK - done
    return false;
}

Problem: Given numCourses and prerequisites, determine if you can finish all courses. A prerequisite [a, b] means you must take course b before course a.

// Input: numCourses = 4, prerequisites = [[1,0], [2,1], [3,2], [1,3]]
// Output: false (cycle: 1 -> 2 -> 3 -> 1)

public static boolean canFinish(int numCourses, int[][] prerequisites) {
    // Your code here - detect cycle in course dependency graph
}
Show Solution
public static boolean canFinish(int numCourses, int[][] prerequisites) {
    List> adj = new ArrayList<>();
    for (int i = 0; i < numCourses; i++) adj.add(new ArrayList<>());
    
    // Build graph: b -> a (must take b before a)
    for (int[] prereq : prerequisites) {
        adj.get(prereq[1]).add(prereq[0]);
    }
    
    int[] color = new int[numCourses];
    
    for (int i = 0; i < numCourses; i++) {
        if (color[i] == 0) {
            if (hasCycle(i, adj, color)) return false;
        }
    }
    return true;  // No cycle = can finish all courses
}

private static boolean hasCycle(int node, List> adj, int[] color) {
    color[node] = 1;  // GRAY
    
    for (int neighbor : adj.get(node)) {
        if (color[neighbor] == 1) return true;
        if (color[neighbor] == 0 && hasCycle(neighbor, adj, color)) return true;
    }
    
    color[node] = 2;  // BLACK
    return false;
}

Interactive Demos

Experiment with these interactive visualizations to deepen your understanding of graph traversals and representations.

Interactive: DFS vs BFS Traversal Comparison
DFS (Stack)
Click "Start Traversal" to begin
BFS (Queue)
Click "Start Traversal" to begin
Graph Structure: 0-1, 0-2, 1-3, 1-4, 2-4. Watch how DFS goes deep first while BFS explores level by level!
Visual: Adjacency List vs Matrix Comparison Step-by-Step
Step 1: Sample Undirected Graph
0 1 2 3 4
Graph Properties:
  • Vertices (V): 5 nodes (0, 1, 2, 3, 4)
  • Edges (E): 5 edges (0-1, 0-2, 0-3, 1-4, 2-3)
  • Type: Undirected, Unweighted
Step 2: Adding Edges One by One
1 addEdge(0, 1)
2 addEdge(0, 2)
3 addEdge(0, 3)
4 addEdge(1, 4)
5 addEdge(2, 3)
Adjacency List
0 [1, 2, 3]
1 [0, 4]
2 [0, 3]
3 [0, 2]
4 [1]
Space: O(V + E) = O(10)
Add Edge: O(1)
Check Edge: O(degree)
Adjacency Matrix
0 1 2 3 4
0 0 1 1 1 0
1 1 0 0 0 1
2 1 0 0 1 0
3 1 0 1 0 0
4 0 1 0 0 0
Space: O(V²) = O(25)
Add Edge: O(1)
Check Edge: O(1)
Use Adjacency List when:
  • Graph is sparse (few edges)
  • Need to iterate over neighbors
  • Memory is a concern
Use Adjacency Matrix when:
  • Graph is dense (many edges)
  • Need O(1) edge existence check
  • Vertices are numbered 0 to V-1

Key Takeaways

Adjacency List is Preferred

Use adjacency list for most graphs - O(V + E) space, efficient for sparse graphs. Use matrix only when you need O(1) edge lookup.

DFS Uses Stack/Recursion

Depth-first explores as deep as possible before backtracking. Perfect for cycle detection, connected components, and path finding.

BFS Uses Queue

Breadth-first explores level-by-level like ripples in water. Guarantees shortest path in unweighted graphs - essential for distance problems!

Cycle Detection Differs

Undirected graphs: track parent to avoid false positives. Directed graphs: use three colors (WHITE/GRAY/BLACK) to detect back edges.

Grids are Graphs Too

2D grids are graphs where cells are nodes and adjacent cells are neighbors. Use 4-directional arrays for up/down/left/right movement.

Time Complexity: O(V + E)

Both DFS and BFS visit every vertex and edge once. This linear time makes them efficient for exploring entire graphs.

Knowledge Check

Question 1 of 6

Which data structure does BFS use?

Question 2 of 6

What is the space complexity of adjacency list for a graph with V vertices and E edges?

Question 3 of 6

Which algorithm finds shortest path in unweighted graph?

Question 4 of 6

In cycle detection for directed graphs, what does a GRAY node indicate?

Question 5 of 6

What is the time complexity of BFS and DFS traversals?

Question 6 of 6

When is adjacency matrix preferred over adjacency list?