Graphs — across Python, Go, TypeScript & C++

Use adjacency lists for interviews: build in O(V + E), traverse in O(V + E). For grids, each cell is a node and each valid direction is an edge.

Adjacency lists from edge lists

operationPythonGoTypeScriptC++
directed
O(V + E)
g = [[] for _ in range(n)]
for u, v in edges:
  g[u].append(v)
g := make([][]int, n)
for _, e := range edges {
  u, v := e[0], e[1]
  g[u] = append(g[u], v)
}
const g = Array.from({ length: n }, () => [] as number[])
for (const [u, v] of edges) g[u].push(v)
vector<vector<int>> g(n);
for (auto [u, v] : edges) g[u].push_back(v);
undirected
O(V + E)
g = [[] for _ in range(n)]
for u, v in edges:
  g[u].append(v)
  g[v].append(u)
g[u] = append(g[u], v)
g[v] = append(g[v], u)
g[u].push(v)
g[v].push(u)
g[u].push_back(v);
g[v].push_back(u);
weighted
O(V + E)
g = [[] for _ in range(n)]
for u, v, w in edges:
  g[u].append((v, w))
type Edge struct { to, w int }
g := make([][]Edge, n)
g[u] = append(g[u], Edge{v, w})
type Edge = [to: number, w: number]
const g: Edge[][] = Array.from({ length: n }, () => [])
g[u].push([v, w])
vector<vector<pair<int, int>>> g(n);
g[u].push_back({v, w});

Visited sets

operationPythonGoTypeScriptC++
dense ids
O(1)
seen = [False] * n
seen[u] = True
if not seen[v]: ...
seen := make([]bool, n)
seen[u] = true
if !seen[v] {}
const seen = Array(n).fill(false)
seen[u] = true
if (!seen[v]) {}
vector<char> seen(n, 0);
seen[u] = 1;
if (!seen[v]) {}
labels
avg O(1)
seen = {start}
if v not in seen:
  seen.add(v)
seen := map[string]struct{}{start: {}}
if _, ok := seen[v]; !ok {
  seen[v] = struct{}{}
}
const seen = new Set<string>([start])
if (!seen.has(v)) seen.add(v)
unordered_set<string> seen{start};
if (!seen.count(v)) seen.insert(v);

Core graph templates

operationPythonGoTypeScriptC++
iterative DFS
O(V + E)
def dfs(g, start):
  order, seen = [], {start}
  st = [start]
  while st:
    u = st.pop()
    order.append(u)
    for v in g[u]:
      if v not in seen:
        seen.add(v); st.append(v)
  return order
seen := map[int]struct{}{s: {}}
st := []int{s}
for len(st) > 0 {
  u := st[len(st)-1]; st = st[:len(st)-1]
  for _, v := range g[u] { if _, ok := seen[v]; !ok { seen[v] = struct{}{}; st = append(st, v) } }
}
const seen = new Set<number>([s])
const st = [s]
while (st.length) {
  const u = st.pop()!
  for (const v of g[u]) if (!seen.has(v)) { seen.add(v); st.push(v) }
}
vector<char> seen(n);
vector<int> st{s}; seen[s] = 1;
while (!st.empty()) {
  int u = st.back(); st.pop_back();
  for (int v : g[u]) if (!seen[v]) { seen[v] = 1; st.push_back(v); }
}
BFS shortest path
unweighted
O(V + E)
from collections import deque

def bfs_dist(g, start):
  dist = {start: 0}
  q = deque([start])
  while q:
    u = q.popleft()
    for v in g[u]:
      if v not in dist:
        dist[v] = dist[u] + 1
        q.append(v)
  return dist
dist := map[int]int{s: 0}
q, head := []int{s}, 0
for head < len(q) {
  u := q[head]; head++
  for _, v := range g[u] { if _, ok := dist[v]; !ok { dist[v] = dist[u] + 1; q = append(q, v) } }
}
const dist = new Map<number, number>([[s, 0]])
const q = [s]; let head = 0
while (head < q.length) {
  const u = q[head++]
  for (const v of g[u]) if (!dist.has(v)) { dist.set(v, dist.get(u)! + 1); q.push(v) }
}
vector<int> dist(n, -1);
queue<int> q; dist[s] = 0; q.push(s);
while (!q.empty()) {
  int u = q.front(); q.pop();
  for (int v : g[u]) if (dist[v] == -1) { dist[v] = dist[u] + 1; q.push(v); }
}
Kahn topo sort
DAG only
O(V + E)
from collections import deque

def topo(n, edges):
  g = [[] for _ in range(n)]
  indeg = [0] * n
  for u, v in edges:
    g[u].append(v); indeg[v] += 1
  q = deque(i for i, d in enumerate(indeg) if d == 0)
  order = []
  while q:
    u = q.popleft(); order.append(u)
    for v in g[u]:
      indeg[v] -= 1
      if indeg[v] == 0: q.append(v)
  return order if len(order) == n else []
indeg := make([]int, n)
// build g and indeg[v]++ for each u->v
q, head := []int{}, 0
// enqueue all indeg 0
for head < len(q) {
  u := q[head]; head++
  for _, v := range g[u] { indeg[v]--; if indeg[v] == 0 { q = append(q, v) } }
}
const indeg = Array(n).fill(0)
// build g and indeg[v]++
const q: number[] = [], order: number[] = []
let head = 0
while (head < q.length) {
  const u = q[head++]; order.push(u)
  for (const v of g[u]) if (--indeg[v] === 0) q.push(v)
}
vector<int> indeg(n), order;
queue<int> q;
// build g and indeg[v]++; push zeros
while (!q.empty()) {
  int u = q.front(); q.pop(); order.push_back(u);
  for (int v : g[u]) if (--indeg[v] == 0) q.push(v);
}
Dijkstra
nonnegative weights
O((V + E) log V)
import heapq

def dijkstra(g, src):
  INF = float("inf")
  dist = [INF] * len(g)
  dist[src] = 0
  pq = [(0, src)]
  while pq:
    d, u = heapq.heappop(pq)
    if d != dist[u]: continue
    for v, w in g[u]:
      nd = d + w
      if nd < dist[v]:
        dist[v] = nd
        heapq.heappush(pq, (nd, v))
  return dist
type Item struct { d, u int }
// heap.Interface; Less by smaller d
heap.Push(&pq, Item{0, s})
for pq.Len() > 0 {
  it := heap.Pop(&pq).(Item)
  if it.d != dist[it.u] { continue }
  // relax Edge{to,w}; push Item{nd,to}
}
type P = [d: number, u: number]
const pq = new MinHeap<P>((a, b) => a[0] - b[0])
pq.push([0, s])
while (pq.size()) {
  const [d, u] = pq.pop()!
  if (d !== dist[u]) continue
  // relax [v,w]; push [nd,v]
}
using P = pair<int, int>;
priority_queue<P, vector<P>, greater<P>> pq;
pq.push({0, s});
while (!pq.empty()) {
  auto [d, u] = pq.top(); pq.pop();
  if (d != dist[u]) continue;
  // relax pair{v,w}; push {nd,v}
}

Grid as graph

operationPythonGoTypeScriptC++
4 directions
O(1)
DIRS = ((1, 0), (-1, 0), (0, 1), (0, -1)) dirs := [][2]int{{1,0}, {-1,0}, {0,1}, {0,-1}} const dirs: [number, number][] = [[1,0], [-1,0], [0,1], [0,-1]] int dirs[4][2] = {{1,0}, {-1,0}, {0,1}, {0,-1}};
neighbors
O(RC)
rows, cols = len(grid), len(grid[0])
for dr, dc in DIRS:
  nr, nc = r + dr, c + dc
  if 0 <= nr < rows and 0 <= nc < cols:
    visit(nr, nc)
rows, cols := len(grid), len(grid[0])
for _, d := range dirs {
  nr, nc := r+d[0], c+d[1]
  if 0 <= nr && nr < rows && 0 <= nc && nc < cols { visit(nr, nc) }
}
const rows = grid.length, cols = grid[0].length
for (const [dr, dc] of dirs) {
  const nr = r + dr, nc = c + dc
  if (0 <= nr && nr < rows && 0 <= nc && nc < cols) visit(nr, nc)
}
int rows = grid.size(), cols = grid[0].size();
for (auto& d : dirs) {
  int nr = r + d[0], nc = c + d[1];
  if (0 <= nr && nr < rows && 0 <= nc && nc < cols) visit(nr, nc);
}

Gotchas

PythonGoTypeScriptC++
  • Recursive DFS hits the default recursion limit on deep graphs; prefer an explicit stack for big inputs.
  • In BFS, mark visited when enqueuing, not when popping, or parallel edges can enqueue the same node many times.
  • heapq has no decrease-key; push the new distance and skip stale (d, node) pairs.
  • Use _, ok := seen[v]; reading a missing map key returns the zero value.
  • Keep a queue head index; repeatedly slicing from the front can retain old backing-array storage.
  • container/heap stores any; Push/Pop usually need pointer receivers and type assertions.
  • Array.shift() is O(n); BFS queues should use head++.
  • Set<[number, number]> compares array identity; encode grid cells as strings or ids.
  • There is no built-in heap; sorting the queue on every Dijkstra step is too slow.
  • Recursive DFS can overflow the process stack; iterative DFS is safer on hostile depth.
  • priority_queue<pair<int,int>> is a max-heap and pairs compare first, then second; store {dist, node} with greater<>.
  • Self-loops block Kahn topo sort; parallel edges must increment and decrement indegree once per edge.

Official docs: https://docs.python.org/3/library/collections.html#collections.deque · https://docs.python.org/3/library/heapq.html · https://pkg.go.dev/container/heap · https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set · https://en.cppreference.com/w/cpp/container/priority_queue