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

    while i < len(nums):
        home = nums[i] - 1                    # where nums[i] wants to live

        if nums[i] != nums[home]:             # compare VALUES, not indices
            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.

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)
    i = 0

    while i < n:
        home = nums[i]                        # 0..n range, so home == value

        # n has no slot to go to, so leave it where it is.
        if home < n and nums[i] != nums[home]:
            nums[i], nums[home] = nums[home], nums[i]
        else:
            i += 1

    for index in range(n):
        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.

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)
    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."""
    result = len(nums)
    for index, value in enumerate(nums):
        result ^= index ^ value
    return result
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)
    i = 0

    while i < n:
        home = nums[i] - 1

        if nums[i] != nums[home]:
            nums[i], nums[home] = nums[home], nums[i]
        else:
            i += 1

    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.

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."""
    for value in nums:
        home = abs(value) - 1
        if nums[home] > 0:
            nums[home] = -nums[home]

    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

    while i < len(nums):
        home = nums[i] - 1

        if nums[i] != nums[home]:
            nums[i], nums[home] = nums[home], nums[i]
        else:
            i += 1

    # Exactly one slot is left holding a value that is not its own.
    for index, value in enumerate(nums):
        if value != index + 1:
            return value

    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.

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)
    i = 0

    while i < n:
        home = nums[i] - 1

        # Only values in 1..n have a home. The bounds check is what makes
        # this safe for negatives, zeros and huge values.
        if 0 <= home < n and nums[i] != nums[home]:
            nums[i], nums[home] = nums[home], nums[i]
        else:
            i += 1

    for index in range(n):
        if nums[index] != index + 1:
            return index + 1

    return n + 1

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.

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