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. Then the odd ones out name themselves.

This is the narrowest pattern on the site. It is also the easiest to spot once you know it. It only applies when the numbers come from a bounded range like 1..n. When it applies, it beats sorting and beats hashing. You get O(n) time with O(1) space, on a plain int[].

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 from a bounded range tied to n, usually 1..n or 0..n. The question asks for the missing, duplicated, corrupted, or smallest absent value. Then comes the constraint that seals it: O(n) time and O(1) space.
The disqualifier: “do not modify the input”. Cyclic sort works by swapping, so a read-only rule kills it. In C# the signature often tells you: a parameter of type ReadOnlySpan<int> or IReadOnlyList<int> means hands off. That is when Find the Duplicate Number switches to Floyd’s cycle detection. Both patterns claim that problem. The constraint decides which one is meant.

Core idea

If the values are 1..n, 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 there is home, then move on. When the pass ends, every index that does not hold its own value is an anomaly. 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.

Reading the figure. Red cells hold a value that lives somewhere else. Green cells hold their own value. The purple arrow is one swap. Each swap turns at least one cell green for good.

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.

Why it is linear

The loop is a while that sometimes does not advance i. So linear time needs an argument. It is short, and interviewers like it.

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

Put another way: the count of seated values never goes down. Every pass that does not advance i raises it by at least one. The loop cannot spin.

The template

Cyclic sort for values 1..n
public static class CyclicSort
{
    /// <summary>Place every value v of 1..n at index v - 1, in place.</summary>
    /// <param name="nums">Values in 1..nums.Length. Mutated.</param>
    /// <returns>Nothing. The array itself is the result.</returns>
    /// <example><c>Sort([3, 1, 4, 2])</c> leaves the array as [1, 2, 3, 4].</example>
    public static void Sort(int[] nums)
    {
        int 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 < nums.Length)
        {
            // - 1: values are 1-based, indices are 0-based. Value v lives at v - 1.
            int 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 seats one value for good, 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++;                              // settled, or a duplicate. Move on
            }
        }
    }
}

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

The swap line uses a tuple swap. C# reads both right-hand values before it writes either slot. So it is as safe as the three-line version with a temp, and much shorter.

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. You swap and get [2, 2] again, forever. Comparing values sees that the target already holds this value. There is nothing to gain, so it 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 from the range 0..n. Exactly one number in that range is absent. Return it.

Approach

Solution

public static class MissingNumber
{
    /// <summary>The one value of 0..n absent from nums.</summary>
    /// <param name="nums">n distinct integers from 0..n. Mutated in place.</param>
    /// <returns>The missing value.</returns>
    /// <example><c>MissingNumber.Find([3, 0, 1])</c> returns 2.</example>
    public static int Find(int[] nums)
    {
        int n = nums.Length;                      // n slots, but n + 1 values 0..n
        int 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.
            int 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 && nums[i] != nums[home])
            {
                (nums[i], nums[home]) = (nums[home], nums[i]);   // one value lands
            }
            else
            {
                i++;                              // this slot is done. Next one
            }
        }

        // Every value is home now. The first slot that does not hold its own
        // index is the hole left by the missing value.
        for (int index = 0; index < n; index++)   // 0..n-1: every slot once
        {
            if (nums[index] != index)
            {
                return index;
            }
        }

        return n;                                 // every slot matched, so n is missing
    }
}

Walkthrough

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

  1. i = 0, [3, 0, 1]: home of 3 is 3, out of range, advance.
  2. i = 1, [3, 0, 1]: home of 0 is 0, swap.
  3. i = 1, [0, 3, 1]: home of 3 is out of range, advance.
  4. i = 2, [0, 3, 1]: home of 1 is 1, swap.
  5. i = 2, [0, 1, 3]: home of 3 is out of range, advance. Done.

Scan: index 2 holds 3, a 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. It ends in the slot that 2 should own.

TimeO(n)SpaceO(1)

Two shorter answers worth naming

public static class MissingNumberShortcuts
{
    /// <summary>Subtract the actual sum from the expected sum of 0..n.</summary>
    /// <param name="nums">n distinct integers from 0..n. Not modified.</param>
    /// <returns>The missing value.</returns>
    /// <example><c>MissingNumberShortcuts.Gauss([3, 0, 1])</c> returns 2.</example>
    public static int Gauss(int[] nums)
    {
        long n = nums.Length;                     // long: n * (n + 1) overflows int fast
        // n * (n + 1) / 2: Gauss sum of 0..n. / 2 is exact since n * (n + 1) is even.
        long expected = n * (n + 1) / 2;
        long actual = 0;                          // 0: empty sum. long for the same reason

        // value is the next number. Invariant: actual is the sum of all seen so far.
        foreach (int value in nums)
        {
            actual += value;
        }

        return (int)(expected - actual);          // the gap fits in int: it is in 0..n
    }

    /// <summary>XOR every index and value. Pairs cancel, the missing one survives.</summary>
    /// <param name="nums">n distinct integers from 0..n. Not modified.</param>
    /// <returns>The missing value.</returns>
    /// <example><c>MissingNumberShortcuts.Xor([3, 0, 1])</c> returns 2.</example>
    public static int Xor(int[] nums)
    {
        // Start with n: indices only reach n - 1, so n must be added by hand.
        int result = nums.Length;

        // index runs 0..n-1. Invariant: result is the XOR of n and every
        // index and value seen so far.
        for (int index = 0; index < nums.Length; index++)
        {
            result ^= index ^ nums[index];        // x ^ x == 0, so matched pairs vanish
        }

        return result;                            // only the unpaired value is left
    }
}
Both are O(n) time and O(1) space. Neither changes the input. In C# the Gauss version is a real overflow risk. An int is 32 bits, and n * (n + 1) passes int.MaxValue once n is about 46,341. By default C# wraps around silently, with no exception. So do the math in long, or wrap it in checked(...) to get an OverflowException. The XOR version has no overflow at all, which is why it exists. Cyclic sort is still the one to know. 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. The scan reports the hole, 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

public static class DisappearedNumbers
{
    /// <summary>Every value of 1..n missing from nums.</summary>
    /// <param name="nums">n integers in 1..n, repeats allowed. Mutated in place.</param>
    /// <returns>The absent values, in increasing order.</returns>
    /// <example><c>Find([4, 3, 2, 7, 8, 2, 3, 1])</c> returns [5, 6].</example>
    public static List<int> Find(int[] nums)
    {
        int n = nums.Length;                      // n slots for values 1..n
        int i = 0;                                // 0: start at the first slot

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

            // Equal values mean home is taken, or this IS home. A swap would loop.
            if (nums[i] != nums[home])
            {
                (nums[i], nums[home]) = (nums[home], nums[i]);   // seat one value
            }
            else
            {
                i++;                              // settled or a duplicate. Move on
            }
        }

        // A slot that does not hold index + 1 is the home of a missing value.
        List<int> missing = [];
        for (int index = 0; index < n; index++)   // 0..n-1: scan every slot once
        {
            if (nums[index] != index + 1)         // + 1: slot index should hold index + 1
            {
                missing.Add(index + 1);           // + 1: back to the 1-based value
            }
        }

        return missing;
    }
}

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]. The leftover values in those slots are the duplicates. That 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

By the usual rule, the output list does not count as extra space. Say that out loud.

The sign-marking alternative

public static class DisappearedByMarking
{
    /// <summary>Mark seen values by negating the number at their home index.</summary>
    /// <param name="nums">n integers in 1..n, repeats allowed. Signs are changed.</param>
    /// <returns>The absent values, in increasing order.</returns>
    /// <example><c>DisappearedByMarking.Find([1, 1, 2])</c> returns [3].</example>
    public static List<int> Find(int[] nums)
    {
        // index is the slot we read. Invariant: for every value read so far,
        // the number at its home index is negative.
        for (int index = 0; index < nums.Length; index++)
        {
            // Math.Abs: an earlier pass may have negated this slot already.
            // - 1: value v lives at index v - 1.
            int home = Math.Abs(nums[index]) - 1;
            if (nums[home] > 0)                   // > 0: not marked yet
            {
                nums[home] = -nums[home];         // negate = "home of v was seen"
            }
        }

        // A slot still > 0 was never marked, so its value index + 1 never appeared.
        List<int> missing = [];
        for (int index = 0; index < nums.Length; index++)   // 0..n-1: every slot
        {
            if (nums[index] > 0)                  // > 0: never marked
            {
                missing.Add(index + 1);           // + 1: back to the 1-based value
            }
        }

        return missing;
    }
}

Same cost. Reach for it when the values are positive and you want a one-pass mark. It flips signs but keeps sizes, so you can undo it with Math.Abs. Know it as the sibling trick. A LINQ one-liner like Enumerable.Range(1, n).Except(nums) also works. But it builds a hash set inside, so it is O(n) extra space.

Edge cases to raise

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

3. Find the Duplicate Number Medium

Problem

nums has n + 1 integers, each in 1..n. Exactly one value repeats, maybe many times. Return it.

Read the rules before you choose. If the problem says do not modify the array, this solution is out. Use Floyd’s cycle detection instead. If changes are allowed, cyclic sort is simpler and easier to explain. The strongest answer states both and says why the rule picks one.

Approach

Solution

public static class FindDuplicate
{
    /// <summary>
    /// The one repeated value in n + 1 integers from 1..n. Mutates nums.
    /// If the array must stay read-only, use Floyd's cycle detection instead.
    /// </summary>
    /// <param name="nums">n + 1 integers in 1..n with one repeated value.</param>
    /// <returns>The repeated value.</returns>
    /// <exception cref="ArgumentException">No duplicate is present.</exception>
    /// <example><c>FindDuplicate.Find([1, 3, 4, 2, 2])</c> returns 2.</example>
    public static int Find(int[] nums)
    {
        int i = 0;                                // 0: start at the first slot

        // Invariant: slots 0..i-1 hold their own value, or the duplicate.
        while (i < nums.Length)
        {
            int 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]);   // seat one value
            }
            else
            {
                i++;                              // nothing to swap. Move on
            }
        }

        // Exactly one slot is left holding a value that is not its own.
        for (int index = 0; index < nums.Length; index++)   // 0-based slots
        {
            if (nums[index] != index + 1)         // + 1: slot index should hold index + 1
            {
                return nums[index];               // the extra copy that found no home
            }
        }

        throw new ArgumentException("no duplicate found", nameof(nums));
    }
}

Walkthrough

[1, 3, 4, 2, 2] settles to [1, 2, 3, 4, 2]. Index 4 should hold 5. That value is out of range and never existed. Instead it holds 2, which 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. It finds a 2 already there, so it stays. It is the only red cell, so it is the answer.

TimeO(n)SpaceO(1)Mutatesyes

The three accepted solutions, ranked

Edge cases to raise

Say this out loud: “n plus one slots, n homes, so by pigeonhole some slot ends up wrong. The value in it is the duplicate. If I may not write to the array, 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 with 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 held exactly 1, 2, …, n, the answer would be n + 1. Any other case leaves a gap below that. So every value outside 1..n does not matter. Negatives, zeros, and anything above n can be ignored. That one sentence turns an unbounded problem into a cyclic-sort problem.

Approach

Solution

public static class FirstMissingPositive
{
    /// <summary>Smallest positive integer absent from nums, O(n) time, O(1) space.</summary>
    /// <param name="nums">Any integers, in any order. Mutated in place.</param>
    /// <returns>The smallest missing positive. Always in 1..nums.Length + 1.</returns>
    /// <example><c>FirstMissingPositive.Find([3, 4, -1, 1])</c> returns 2.</example>
    public static int Find(int[] nums)
    {
        int n = nums.Length;                      // n slots: only values 1..n can matter
        int 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)
        {
            // - 1: value v lives at index v - 1. No overflow: int.MinValue - 1 would
            // wrap, but a home that negative fails the bounds check either way.
            int home = nums[i] - 1;

            // Only values in 1..n have a home. The bounds check makes this safe
            // for negatives, zeros and huge values. It must come first.
            // 0 <= home && home < n is the same as 1 <= value <= n.
            if (0 <= home && home < n && nums[i] != nums[home])
            {
                (nums[i], nums[home]) = (nums[home], nums[i]);   // seat one value
            }
            else
            {
                i++;                              // junk, duplicate, or settled. Next
            }
        }

        // The first slot not holding index + 1 names the answer.
        for (int index = 0; index < n; index++)   // 0..n-1: check every slot
        {
            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
    }
}

One honest wrinkle: int.MinValue - 1 wraps to int.MaxValue in C#. That huge home still fails home < n, so the code stays correct. In a checked context it would throw instead. If an interviewer asks, say you would compare the value with nums[i] >= 1 && nums[i] <= n to avoid the subtraction.

Walkthrough

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

  1. i = 0, [3, 4, -1, 1]: home of 3 is 2, swap with -1.
  2. i = 0, [-1, 4, 3, 1]: -1 has no home, advance.
  3. i = 1, [-1, 4, 3, 1]: home of 4 is 3, swap with 1.
  4. i = 1, [-1, 1, 3, 4]: home of 1 is 0, swap with -1.
  5. i = 1, [1, -1, 3, 4]: -1 has no home, advance.
  6. i = 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. In C#, nums[-6] throws IndexOutOfRangeException. That is louder than Python, which quietly reads from the end of the list. But a crash is still a failed test. The && operator short-circuits, so put the bounds checks to the left of nums[home]. Then the array read never runs for a bad index. Say why out loud.

A common C# speed trick folds both bounds into one compare. Casting to uint turns any negative into a huge number:

// (uint)home < (uint)n is true only for 0 <= home < n.
// A negative home becomes a value above 2^31, which is never < n.
if ((uint)home < (uint)n && nums[i] != nums[home]) { /* swap */ }

The BCL uses this trick inside List<T>. In an interview the two plain compares read better. Mention the trick only if asked about speed.

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 turns an unbounded question into a bounded one. 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 and no extra memory. But the state you carry is three references instead of an index.


← 04 — Merge Intervals 06 — In-Place Reversal of a Linked List →