Part II · Ordering and Rearranging Pattern 5 4 problems

Cyclic Sort

When the values are a permutation of a known range, the array is its own index. Put every value in its home slot and the anomalies name themselves.

This is the narrowest pattern on the site and the easiest to spot once you know it. It applies only when the numbers come from a bounded range like 1..n, but when it applies it beats sorting, beats hashing, and gives O(n) time with O(1) space.

Contents

  1. When to use
  2. Core idea
  3. Why it is linear
  4. The template
  5. Common mistakes
  6. Missing Number
  7. Find All Numbers Disappeared in an Array
  8. Find the Duplicate Number
  9. First Missing Positive
  10. Recap

When to use

The trigger. The array holds n numbers drawn from a bounded range tied to n, usually 1..n or 0..n, and the question asks for the missing, duplicated, corrupted, or smallest absent value. Then add the constraint that seals it: O(n) time and O(1) space.
SignalWhat it looks like
Range tied to length“n distinct numbers in the range [0, n]”, “values are in 1 to n”, “a permutation of 1..n”.
Anomaly hunting“find the missing one”, “find all missing”, “find the duplicate”, “the smallest positive not present”.
Space constraint“O(1) extra space”, which forbids the obvious set or count array.
The disqualifier: “do not modify the input”. Cyclic sort works by swapping, so a read-only constraint rules it out. That is precisely when Find the Duplicate Number switches to Floyd’s cycle detection. Both patterns claim that problem, and the constraint decides which one is intended.

Core idea

If the values are 1..n, then value v has an obvious home: index v - 1. Walk the array. If the value at i is not home, swap it to its home. Repeat at the same i until the value sitting there is already home, then move on. When the pass finishes, every index that does not hold its own value is an anomaly, and it points straight at the answer.
idx 0idx 1 idx 2idx 3 start 3 1 4 2 3 belongs at index 2, swap then 4 1 3 2 4 belongs at index 3, swap, and so on done 1 2 3 4 nums[i] == i + 1 everywhere
Figure 5.1 — Each swap is a jump along a permutation cycle. The name of the pattern comes from those cycles.

The post-condition is the whole answer

Once the placement pass is done, one linear scan reads off the result. What you look for depends on the question.

QuestionAfter placement, look for
Which single value is missing?The one index i where nums[i] != i (or != i + 1).
Which values are all missing?Every such index, collected.
Which value is duplicated?The value found sitting at a wrong index.
Smallest missing positive?The first such index, converted back to a value.

Why it is linear

The loop has a while that sometimes does not advance i, so linearity needs an argument. It is short and interviewers like it.

Amortised argument. Every swap moves at least one value into its permanent home, and a value that is home is never moved again. There are n values, so there are at most n swaps in the whole run. Separately, i advances at most n times. Total work is at most 2n steps, so O(n).

Put differently: the number of correctly placed values never decreases, and every non-advancing iteration increases it by at least one. The loop cannot spin.

The template

Cyclic sort for values 1..n
def cyclic_sort(nums: list[int]) -> None:
    """Place every value v of 1..n at index v - 1, in place.

    Args:
        nums: A list whose values are in 1..len(nums). Mutated.
    """
    i = 0                                     # 0: start scanning at the first slot

    # i is the slot we are fixing. Invariant: slots 0..i-1 are settled
    # (each holds its own value, or a duplicate whose home is taken).
    while i < len(nums):
        # - 1: values are 1-based, indices are 0-based. Value v lives at v - 1.
        home = nums[i] - 1                    # where nums[i] wants to live

        # If home already holds the same value, a swap would loop forever.
        if nums[i] != nums[home]:             # compare VALUES, not indices
            # Each swap puts one value in its final slot, so at most n swaps.
            # Do not advance i: a new value just landed here and needs a home.
            nums[i], nums[home] = nums[home], nums[i]
        else:
            i += 1                            # settled, or a duplicate. Move on

For a 0..n range, drop the - 1: home = nums[i]. That is the only change.

value at home differs: swap, i stays value at home is equal: advance i before 3 1 2 home of 3 is index 2, which holds 2 after 2 1 3 i is still 0: the new 2 needs a home too before 2 2 index 1 already holds a 2 after 2 2 no swap, i moves to 1. No endless loop
Figure 5.2 — The loop only advances i when a swap would gain nothing, which is what stops it looping on duplicates.

Reading the figure. The amber box is slot i. Green means the value sits at its home, index v - 1. Red means it does not. The purple arrow is the swap, from slot i to the home of its value. On the left, a swap brings in a new value, so i stays put. On the right, the home already holds a 2, so the loop gives up on this copy and moves on.

Why nums[i] != nums[home] and not i != home. With duplicates the two differ. Take [2, 2]: at i = 0 the home is index 1, so i != home is true and you swap, getting [2, 2] again, forever. Comparing values instead notices that the destination already holds this value, so there is nothing to gain, and advances. This one line is the most common bug in the pattern.

Common mistakes

The problems

1. Missing Number Easy

Problem

An array nums holds n distinct numbers taken from the range 0..n. Exactly one number in that range is absent. Return it.

Approach

Solution

def missing_number(nums: list[int]) -> int:
    """The one value of 0..n absent from nums.

    Args:
        nums: n distinct integers drawn from 0..n. Mutated in place.

    Returns:
        The missing value.

    Example:
        >>> missing_number([3, 0, 1])
        2
    """
    n = len(nums)                             # n slots, but n + 1 possible values 0..n
    i = 0                                     # 0: start at the first slot

    # Invariant: slots 0..i-1 hold their own value, or hold n (no home).
    while i < n:
        # No - 1 here: values start at 0, so value v lives at index v.
        home = nums[i]                        # 0..n range, so home == value

        # n has no slot to go to, so leave it where it is.
        # home < n guards the index. nums[i] != nums[home] stops a self-swap.
        if home < n and nums[i] != nums[home]:
            nums[i], nums[home] = nums[home], nums[i]   # one value lands for good
        else:
            i += 1                            # this slot is done. Move to the next

    # Every value is now home. The first slot that does not hold its own
    # index is the hole left by the missing value.
    for index in range(n):                    # range(n): check slots 0..n-1
        if nums[index] != index:
            return index

    return n                                  # every slot matched, so n is missing

Walkthrough

nums = [3, 0, 1], n = 3:

inumsaction
0[3, 0, 1]home of 3 is 3, out of range, advance
1[3, 0, 1]home of 0 is 0, swap
1[0, 3, 1]home of 3 out of range, advance
2[0, 3, 1]home of 1 is 1, swap
2[0, 1, 3]home of 3 out of range, advance, done

Scan: index 2 holds 3, mismatch, so the answer is 2.

idx 0 idx 1 idx 2 i = 0 3 0 1 3 has no home (no index 3): advance i = 1 3 0 1 home of 0 is index 0: swap i = 1 0 3 1 3 has no home: advance i = 2 0 3 1 home of 1 is index 1: swap i = 2 0 1 3 3 has no home: advance, done scan 0 1 3 index 2 holds 3, not 2 answer: 2 ↑ missing
Figure 5.3 — After placement, the only slot not holding its own index is slot 2, so 2 is the missing number.

Reading the figure. Each row is the array at one step. The amber box is slot i. Green values sit at home, which is index v here. Grey marks 3, which has no index to live at. The purple arrow sends a value home. Notice that 3 drifts right with each swap and ends in the slot that 2 should own.

TimeO(n)SpaceO(1)

Two shorter answers worth naming

def missing_number_gauss(nums: list[int]) -> int:
    """Subtract the actual sum from the expected sum."""
    n = len(nums)                             # n numbers, so the full range is 0..n
    # n * (n + 1) // 2: Gauss sum of 0..n. // 2 is exact since n * (n + 1) is even.
    return n * (n + 1) // 2 - sum(nums)


def missing_number_xor(nums: list[int]) -> int:
    """XOR every index and value; pairs cancel, the missing one survives."""
    # Start with n: indices only reach n - 1, so n must be added by hand.
    result = len(nums)
    # Invariant: result is the XOR of n and every index and value seen so far.
    for index, value in enumerate(nums):      # enumerate starts at 0: indices 0..n-1
        result ^= index ^ value               # x ^ x == 0, so matched pairs vanish
    return result                             # only the unpaired missing value is left
Both are O(n) time and O(1) space, and neither modifies the input. The Gauss version can overflow in a fixed-width language, which is exactly why the XOR version exists; in Python integers are arbitrary precision so it is not a real risk, but say it anyway, because the interviewer is usually thinking in C++ or Java. Cyclic sort is still the one to know, because it is the version that extends to all the missing numbers and to duplicates.

Edge cases to raise

Say this out loud: “The range is 0 to n but there are only n slots, so one value has no home. That is the value the scan reports, or n itself if every slot matches.”

2. Find All Numbers Disappeared in an Array Easy

Problem

nums has n integers, each in 1..n. Some appear twice and some not at all. Return every value in 1..n that does not appear.

Approach

Solution

def find_disappeared_numbers(nums: list[int]) -> list[int]:
    """Every value of 1..n missing from nums.

    Args:
        nums: n integers in 1..n, with repeats allowed. Mutated in place.

    Returns:
        The absent values, in increasing order.

    Example:
        >>> find_disappeared_numbers([4, 3, 2, 7, 8, 2, 3, 1])
        [5, 6]
    """
    n = len(nums)                             # n slots for values 1..n
    i = 0                                     # 0: start at the first slot

    # Invariant: slots 0..i-1 hold their own value, or a duplicate.
    while i < n:
        home = nums[i] - 1                    # - 1: value v lives at index v - 1

        # Equal values mean home is taken (or this IS home). Swapping would loop.
        if nums[i] != nums[home]:
            nums[i], nums[home] = nums[home], nums[i]   # place one value for good
        else:
            i += 1                            # settled or a duplicate. Move on

    # A slot that does not hold index + 1 is the home of a missing value.
    # + 1 turns the 0-based index back into its 1-based value.
    return [index + 1 for index in range(n) if nums[index] != index + 1]

Walkthrough

[4, 3, 2, 7, 8, 2, 3, 1] becomes [1, 2, 3, 4, 3, 2, 7, 8] after placement. Indices 4 and 5 hold 3 and 2 instead of 5 and 6, so the answer is [5, 6]. Notice the leftover values at those slots are the duplicates, which is the next problem for free.

idx 0 idx 1 idx 2 idx 3 idx 4 idx 5 idx 6 idx 7 start 4 3 2 7 8 2 3 1 placement pass: swap each value to index v - 1 placed 1 2 3 4 3 2 7 8 5 missing ↑ ↑ 6 missing answer: [5, 6]. The leftover values 3 and 2 are the duplicates.
Figure 5.4 — Every slot left holding a stranger names a missing value: slot 4 wants 5 and slot 5 wants 6.

Reading the figure. Green slots hold their own value, v at index v - 1. Red slots hold a value that belongs elsewhere. After placement only two red slots remain. Each one reads off as index + 1, the value that never showed up.

TimeO(n)SpaceO(1) extra

The output list is not counted as extra space, by the usual convention. Say that explicitly.

The sign-marking alternative

def find_disappeared_by_marking(nums: list[int]) -> list[int]:
    """Mark seen values by negating the value at their home index."""
    # value is the next number read. Invariant: for every value seen so far,
    # the number at its home index is negative.
    for value in nums:
        # abs: an earlier pass may have negated this slot already.
        # - 1: value v lives at index v - 1.
        home = abs(value) - 1
        if nums[home] > 0:                    # > 0: not yet marked
            nums[home] = -nums[home]          # negate = "home of v was seen"

    # A slot still > 0 was never marked, so its value i + 1 never appeared.
    # + 1 turns the 0-based index back into its 1-based value.
    return [i + 1 for i, value in enumerate(nums) if value > 0]

Same complexity, and it is the trick to reach for when values are positive and you want a one-pass mark. It destroys the values but keeps their magnitudes, so it is recoverable. Worth knowing as the sibling technique.

Edge cases to raise

Say this out loud: “After placement, a slot holding the wrong value means its owner never appeared. The wrong value sitting there is a duplicate, so this one pass answers both the missing and the duplicated question.”

3. Find the Duplicate Number Medium

Problem

nums has n + 1 integers, each in 1..n. Exactly one value is repeated, possibly many times. Return it.

Read the constraints before choosing. If the problem says do not modify the array, this solution is disqualified and you must use Floyd’s cycle detection. If modification is allowed, cyclic sort is simpler and easier to explain. Stating both, and why the constraint picks between them, is the strongest possible answer.

Approach

Solution

def find_duplicate(nums: list[int]) -> int:
    """The one repeated value in n + 1 integers drawn from 1..n.

    Mutates nums. If the array must stay read-only, use Floyd's cycle
    detection on the index graph instead.

    Args:
        nums: n + 1 integers in 1..n with exactly one repeated value.

    Returns:
        The repeated value.

    Raises:
        ValueError: If no duplicate is present.

    Example:
        >>> find_duplicate([1, 3, 4, 2, 2])
        2
    """
    i = 0                                     # 0: start at the first slot

    # Invariant: slots 0..i-1 hold their own value, or the duplicate.
    while i < len(nums):
        home = nums[i] - 1                    # - 1: value v lives at index v - 1

        # Equal values: home is taken, so nums[i] is settled or the duplicate.
        if nums[i] != nums[home]:
            nums[i], nums[home] = nums[home], nums[i]   # place one value for good
        else:
            i += 1                            # nothing to swap. Move on

    # Exactly one slot is left holding a value that is not its own.
    for index, value in enumerate(nums):      # enumerate from 0: 0-based indices
        if value != index + 1:                # + 1: slot index should hold index + 1
            return value                      # the extra copy that found no home

    raise ValueError("no duplicate found")

Walkthrough

[1, 3, 4, 2, 2] settles to [1, 2, 3, 4, 2]. Index 4 should hold 5, which is out of range and never existed, and instead holds 2. That is the duplicate.

idx 0 idx 1 idx 2 idx 3 idx 4 i = 0 1 3 4 2 2 1 is home: advance i = 1 1 3 4 2 2 3 goes to index 2 i = 1 1 4 3 2 2 4 goes to index 3 i = 1..3 1 2 3 4 2 1, 2, 3, 4 home: advance i = 4 1 2 3 4 2 index 1 already holds 2 ↑ duplicate: 2
Figure 5.5 — With five slots and only four homes, one value cannot get home, and that value is the duplicate.

Reading the figure. The amber box is slot i. Green values are home, red values are not. The purple arrow sends a value to index v - 1. In the last row the 2 at index 4 tries to go home, finds a 2 already there, and stays. It is the only red cell, so it is the answer.

TimeO(n)SpaceO(1)Mutatesyes

The three accepted solutions, ranked

SolutionTimeSpaceModifiesUse when
Cyclic sort placementO(n)O(1)yesMutation is allowed. Easiest to explain.
Floyd on the index graphO(n)O(1)noThe array is read-only. The intended answer on LeetCode.
Binary search on the value rangeO(n log n)O(1)noYou want a read-only answer you can derive under pressure.

Edge cases to raise

Say this out loud: “n plus one slots, n possible homes, so by pigeonhole some slot ends up wrong, and the value sitting in it is the duplicate. If I am not allowed to write, I switch to Floyd.”

4. First Missing Positive Hard

Problem

Given an unsorted array of integers, find the smallest positive integer that is not present. It must run in O(n) time and use O(1) extra space. Values may be negative, zero, or far larger than n.

The key observation

With n slots, the answer must lie in 1 .. n + 1. If the array happened to contain exactly 1, 2, …, n, the answer would be n + 1. Any other case leaves a gap below that. So every value outside 1..n is irrelevant: negatives, zeros, and anything above n can be ignored entirely. That single sentence turns an unbounded problem into a cyclic-sort problem.

Approach

Solution

def first_missing_positive(nums: list[int]) -> int:
    """Smallest positive integer absent from nums, in O(n) time, O(1) space.

    Args:
        nums: Any integers, in any order. Mutated in place.

    Returns:
        The smallest positive integer not present. Always in 1..len(nums) + 1.

    Example:
        >>> first_missing_positive([3, 4, -1, 1])
        2
    """
    n = len(nums)                             # n slots: only values 1..n can matter
    i = 0                                     # 0: start at the first slot

    # Invariant: slots 0..i-1 hold their own value, or junk that has no home.
    while i < n:
        home = nums[i] - 1                    # - 1: value v lives at index v - 1

        # Only values in 1..n have a home. The bounds check is what makes
        # this safe for negatives, zeros and huge values.
        # 0 <= home < n is the same as 1 <= value <= n.
        if 0 <= home < n and nums[i] != nums[home]:
            nums[i], nums[home] = nums[home], nums[i]   # place one value for good
        else:
            i += 1                            # junk, duplicate, or settled. Move on

    # The first slot not holding index + 1 names the answer.
    for index in range(n):                    # range(n): check slots 0..n-1
        if nums[index] != index + 1:          # + 1: slot index should hold index + 1
            return index + 1                  # + 1: back to the 1-based value

    return n + 1                              # 1..n all present, so n + 1 is missing

Walkthrough

nums = [3, 4, -1, 1], n = 4:

inumsaction
0[3, 4, -1, 1]3 home is 2, swap with -1
0[-1, 4, 3, 1]-1 has no home, advance
1[-1, 4, 3, 1]4 home is 3, swap with 1
1[-1, 1, 3, 4]1 home is 0, swap with -1
1[1, -1, 3, 4]-1 has no home, advance
2, 3[1, -1, 3, 4]3 and 4 are already home, advance

Scan: index 0 holds 1, fine. Index 1 holds -1, not 2. Answer 2.

idx 0 idx 1 idx 2 idx 3 i = 0 3 4 -1 1 3 goes to index 2, swap with -1 i = 0 -1 4 3 1 -1 has no home: advance i = 1 -1 4 3 1 4 goes to index 3, swap with 1 i = 1 -1 1 3 4 1 goes to index 0, swap with -1 i = 1 1 -1 3 4 -1 has no home: advance i = 2, 3 1 -1 3 4 3 and 4 are home: advance scan 1 -1 3 4 first slot that fails: index 1 ↑ wants 2 answer: 2
Figure 5.6 — Out-of-range values are parked anywhere. The first slot not holding index + 1 gives the answer.

Reading the figure. The amber box is slot i. Green values are home at index v - 1. Grey marks -1, which has no home, so the bounds check skips it. The purple arrow sends a value home. In the scan row, red marks the first slot that does not hold index + 1. Its index plus one is the smallest missing positive.

TimeO(n)SpaceO(1)

Why the bounds check must come first

Without 0 <= home, a value of -5 gives home = -6, and Python happily indexes from the end of the list, corrupting the array silently rather than raising. That is a bug you will not see in a small test. Write the bounds check before the value comparison, and say why.

Edge cases to raise

Say this out loud: “With n slots the answer is somewhere in 1 to n plus one, so anything outside that range is noise. That reduces an unbounded question to a bounded one, and then it is cyclic sort.”

Recap

The six things to carry forward

Where this goes next

Pattern 6, In-Place Reversal of a Linked List, is the last of the pointer-surgery patterns. Same spirit, no extra memory, but the state you carry is three pointers instead of an index.


← 4 — Merge Intervals 6 — In-Place Reversal of a Linked List →