🕸️ Graphs Representation

A graph is a set of nodes (vertices) connected by edges. Graphs model networks, relationships, maps, dependencies, essentially anything that isn’t strictly hierarchical like a tree.

Directed vs Undirected

# Undirected: edge A-B means you can travel both ways
# Directed: edge A->B means you can only travel from A to B (unless B->A also exists)

Weighted vs Unweighted

# Unweighted: edges just represent connection, e.g. "friends with"
# Weighted: edges carry a cost/distance/capacity, e.g. "road distance between cities"

Adjacency List (Most Common in Practice)

Each node maps to a list of its neighbors. Space efficient for sparse graphs.

graph = {
    "A": ["B", "C"],
    "B": ["A", "D"],
    "C": ["A", "D"],
    "D": ["B", "C"],
}
 
# Weighted version, using (neighbor, weight) tuples
weighted_graph = {
    "A": [("B", 4), ("C", 1)],
    "B": [("A", 4), ("D", 2)],
    "C": [("A", 1), ("D", 5)],
    "D": [("B", 2), ("C", 5)],
}

Building from a list of edges:

from collections import defaultdict
 
def build_graph(edges, directed=False):
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)
        if not directed:
            graph[v].append(u)
    return graph
 
build_graph([("A", "B"), ("B", "C"), ("A", "C")])

Adjacency Matrix

A 2D array where matrix[i][j] indicates an edge between node i and node j. Simple and gives O(1) edge lookup, but wastes memory on sparse graphs.

n = 4     # number of nodes
matrix = [[0] * n for _ in range(n)]
 
def add_edge(matrix, u, v, directed=False):
    matrix[u][v] = 1
    if not directed:
        matrix[v][u] = 1
 
add_edge(matrix, 0, 1)
add_edge(matrix, 1, 2)
matrix[0][1]     # 1, edge exists

Adjacency List vs Adjacency Matrix

Adjacency ListAdjacency Matrix
SpaceO(V + E), good for sparse graphsO(V²), wasteful if sparse
Check if edge (u, v) existsO(degree of u)O(1)
Iterate all neighbors of a nodeO(degree of u), efficientO(V), scans the whole row
Best forMost real-world graphs (sparse)Dense graphs, or when O(1) edge lookups matter most

Default to adjacency list

Most real-world and interview graphs are sparse (relatively few edges compared to possible ones). Adjacency lists are the right default unless you specifically need O(1) edge existence checks on a dense graph.

Object-Oriented Graph Node (Alternative Representation)

class GraphNode:
    def __init__(self, value):
        self.value = value
        self.neighbors = []
 
a, b, c = GraphNode("A"), GraphNode("B"), GraphNode("C")
a.neighbors = [b, c]
b.neighbors = [a]
c.neighbors = [a]

Common in problems that hand you a graph as literal node objects (e.g. “clone this graph”) rather than as a dict/list structure.

Trees Are Just a Special Case of Graphs

A tree is a connected, acyclic, undirected graph

Every tree is a graph, but not every graph is a tree. Graphs can have cycles, disconnected components, and multiple paths between two nodes, none of which are allowed in a tree. This is why graph algorithms need to explicitly track “visited” nodes, something tree traversals usually don’t worry about (no cycles to loop on).

See Graph Traversal BFS and DFS for how to actually walk a graph, and Shortest Path Algorithms / Minimum Spanning Trees for weighted graph algorithms built on top of these representations.