Graphs: Adjacency List, Matrix, and Core Algorithms

Master graph representations (adjacency list and matrix), BFS, DFS, and when to use each representation.

published: reading time: 25 min read author: GeekWorkBench

Graphs: Adjacency List, Matrix, and Core Algorithms

Graphs model pairwise relationships between objects—a vertex (or node) represents an entity, and an edge represents a connection. Social networks, road maps, and dependency systems all decompose naturally into graphs. Understanding graph representations and traversal algorithms matters for pathfinding, network analysis, and scheduling problems.

Introduction

Graphs model pairwise relationships between entities—a vertex represents an object, and an edge represents a connection between two objects. This abstraction fits natural problems: social networks (people as vertices, friendships as edges), road systems (locations as vertices, roads as edges), dependency systems (tasks as vertices, prerequisites as edges). Understanding graph representations and traversal algorithms is essential for solving pathfinding, network analysis, and scheduling problems.

The two primary graph representations—adjacency list and adjacency matrix—make opposite trade-offs. Adjacency lists store each vertex’s neighbors in a list, using O(V + E) space and enabling O(degree) neighbor iteration. Adjacency matrices store a V×V grid where each cell indicates edge presence, using O(V²) space but enabling O(1) edge existence checks. Choosing the right representation depends on graph density, required operations, and size constraints.

In this guide, you’ll implement both representations in Python, master BFS and DFS traversals with their O(V + E) complexity, and understand when to use each traversal pattern. BFS finds shortest paths in unweighted graphs and explores level-by-level; DFS explores deeply before backtracking and enables cycle detection and topological sorting.

Graph Representations

from collections import defaultdict, deque

# Adjacency List: Dictionary of lists
# Best for: Sparse graphs, iterating neighbors
class GraphList:
    def __init__(self, directed=False):
        self.graph = defaultdict(list)
        self.directed = directed

    def add_edge(self, u, v, weight=None):
        if weight is not None:
            self.graph[u].append((v, weight))
        else:
            self.graph[u].append(v)
        if not self.directed:
            self.graph[v].append(u if weight is None else (u, weight))

    def neighbors(self, u):
        return self.graph[u]

    def vertices(self):
        return list(self.graph.keys())


# Adjacency Matrix: 2D array
# Best for: Dense graphs, O(1) edge lookup, small graphs
class GraphMatrix:
    def __init__(self, n, directed=False):
        self.n = n
        self.directed = directed
        self.matrix = [[float('inf')] * n for _ in range(n)]
        for i in range(n):
            self.matrix[i][i] = 0  # Self-loops

    def add_edge(self, u, v, weight=1):
        self.matrix[u][v] = weight
        if not self.directed:
            self.matrix[v][u] = weight

    def has_edge(self, u, v):
        return self.matrix[u][v] != float('inf')

    def neighbors(self, u):
        return [i for i in range(self.n) if self.matrix[u][i] != float('inf')]


# Edge List: List of tuples
# Best for: Kruskal's MST, sorting edges by weight
class GraphEdges:
    def __init__(self, n, directed=False):
        self.n = n
        self.directed = directed
        self.edges = []

    def add_edge(self, u, v, weight=1):
        self.edges.append((weight, u, v))
        if not self.directed:
            self.edges.append((weight, v, u))

    def get_edges(self, sorted_by_weight=False):
        if sorted_by_weight:
            return sorted(self.edges)
        return self.edges
def bfs(graph, start):
    """
    BFS traversal - explores level by level.
    O(V + E) time, O(V) space.

    Returns:
        visited order, distance from start, parent for path reconstruction
    """
    visited = set([start])
    queue = deque([start])
    parent = {start: None}
    distance = {start: 0}
    order = []

    while queue:
        vertex = queue.popleft()
        order.append(vertex)

        for neighbor in graph.neighbors(vertex):
            if neighbor not in visited:
                visited.add(neighbor)
                parent[neighbor] = vertex
                distance[neighbor] = distance[vertex] + 1
                queue.append(neighbor)

    return order, distance, parent


def bfs_shortest_path(graph, start, end):
    """BFS guarantees shortest path in unweighted graph."""
    if start == end:
        return [start]

    _, distance, parent = bfs(graph, start)

    if end not in parent:
        return None  # Not reachable

    # Reconstruct path
    path = []
    current = end
    while current is not None:
        path.append(current)
        current = parent[current]

    return path[::-1]


def is_bipartite(graph):
    """Check if graph is bipartite using BFS coloring."""
    color = {}
    for start in graph.vertices():
        if start in color:
            continue

        queue = deque([start])
        color[start] = 0

        while queue:
            u = queue.popleft()
            for v in graph.neighbors(u):
                if v not in color:
                    color[v] = 1 - color[u]
                    queue.append(v)
                elif color[v] == color[u]:
                    return False

    return True
def dfs_recursive(graph, start, visited=None):
    """Recursive DFS - O(V + E) time, O(V) space for visited + call stack."""
    if visited is None:
        visited = set()

    visited.add(start)
    print(start)  # Process node

    for neighbor in graph.neighbors(start):
        if neighbor not in visited:
            dfs_recursive(graph, neighbor, visited)

    return visited


def dfs_iterative(graph, start):
    """Iterative DFS using explicit stack - avoids recursion depth limit."""
    visited = set()
    stack = [start]
    order = []

    while stack:
        vertex = stack.pop()

        if vertex not in visited:
            visited.add(vertex)
            order.append(vertex)

            # Add neighbors in reverse order to maintain left-to-right processing
            neighbors = graph.neighbors(vertex)
            stack.extend(reversed(neighbors))

    return order


# DFS with entry/exit times for cycle detection and topological sort
def dfs_with_times(graph):
    """
    DFS that records discovery and finish times.
    Useful for cycle detection, topological sort, and articulation points.
    """
    visited = set()
    discovered = {}
    finished = {}
    time = [0]
    parent = {}

    def dfs(u):
        visited.add(u)
        discovered[u] = time[0]
        time[0] += 1

        for v in graph.neighbors(u):
            if v not in visited:
                parent[v] = u
                dfs(v)

        finished[u] = time[0]
        time[0] += 1

    for vertex in graph.vertices():
        if vertex not in visited:
            parent[vertex] = None
            dfs(vertex)

    return discovered, finished, parent


def has_cycle(graph):
    """Detect cycle using DFS (works for directed and undirected)."""
    visited = set()
    rec_stack = set()  # For directed cycle detection

    def dfs(u):
        visited.add(u)
        rec_stack.add(u)

        for v in graph.neighbors(u):
            if v not in visited:
                if dfs(v):
                    return True
            elif v in rec_stack:  # Back edge = cycle
                return True

        rec_stack.remove(u)
        return False

    for vertex in graph.vertices():
        if vertex not in visited:
            if dfs(vertex):
                return True

    return False

When to Use List vs Matrix

AspectAdjacency ListAdjacency Matrix
SpaceO(V + E)O(V²)
Add edgeO(1)O(1)
Check edgeO(degree)O(1)
Iterate neighborsO(degree)O(V)
Dense graphsWastefulEfficient
Sparse graphsEfficientWasteful

BFS vs DFS Exploration Order

graph TD
    subgraph Graph["Sample Graph: A → B, A → C, B → D, B → E, C → F"]
        A["A"] --> B["B"]
        A --> C["C"]
        B --> D["D"]
        B --> E["E"]
        C --> F["F"]
    end

    subgraph BFS_Order["BFS Order (Queue-based)"]
        B1["Level 0: A"]
        B2["Level 1: B, C"]
        B3["Level 2: D, E, F"]
    end

    subgraph DFS_Order["DFS Order (Stack-based)"]
        D1["Path 1: A → B → D"]
        D2["Path 2: A → B → E"]
        D3["Path 3: A → C → F"]
    end

BFS visits nodes level by level: A, then B and C, then D, E, F. BFS finds shortest path in unweighted graphs. DFS explores as deep as possible before backtracking: A, B, D, backtrack to B, E, backtrack to A, C, F.

Graph Traversal Applications

ProblemAlgorithmUse Case
Shortest path (unweighted)BFSSocial network degrees, maze
Connected componentsBFS/DFSNetwork analysis, island counting
Cycle detectionDFSDependency validation, deadlock detection
Topological sortDFSBuild systems, course scheduling
Path existenceBFS/DFSReachability, firewall rules

Production Failure Scenarios and Mitigations

Graph implementations tend to break in specific, predictable ways once you put them under real production load. These are the failure modes I run into most often.

Infinite BFS/DFS from unvisited cycles in disconnected components

If your graph has multiple disconnected components and your traversal does not track visited nodes properly across them, you can loop forever. The problem is that a naive implementation clears the visited set when starting a new component. If that component has an edge pointing back into an already-visited component, your algorithm will chase that edge and revisit nodes indefinitely.

Keep one shared visited set across all components. Pass it through recursive calls or iterations instead of recreating it for each start node.

Memory exhaustion from dense adjacency matrix

An adjacency matrix allocates V squared cells regardless of how many edges you actually have. A 50,000-vertex graph needs 2.5 billion cells. At 8 bytes per weight, that is about 20 GB. Services running on instances with limited RAM will get killed by the OOM killer and restart in a loop until you fix the representation.

Switch to adjacency lists for graphs over a few thousand vertices unless you actually need O(1) edge lookups and your graph is genuinely dense.

Stack overflow from recursive DFS on deep graphs

Recursive DFS is just recursion, which means it uses the call stack. Chain-like structures or very deep graphs will exhaust stack memory. A 100,000-node linked list will crash a recursive DFS on most systems.

Use the iterative version with an explicit stack. If you genuinely prefer the recursive style for readability, at least switch to iterative before deploying to production.

Race conditions in concurrent graph updates

When two threads add edges between the same vertices simultaneously, both can read the graph as empty, both write their edge, and you end up with duplicates or overwrites. This depends on your data structure, but the underlying problem is the same: non-atomic read-modify-write operations on shared state.

Use thread-safe data structures or add locks around mutations. For read-heavy workloads, immutable snapshots let readers work without contention.

Trade-Off Table

AspectAdjacency ListAdjacency MatrixEdge List
Space complexityO(V + E)O(V squared)O(E)
Add vertexO(1)O(V squared)O(1)
Add edgeO(1)O(1)O(1)
Remove vertexO(V + E)O(V squared)O(E)
Remove edgeO(degree)O(1)O(E)
Check edge existsO(degree)O(1)O(E)
Iterate neighborsO(degree)O(V)O(E)
Find all edges by weightO(E)O(V squared)O(E log E)
Dense graph performanceWastefulEfficientWasteful
Sparse graph performanceEfficientWastefulEfficient
Typical use casesRoute planning, social networks, dependency graphsDense network analysis, adjacency queries, small dense graphsMST algorithms, Kruskal, sorting edges by weight

Adjacency lists work well for sparse graphs where you iterate neighbors often. The matrix is better when you need fast edge lookups or your graph has many edges relative to vertices. Edge lists show up in algorithms like Kruskal where you need edges sorted by weight and random access is not important.

Observability Checklist

When running graph-based services in production, you need visibility into the health of your graph structure and traversal operations. This checklist covers the key signals to monitor.

Reference: Cycle detection and structural integrity

Reference: Connected component tracking

Cycle detection and structural integrity

Monitor the ratio of edges to vertices in your graph. A sudden spike in this ratio can indicate data corruption or ingestion bugs inserting duplicate edges. If your graph is supposed to be acyclic (like a dependency graph), alerting on cycle detection failures catches issues before they cascade into application errors.

Memory usage tracking by graph density

For adjacency matrix implementations, graph size directly drives memory consumption. Track your vertex count and alert when you approach your memory budget. For adjacency lists, watch for unexpected growth in the total edge count. If your graph is supposed to stabilize at a certain size and keeps growing, you likely have an ingestion bug creating duplicate or erroneous edges.

Traversal depth monitoring

Track the maximum and average traversal depth for your BFS and DFS operations. Deep traversals consume more memory and CPU. Sudden increases in maximum depth can indicate structural changes in your graph data, such as the introduction of long chains or unexpected connectivity.

Connected component tracking

Monitor the number of connected components and their size distribution. A service that expects a single connected component will behave unexpectedly when given disconnected data. Component count trending upward over time can indicate partial data ingestion failures or upstream data source issues.

Traversal latency percentiles

Graph traversal time scales with the size of the reachable subgraph from your start node. Monitor p50, p95, and p99 latencies for your traversal operations. If p99 is significantly higher than p50, you likely have outliers from traversals hitting large components or pathological structures.

Queue and stack depth for iterative traversals

When using iterative BFS or DFS, track the maximum size of your queue or stack during traversal. Spikes in these depths can precede memory pressure issues, especially for wide or deep graph structures.

Security Notes

Graph structures present unique security concerns that do not map cleanly to traditional application security categories.

Graph traversal denial of service via maliciously connected graphs

A malicious actor can craft input data that produces a graph structure designed to maximize traversal time. For example, a graph with high out-degree nodes concentrated in a small area will cause BFS to explore an enormous frontier before discovering that the target is unreachable. An attacker who can control input data can submit graphs that cause your traversal to consume excessive CPU and memory.

Mitigate this by imposing limits on graph size, vertex degree, and traversal budget before processing. If your service accepts graph data from external sources, validate structural properties before running expensive traversals.

Memory exhaustion from sparse graph flooding

An attacker can submit a graph with a very high number of vertices but few edges. With an adjacency matrix representation, this forces allocation of O(V squared) space even though almost all cells are empty. With an adjacency list, the overhead per vertex can still be significant if you initialize data structures for each vertex.

Validate that incoming graphs meet reasonable bounds on the ratio of edges to vertices, and choose representation based on your expected density rather than trusting user-provided vertex counts.

Timing attacks on shortest path algorithms

If your shortest path implementation runs in time proportional to the number of edges explored, an attacker who can infer timing variations may be able to infer graph structure. For example, if path queries with longer execution times indicate the presence of additional intermediate nodes, an attacker can probe your graph to learn about its topology.

This is a concern primarily for sensitive applications where the existence of connections itself is confidential. Constant-time pathfinding algorithms or noise injection can mitigate this, but the performance cost is significant.

Integer overflow in graph size calculations

When allocating buffers based on vertex count, be careful about integer overflow in size calculations. Computing n * n for matrix allocation with a large vertex count can overflow a 32-bit integer, resulting in a buffer far smaller than intended. Validate vertex counts against maximum allocation sizes before computing buffer dimensions.

Quick Recap Checklist

  • Adjacency list: O(V + E) space, good for sparse graphs
  • Adjacency matrix: O(V²) space, good for dense graphs, O(1) edge lookup
  • BFS: explores level by level, finds shortest path in unweighted graphs
  • DFS: explores deep first, used for topological sort, cycle detection
  • Both BFS and DFS are O(V + E) for adjacency list representation

Interview Questions

1. When would you use BFS over DFS?

Choose BFS when you need the shortest path in an unweighted graph, or when exploring level-by-level makes sense (like finding minimum number of "hops"). BFS uses a queue and explores all nodes at distance d before distance d+1. Choose DFS when you're doing tree traversal, need to explore deeply first (like solving mazes), or doing topological sorting. DFS uses less memory for wide graphs because it only stores the current path, not all frontier nodes.

2. How do you detect a cycle in a directed graph?

Use DFS with a recursion stack. Track nodes currently in the recursion stack (nodes on the current DFS path). When you encounter a neighbor that's in the recursion stack, you've found a back edge → cycle. Alternatively, use Kahn's algorithm (BFS-based topological sort): if the topological ordering doesn't include all vertices, there's a cycle. The DFS method is simpler: during DFS, if you see a node you're currently visiting, that's a cycle.

3. What's the space complexity of BFS vs DFS?

BFS uses O(V) for the visited set and O(W) for the queue, where W is the maximum width of the graph. In a balanced tree, W could be O(V). DFS uses O(V) for visited and O(H) for the stack, where H is the maximum depth (O(V) in worst case). For very wide graphs, BFS uses more memory; for very deep graphs (like linked lists), DFS uses more memory due to recursion depth.

4. How do you handle disconnected graphs?

Start a new BFS/DFS from each unvisited vertex. Maintain a visited set across all traversals. For each vertex not yet visited, run the search algorithm. This finds all connected components. For BFS, you'll have multiple queues (one per component). For DFS, the recursive version naturally handles this by calling dfs on unvisited vertices after each component.

5. What are the trade-offs between adjacency list and adjacency matrix representations?

Adjacency List: O(V + E) space, O(degree) edge lookup, O(degree) neighbor iteration, efficient for sparse graphs, easy to add vertices. Adjacency Matrix: O(V²) space, O(1) edge lookup, O(V) neighbor iteration, efficient for dense graphs, expensive to add vertices. Choose adjacency list when your graph is sparse (most real-world graphs). Choose adjacency matrix when you need fast edge lookups or the graph is dense.

6. How do you implement topological sort using DFS?

Perform DFS on each unvisited vertex. After visiting all neighbors of a node, push the node onto a stack (or prepend to a list). The final stack contains the topological ordering (reversed finish times). This works only for Directed Acyclic Graphs (DAGs). Time complexity: O(V + E). If a cycle is detected during DFS, the graph cannot be topologically sorted.

7. What is Kahn's algorithm for topological sorting?

Kahn's algorithm uses BFS-based approach: (1) Compute in-degree for each vertex. (2) Add all vertices with in-degree 0 to a queue. (3) While the queue is not empty, remove a vertex, add it to the topological order, and decrement in-degrees of its neighbors. Any neighbor whose in-degree becomes 0 is added to the queue. If the final order doesn't include all vertices, the graph has a cycle. Time complexity: O(V + E).

8. Explain Dijkstra's algorithm for shortest path in weighted graphs.

Dijkstra's algorithm finds the shortest path from a source node to all other nodes in a weighted graph with non-negative edge weights. It uses a priority queue (min-heap): start with distance 0 at source, repeatedly extract the node with the smallest distance, relax its neighbors (update distance if a shorter path is found). Time complexity: O((V + E) log V) with a binary heap. It does not work with negative edge weights — use Bellman-Ford instead.

9. What are articulation points and bridges in a graph?

An articulation point (or cut vertex) is a vertex whose removal increases the number of connected components. A bridge (or cut edge) is an edge whose removal increases the number of connected components. Both can be found in O(V + E) using DFS-based Tarjan's algorithm, which computes discovery times and low-link values. Articulation points and bridges identify critical nodes and edges in network reliability analysis.

10. Compare Prim's and Kruskal's algorithms for Minimum Spanning Tree.

Both find the Minimum Spanning Tree (MST) of a weighted undirected graph. Prim's algorithm grows a single tree from a start vertex, always adding the cheapest edge connecting the tree to a vertex outside it — O(E log V) with a binary heap. Best for dense graphs. Kruskal's algorithm sorts all edges by weight and adds them if they don't create a cycle (using Union-Find) — O(E log E). Best for sparse graphs. Prim is vertex-focused; Kruskal is edge-focused.

11. How would you check if a graph is bipartite?

Use BFS with graph coloring: assign color 0 to start vertex, then alternate colors (1 - color[u]) for all neighbors. If you ever encounter a neighbor with the same color as the current vertex, the graph is not bipartite. This works because bipartite graphs are exactly those with no odd-length cycles. Run this on each component for disconnected graphs. Time: O(V + E).

12. Explain the time and space complexity of BFS and DFS on an adjacency list.

BFS: Time O(V + E) — visits each vertex once and examines each edge once. Space O(V) — visited set, queue, parent map, and distance map all scale with V. DFS: Time O(V + E) — same as BFS. Space O(V) for visited set plus O(H) for the stack or recursion stack where H is the maximum depth. In the worst case (a path or linked list), H = V.

13. When is an adjacency matrix preferred over an adjacency list?

Choose adjacency matrix when: (1) The graph is dense — you have O(V^2) edges so the matrix is actually full, making O(1) edge lookup valuable. (2) You need to quickly check if an edge exists between any two vertices — matrix gives O(1) lookup vs O(degree) for a list. (3) The graph is small and fixed in size. (4) You're doing algorithms that scan all possible edges, like Floyd-Warshall. Avoid it for sparse graphs with thousands of vertices — the O(V^2) space is wasteful.

14. How do you find the shortest path in a weighted graph with negative edges?

Use the Bellman-Ford algorithm, which handles negative edge weights. Initialize distances to infinity except source = 0. Relax all edges V-1 times. If a distance still changes on the V-th iteration, a negative cycle exists and shortest path is undefined. Time: O(V * E). For better performance on negative graphs, consider SPFA (Shortest Path Faster Algorithm) which optimistically queues only nodes that actually changed. For graphs with no negative cycles but many vertices, Dijkstra with a deque (0-1 BFS variant) works if weights are -1, 0, or 1.

15. What is the difference between a directed and undirected graph in representation?

Undirected graph: Each edge (u, v) is symmetric — adding an edge in both directions. In adjacency list, v appears in u's list AND u appears in v's list. In matrix, matrix[u][v] == matrix[v][u]. Total edges is at most V(V-1)/2. Directed graph: Edges have direction — (u → v) does not imply (v → u). In adjacency list, only the specified direction is stored. In matrix, matrix[u][v] may differ from matrix[v][u]. Total edges can be V(V-1). Traversal algorithms must respect direction — neighbors() returns only outgoing edges by default.

16. How would you implement cycle detection for an undirected graph?

Use DFS with a parent tracker: when visiting a neighbor that's already visited but is NOT the parent of the current node, you found a cycle. For each vertex, if an adjacent (non-parent) visited vertex exists, cycle detected. Time: O(V + E). BFS also works: track visited and parent; if you see a visited node that isn't the parent, cycle exists. For undirected graphs, a third approach is Union-Find: process each edge — if both endpoints are already in the same set, cycle exists; otherwise union the sets. Union-Find is useful when you need to track connected components while detecting cycles incrementally.

17. Explain strongly connected components and how to find them.

A strongly connected component (SCC) is a maximal subgraph where every vertex reaches every other vertex. Find SCCs using Kosaraju's algorithm: (1) Run DFS to get vertices ordered by finish time (push to stack on exit). (2) Reverse all edges. (3) Pop vertices from stack and run DFS on the reversed graph — each DFS tree is one SCC. Time: O(V + E). Alternatively, use Tarjan's algorithm which does a single DFS pass using low-link values and a stack — slightly more memory efficient but conceptually similar.

18. What is a topological sort and when is it used?

Topological sort orders vertices in a DAG so that all edges point from earlier to later in the order. Used for: (1) Build systems — order tasks by dependencies. (2) Course scheduling — ensure prerequisites come first. (3) Task scheduling in parallel systems. (4) Package dependency resolution. Two methods: Kahn's algorithm (BFS with in-degree counting) or DFS with finish times (reverse postorder of DFS). A valid topological order exists if and only if the graph is a DAG — if you detect a cycle, ordering is impossible.

19. How does A* search differ from Dijkstra's algorithm?

Dijkstra's finds shortest path by always expanding the node with minimum distance from source — greedy, no heuristic. A* extends this by adding a heuristic estimate h(n) of the cost from node n to the goal: f(n) = g(n) + h(n), where g(n) is actual cost from source. A* is optimal if the heuristic is admissible (never overestimates true cost). It expands fewer nodes than Dijkstra but uses more memory to store the priority queue. Poor heuristics can degrade to Dijkstra performance; inconsistent heuristics (triangle inequality) allow early termination guarantees. A* is used in pathfinding, games (A* pathfinding), and robotics where domain knowledge provides good heuristics.

20. How would you handle very large graphs that don't fit in memory?

Use external memory graph algorithms: (1) For adjacency list, store edge lists on disk sorted by source vertex; stream through chunks. (2) For traversal, use disk-based BFS/DFS with frontier spilling. (3) Graph partitioning: divide vertices across machines (Pregel, Giraph style) — each machine stores outgoing edges for its partition. (4) For algorithms requiring random edge access, consider CSR/CSC-like sparse formats that group edges by source for sequential I/O. (5) Sampling and streaming: approximate results on subsets when exact answers are unnecessary. The key challenge is minimizing disk seeks — structure edge storage for sequential access and overlap computation with I/O where possible.

Further Reading

Graph Theory Foundations

  • “Introduction to Algorithms” (CLRS), Chapters 22–26 — The definitive textbook covering graph representations, BFS, DFS, topological sort, MSTs, and shortest paths. The pseudocode translates directly into any language.
  • “Algorithms” by Robert Sedgewick and Kevin Wayne — Excellent visual approach to graph algorithms with practical Java implementations. Part of the Coursera Algorithms course.

Traversal and Pathfinding Deep Dives

  • BFS and DFS visualizerVisuAlgo: Graph Traversal — Interactive step-through of BFS and DFS on customizable graphs. Helps build intuition for exploration order and visited state.
  • Dijkstra’s algorithm interactiveVisualgo: SSSP — Watch Dijkstra relax edges in real time on weighted graphs. Useful for understanding why it fails with negative edges.
  • A* search algorithm — Extends Dijkstra with heuristics for faster pathfinding in games and robotics. Start with “Artificial Intelligence: A Modern Approach” (Russell & Norvig), Chapter 3.

Representations and Applications

  • Adjacency list vs matrix benchmarksStanford CS166: Graph Data Structures — Detailed time/space analysis with concrete benchmarks across graph densities.
  • NetworkX library documentation — Python’s premier graph library. The NetworkX tutorial shows real-world usage patterns for adjacency representations.
  • Graph databases — Neo4j and Amazon Neptune use adjacency-list-like representations internally. “Graph Databases” by Ian Robinson, Jim Webber, and Emil Eifrém covers the theory behind property graph models.

Advanced Topics

  • Tarjan’s algorithm — Find strongly connected components, articulation points, and bridges in O(V + E). CLRS Chapter 22 covers this in depth.
  • Bellman-Ford algorithm — Handles negative edge weights and detects negative cycles. Essential when Dijkstra is insufficient.
  • Floyd-Warshall algorithm — All-pairs shortest path on dense graphs using dynamic programming. O(V³) but elegant and cache-friendly for small graphs (<1000 vertices).

Conclusion

Adjacency lists store neighbors per vertex (O(V + E) space, efficient for sparse graphs) while adjacency matrices use a 2D grid (O(V²) space, O(1) edge lookup, efficient for dense graphs). BFS explores level-by-level using a queue for shortest paths in unweighted graphs; DFS explores deeply first using a stack or recursion, used for topological sort and cycle detection. Both run in O(V + E) for adjacency list. Handle disconnected graphs by running search from each unvisited vertex with a shared visited set.

Category

Related Posts

Arrays vs Linked Lists: Understanding the Trade-offs

Compare arrays and linked lists in terms of access time, insertion/deletion efficiency, memory usage, and cache performance.

#arrays #linked-lists #data-structures

Arrays: 1D, 2D, and Multi-dimensional Mastery

Master array operations, traversal, search, common patterns like two-pointer and sliding window, and when to use multi-dimensional arrays.

#arrays #1d-arrays #2d-arrays

Articulation Points & Bridges in Graphs

Find critical vertices and edges whose removal disconnects the graph using DFS-based algorithms.

#articulation-points #bridges #graph-algorithms