skip to content

Neighbor iteration is implemented by scanning all E edge records once per vertex — what does that cost?

level: seniorimportance: should knowfreq 44%

answer

  1. a linear helper called a linear number of times
  2. multiply the two loop bounds
  3. the inner pass restarts for every vertex
  4. one bucketing pass makes it linear
  5. count, prefix-sum, place

basics

~10 s

The total cost is O(V*E), because each of the V vertices triggers a full pass over the edge records. Bucketing the edges by source once, in O(V+E), turns the whole sweep into O(V+E).

solid answer

~50 s

Each vertex triggers a full O(E) pass, so the pair of loops is O(V*E) — with 10^5 vertices and 10^6 edges that is 10^11 record comparisons, and on a dense graph where E grows like V^2 it is O(V^3). The inner loop looks innocent because its bound really is O(E); the cost is in how often it runs. The fix is a one-time index build: count how many edges leave each vertex, prefix-sum those counts into per-vertex start offsets, then place each edge record into its slot. Two passes over the records, O(V+E) time and O(V+E) memory, after which every vertex's neighbors are contiguous and the whole sweep is O(V+E). In review the tell is a helper that takes the entire edge collection plus one vertex — that helper is O(E) per call, so count its callers.

code

pseudocode · 9 lines
pseudocode
// edge list: edge_from[i], edge_to[i], edge_w[i] for i in 0..E-1
for s in 0..V-1:
    best = INFINITY
    for i in 0..E-1:
        if edge_from[i] == s:
            if edge_w[i] < best:
                best = edge_w[i]
    cheapest[s] = best
...

go deeper

for a junior

Recall that nesting a loop over all E edges inside a loop over all V vertices multiplies the two, giving O(V*E). Knowing that grouping edges by their source vertex first is what avoids the repetition is enough at this level.

for a middle

Explain the counting build step by step — count out-degrees, prefix-sum into start offsets, place each edge — and state that it costs O(V+E) once, less than a single unindexed sweep already costs.

for a senior

Show the review instinct: name the cost as a product, convert it to operations at production scale, and explain why fixture-sized tests cannot reveal it. Be ready to reject the matrix as an over-correction that swaps a time bug for a memory bug.

for a principal

Own the systemic angle: a representation chosen for ingestion convenience quietly sets the cost of every later query, so the call is where the conversion happens and who owns it. Weigh a one-time index build against carrying two representations and keeping them consistent.

## What the code actually does An edge list stores edges as flat records — a source, a target, maybe a weight — with no per-vertex index. Code that needs "the edges leaving s" therefore has only one option available in the data it was handed: filter the whole collection. Written once, that is a defensible O(E) operation. Written inside a loop over vertices, it becomes O(V*E), and the multiplication is invisible at a glance because each half looks reasonable on its own. This is the review pattern worth training your eye on: a linear helper called a linear number of times. The helper's own bound is honest, the enclosing loop's bound is honest, and the product is the defect. ## Putting numbers on the blowup O(V*E) is not a mild penalty. For a graph with V = 10^5 and E = 10^6, a single sweep is 10^11 record comparisons — minutes to hours, where the indexed version finishes in the time it takes to touch 1.1*10^6 entries. The scaling is worse than it first appears, because on real data E grows with V. If the graph is sparse with E proportional to V, the sweep is effectively O(V^2). If the graph is dense with E proportional to V^2, it is O(V^3). This is also why the bug survives code review and testing. On a fixture graph with 50 vertices and 200 edges, V*E is 10,000 — instant. Nothing in the test suite distinguishes the two implementations; only production scale does. ## The fix: build the index once The repair is a counting pass, and it is worth being able to describe it precisely rather than gesturing at "just use an adjacency list": 1. **Count.** Walk the E records once, incrementing `outdeg[from]` for each. O(E). 2. **Prefix-sum.** Turn those counts into start offsets: `start[0] = 0`, and `start[i] = start[i-1] + outdeg[i-1]`. O(V). 3. **Place.** Walk the records once more, writing each target into the next free slot of its source's region and advancing a per-vertex cursor. O(E). The result is one contiguous array of neighbor entries plus a V+1-element offset array. Vertex s's neighbors occupy positions `start[s]` through `start[s+1] - 1`, so enumerating them is O(deg(s)) and the whole sweep over every vertex touches each edge exactly once: O(V+E). Total build cost O(V+E) time, O(V+E) memory. Since the sweep alone was O(V*E), the index pays for itself the first time you run it — you do not need to amortise it over many queries to justify it. The contiguous layout is a second, non-asymptotic win: neighbor entries for one vertex sit next to each other in memory, so the walk streams instead of chasing scattered references. ## Alternatives, and why they are usually worse here **Sort the records by source.** O(E log E), after which each vertex's edges form one contiguous run and a merged sweep is O(E). Correct and simple, but strictly more work than the O(E) counting build, and it mutates or copies the input. **Convert to a matrix.** That drops the sweep to O(V^2), which beats O(V*E) whenever E exceeds V, but it costs V^2 memory and is still asymptotically worse than O(V+E) on a sparse graph. Reaching for the matrix to fix a scan problem is the classic over-correction: it trades a time bug for a memory bug. **Cache the last scanned position.** Tempting and wrong, because the records are unordered — the edges leaving vertex 7 are scattered arbitrarily, so there is no position to resume from. This distractor is common precisely because it feels like a cheap fix. ## When the edge list is the right storage None of this means edge lists are bad. They are the smallest representation, O(E) with no per-vertex overhead, and they are exactly right for workloads that consume every edge in some order and never ask about one vertex: sorting all edges by weight, streaming them from storage in batches, or algorithms whose inner step relaxes or examines every edge each round. The defect is not the storage choice; it is *querying by vertex against a representation that has no vertex index*. Either build the index or stop asking per-vertex questions. ## What to say in the review Name the cost as a product, not as a bound on one loop: "this is V passes over E records, so O(V*E) — at our scale that's 10^11 comparisons." Then propose the concrete replacement — the counting build above, one time, in O(V+E) — and note that the fixture graphs in the test suite are far too small to show the difference, so the change needs a benchmark at a realistic size rather than a green test run.

  • When is keeping an edge-list-only representation the right call?
    When the workload consumes every edge in some order and never asks a per-vertex question: sorting all edges by weight, streaming them from storage, or an algorithm whose round examines every edge once. The edge list is the smallest layout, O(E) with no per-vertex overhead, and building an index for it would be wasted work. The defect is querying by vertex against a representation that has no vertex index — not the representation itself.
  • Why does this survive code review and the test suite?
    Because both loops are individually honest — an O(E) filter and a loop over V vertices — and the product is only visible if you multiply them. Test fixtures make it worse: at 50 vertices and 200 edges, V*E is 10,000 operations, indistinguishable from the linear version. Catching it takes reading for the product and benchmarking at realistic size, not a green test run.
  • Would converting to an adjacency matrix fix it?
    It would improve the sweep to O(V^2), which beats O(V*E) whenever E exceeds V, but it is still asymptotically worse than the O(V+E) you get from bucketing, and it costs V^2 memory that a large sparse graph cannot afford. Fixing a scan problem by allocating a quadratic grid trades a time bug for a memory bug; build the per-vertex index instead.

saying these in an interview costs you the question

  • Calls it O(E) because the inner loop scans the edges
  • Says it is fine because today's graph is small
  • Claims the runtime will index the scan automatically
  • Suggests caching a resume position in an unordered record set
  • Reaches for a matrix without checking V^2 memory

context