Skip to content
Hello Python
Data Structure1 Practice2 Interview

Graph

Vertices and edges representing arbitrary relationships, reachability, paths, and dependencies. This guide turns that contract into deliberate Python operations, interview invariants, and a runnable practice loop.

Recognize it when

Consider Graph when the prompt's constraints and required operations match this shape: Vertices and edges representing arbitrary relationships, reachability, paths, and dependencies.

Pybit studies a professional Graph interview workspace with precise technical objects.
On this page · Mental Model

Checking your account…

Sign in to save your code and progress across devices. The lesson and problem statement remain public.

Graph Code Labs

Mental Model

A graph separates entities (vertices) from relationships (edges). Unlike a tree, a vertex may have multiple incoming paths, and cycles may return to already discovered state. Direction, weight, parallel edges, and self-loops are contract decisions, not implementation details.

Vertices, Edges, and Direction

For an undirected edge, record both directions; for a directed edge, record only source to target. Degree, reachability, and cycle meaning change with direction. Normalize duplicate-edge policy before building storage so traversal and complexity claims match the logical graph.

Choose an Adjacency Representation

An adjacency list uses O(V + E) space and enumerates actual neighbors efficiently. An adjacency matrix uses O(V squared) space but tests a particular edge in O(1). Compare density and query type rather than choosing by habit. Sets deduplicate neighbors; lists preserve repeated edges and input order.

Build Two Graph Representations

Reference
def build_graph_representations(node_count, edges):
    adjacency = [set() for _ in range(node_count)]
    matrix = [[0 for _ in range(node_count)] for _ in range(node_count)]
    for left, right in edges:
        adjacency[left].add(right)
        adjacency[right].add(left)
        matrix[left][right] = 1
        matrix[right][left] = 1
    return [[sorted(neighbors) for neighbors in adjacency], matrix]
Practice

Implement build_graph_representations(node_count, edges). Deduplicate undirected edges and return sorted adjacency lists plus an adjacency matrix.

Public tests

  • Verify matching list and matrix graphs

Loading interactive editor.If it does not appear, the reference and starter code remain readable.Reload editor

Traverse with Explicit Visited State

DFS and BFS need visited state because a graph can contain cycles and converging paths. Mark when an item is discovered to prevent duplicate frontier entries. To find components, begin a traversal from each still-unvisited vertex; one traversal consumes exactly one connected component.

Measure Connected Components

Reference
def component_sizes(node_count, edges):
    adjacency = [[] for _ in range(node_count)]
    for left, right in edges:
        adjacency[left].append(right)
        adjacency[right].append(left)
    visited = set()
    sizes = []
    for start in range(node_count):
        if start in visited:
            continue
        visited.add(start)
        stack = [start]
        size = 0
        while stack:
            node = stack.pop()
            size += 1
            for neighbor in adjacency[node]:
                if neighbor not in visited:
                    visited.add(neighbor)
                    stack.append(neighbor)
        sizes.append(size)
    return sizes
Practice

Implement component_sizes(node_count, edges). Include isolated vertices and return undirected component sizes in increasing start-vertex order.

Public tests

  • Verify cycles and isolated vertices

Loading interactive editor.If it does not appear, the reference and starter code remain readable.Reload editor

The deque reference(opens in a new tab) supports the BFS frontier; a list supports iterative DFS as a stack. Both traversals are O(V + E) with adjacency lists because each vertex and stored edge is examined a bounded number of times.

Choose Graph or Grid or Tree

Choose a graph for arbitrary relationships, multiple paths, cycles, or dependencies. Choose an implicit grid when neighbors derive from coordinates instead of stored edges. Choose a rooted tree when every non-root node has one parent and there is a unique root path; then parent state can replace a general visited set. Compare BFS, DFS, topological order, and shortest-path algorithms only after the edge contract is known.

Common Pitfalls

  • Adding only one direction for an undirected edge loses reachability.
  • Marking visited at removal permits duplicate enqueues and inflated work.
  • Omitting isolated vertices because they never appear in the edge list loses components.
  • Using a V×V matrix for a sparse graph wastes memory.
  • Treating any repeated vertex as a directed-cycle proof ignores active versus completed state.

Explain It in an Interview

State V, E, direction, weight, and duplicate policy before coding. Define what visited means and when it changes. Give O(V + E) for adjacency-list traversal, noting that undirected edges are stored twice, and distinguish frontier space from representation space.

Build Graph Representations(opens in a new tab) forces a storage decision. Course Schedule(opens in a new tab) adds direction and cycle detection to model prerequisites.

Python Version Note

Complexity & Invariants

Interview bounds depend on the stated representation, input model, and real Python operations.

OperationAverageWorstInterview note
Core Graph workflowO(V^2 + E + V log V)O(V^2 + E + V log V)Accumulate canonical neighbors in per-node sets while writing symmetric matrix cells, then sort each set for deterministic output.

Space

O(V^2 + E) for the demonstrated Graph workflow.

Assumptions

  • State the concrete operation and representation before claiming a bound; tree height and graph density can change it.
  • The bound counts the operations in Build Graph Representations and does not hide Python slicing, sorting, or copying.

Invariants Worth Saying Aloud

  1. State the precise Graph invariant before coding and preserve it after every update, traversal step, or recursive return.
  2. Represent sparse neighbor relationships and dense edge lookup from one graph contract. This remains true after every accepted operation.
  3. Normalize undirected edges, duplicates, and self-loops consistently. This remains true after every accepted operation.

When To Use Or Avoid Graph

Use It When

  • Use Graph when its representation directly supports the prompt's repeated query or update.
  • Use it when the invariant can be stated before coding and preserved after every operation.

Choose Another Tool When

  • Avoid it when a simpler Python sequence or mapping communicates the same contract with less state.
  • Avoid it when building the structure costs more time or space than the number of required queries can justify.

Common Pitfalls

These are the mistakes most likely to survive a happy-path example and fail a boundary case.

Leaving the core transition unfinished

Placeholder code cannot preserve the Graph invariant or satisfy the public contract.

Prevent it: Implement the smallest complete transition and run the public tests before adding optimizations.

Avoid

def build_graph_representations(node_count, edges):
    pass

Use instead

def build_graph_representations(node_count, edges):
    adjacency = [set() for _ in range(node_count)]
    matrix = [[0 for _ in range(node_count)] for _ in range(node_count)]
    for left, right in edges:
        adjacency[left].add(right)
        adjacency[right].add(left)
        matrix[left][right] = 1
        matrix[right][left] = 1
    return [[sorted(neighbors) for neighbors in adjacency], matrix]

Breaking the central invariant

Records only the listed edge direction, producing a directed graph instead of the required undirected graph.

Prevent it: Keep this invariant visible while editing: State the precise Graph invariant before coding and preserve it after every update, traversal step, or recursive return.

Avoid

def build_graph_representations(node_count, edges):
    adjacency = [[] for _ in range(node_count)]
    matrix = [[0] * node_count for _ in range(node_count)]
    for left, right in edges:
        adjacency[left].append(right)
        matrix[left][right] = 1
    return [adjacency, matrix]

Use instead

def build_graph_representations(node_count, edges):
    adjacency = [set() for _ in range(node_count)]
    matrix = [[0 for _ in range(node_count)] for _ in range(node_count)]
    for left, right in edges:
        adjacency[left].add(right)
        adjacency[right].add(left)
        matrix[left][right] = 1
        matrix[right][left] = 1
    return [[sorted(neighbors) for neighbors in adjacency], matrix]

Hiding Python work inside the loop

Handle empty state, duplicate values, aliasing, and serialization boundaries explicitly in tests.

Prevent it: Account for every slice, copy, sort, membership test, and container mutation before claiming O(V^2 + E + V log V).

Avoid

def build_graph_representations(node_count, edges):
    adjacency = [[] for _ in range(node_count)]
    matrix = [[0] * node_count for _ in range(node_count)]
    for left, right in edges:
        adjacency[left].append(right)
        matrix[left][right] = 1
    return [adjacency, matrix]

Use instead

def build_graph_representations(node_count, edges):
    adjacency = [set() for _ in range(node_count)]
    matrix = [[0 for _ in range(node_count)] for _ in range(node_count)]
    for left, right in edges:
        adjacency[left].add(right)
        adjacency[right].add(left)
        matrix[left][right] = 1
        matrix[right][left] = 1
    return [[sorted(neighbors) for neighbors in adjacency], matrix]

Reviewed References

Official Python documentation supports language behavior. Canonical problem pages provide additional practice context.