Longest consecutive run: expand only from startsLESSON 2.09 · 9 OF 43 IN CHAPTER
PART A / Coding problems and trade-offs
Step 35 of 252
LESSON 2.09 · 9 OF 43 IN CHAPTERTry it, then open the solution

Longest consecutive run: expand only from starts

Constructed practice problem; no company attribution. Prerequisites: map/set membership.

Candidate brief

THE PROBLEM

An importer receives integer record numbers in arbitrary order, with duplicates. Report the length of the longest gap-free run of distinct numbers. Adjacent input positions do not matter. Can you avoid sorting without repeatedly walking the same run?

Write this:

def longest_consecutive(nums):
    ...
Contract Decision
Input Integer list/tuple; booleans excluded
Output Length of longest set of values a, a+1, ..., b
Boundaries Empty gives 0; duplicates do not extend runs; negative values allowed
Invalid input Invalid container or element raises ValueError
Excluded Returning all runs, requiring input adjacency, or mutating input
Optional refresher · the underlying tool

A set answers membership questions but does not order numbers. Count a consecutive run only from a value whose predecessor is absent:

values = {1, 2, 3, 8}
print(1 - 1 not in values)  # True: start a run
print(2 - 1 not in values)  # False: already inside one

For [3,2,1,8,2], the longest run has length 3, despite unsorted input and duplicate 2. Define whether duplicates count as separate run positions.

A design choice worth saying aloud

The values set gives membership without storing duplicates or ordering. Starting a run only when value - 1 is absent prevents walking the same run from every member. A sort-based baseline is simpler but costs O(n log n); say which guarantee the set trades space for.

What the interviewer expects

The interviewer gives you the scenario and the contract above. Explain what a successful call returns, walk through one example below, and name what your state means before choosing a data structure.

Done means: Length of longest set of values a, a+1, ..., b.

Now predict each output before looking at the reference; invalid input should leave any existing state unchanged unless the contract says otherwise.

Test-case scenarios to settle before coding

01 · Representative

Input / starting state
[100,4,200,1,3,2,2]
Expected result
4

What it is testing: Duplicates do not lengthen the run 1..4.

02 · Empty

Input / starting state
[]
Expected result
0

What it is testing: No run exists.

03 · Negative bridge

Input / starting state
[-1,1,0]
Expected result
3

What it is testing: The ordering crosses zero normally.

04 · Duplicates only

Input / starting state
[5,5,5]
Expected result
1

What it is testing: Distinct values define run length.

05 · Separated values

Input / starting state
[1,3,5]
Expected result
1

What it is testing: Input adjacency is irrelevant.

06 · Invalid/atomic

Input / starting state
noninteger or boolean element
Expected result
ValueError; input unchanged

What it is testing: Validate before building the set.

For each case, show which branch or state change produces that result.

longest_consecutive([100, 4, 200, 1, 3, 2, 2]) == 4, for values 1 through 4. longest_consecutive([-1, 1, 0]) == 3; longest_consecutive([]) == 0. longest_consecutive(["1"]) raises ValueError.

Before opening the explanation, restate the contract, trace the smallest useful example, implement a baseline, and identify the repeated work. Then implement your improvement independently and derive tests from the contract. Say what your state means before saying which data structure stores it.

Worked lesson, changed requirements, and reference

Account for which starts are redundant

Sort a copy, ignore duplicates, and count stretches with difference one. That baseline is straightforward O(n log n) time and O(n) auxiliary space in Python. A set supplies expected constant-time existence checks, but starting a forward walk at every value is still quadratic for [1, 2, ..., n]. Changing the container alone does not remove repeated work. Identify which values can be genuine starts: precisely those whose predecessor is absent.

Distinct value Predecessor present? Work allowed
1 no walk 1, 2, 3, 4
2 yes, 1 skip
3 yes, 2 skip
4 yes, 3 skip
100 no walk 100
200 no walk 200

For each start, increment an endpoint while the next consecutive value exists. Every finite maximal run has exactly one smallest element, whose predecessor is missing, so every run is considered. During a walk, every value from start up to but excluding the endpoint is present. The first absent endpoint proves the run is maximal. Non-starts cannot produce a longer answer than their own run's start, so skipping them preserves correctness.

The outer loop touches u distinct values. Across all inner loops, each distinct value is walked once, plus one failed lookup per run. That aggregate argument, not the number of nested loops, establishes expected O(n) time and O(u) auxiliary space. Iterate over the set, not the original list: repeated copies of the same start could otherwise retraverse a long run. Input order and set iteration order do not affect the maximum length.

Follow-up 1: return the run with the smallest start on ties

Predict the additional state: retain (start, length) rather than only length, and compare length first, then start. Never rely on set iteration order.

Completed run Candidate Comparison result
8, 9 (8, 2) initial best
1, 2 (1, 2) replace: same length, smaller start
20 (20, 1) retain (1, 2)

Returning just endpoints stays O(1) output. Materializing all values in the best run adds O(length) output, though it does not change the O(n) total bound.

Follow-up 2: answer after every insertion

Rebuilding the set and rescanning after every arrival costs quadratic total work. Use disjoint-set union: each new distinct value starts as a singleton component, then unions with existing immediate neighbors; component size is run length.

Diagram: Follow-up 2: answer after every insertion

Path compression and union by size give near-constant amortized insertion, but deleting an interior value can split a run and needs a different design. A senior candidate proves the aggregate bound and tests duplicate starts. A lead candidate specifies insertion/deletion semantics and whether integer adjacency is meaningful across shards before adopting a dynamic connectivity representation.

Run and check

From the repository root:

cd curriculum/01-code/02-data-structures-algorithms/problems/08-longest-consecutive
python -m unittest -v test_solution.py

Reference implementation (download file, source below) · Contract and oracle tests (download file, source below). Read the tests after your attempt. A green reference suite verifies the supplied implementation; it does not demonstrate independent transfer. Reimplement one follow-up with the reference closed and explain which old invariant no longer holds.

Reference implementation · solution.py
def _integers(values):
    if not isinstance(values, (list, tuple)) or any(type(x) is not int for x in values):
        raise ValueError("expected a list or tuple of integers, excluding bool")

def longest_consecutive(nums):
    _integers(nums)
    values = set(nums)
    best = 0
    for start in values:
        if start - 1 in values:
            continue
        end = start
        while end in values:
            end += 1
        best = max(best, end - start)
    return best
Contract and oracle tests · test_solution.py
import itertools
import unittest
from solution import longest_consecutive

class ConsecutiveTests(unittest.TestCase):
    def test_boundaries(self):
        self.assertEqual(longest_consecutive([100, 4, 200, 1, 3, 2, 2]), 4)
        self.assertEqual(longest_consecutive([-1, 1, 0]), 3)
        self.assertEqual(longest_consecutive([]), 0)
        self.assertEqual(longest_consecutive([1] * 200 + list(range(1, 200))), 199)
        with self.assertRaises(ValueError):
            longest_consecutive(["1"])

    def test_sorted_oracle(self):
        for n in range(6):
            for nums in itertools.product(range(-2, 3), repeat=n):
                ordered = sorted(set(nums))
                best = run = 0
                previous = None
                for value in ordered:
                    run = run + 1 if previous is not None and value == previous + 1 else 1
                    best = max(best, run)
                    previous = value
                self.assertEqual(longest_consecutive(nums), best)

if __name__ == "__main__":
    unittest.main()