Learn/DSA Patterns
DSA PatternsArrays & Two Pointerseasy14 min read

Cyclic Sort

When numbers live in the range 1 to N, every number already knows its home. We exploit that to sort in O(n) with no extra memory — and then find missing or duplicate numbers as a free bonus.

#cyclic-sort#arrays#in-place#missing-number#duplicates#beginner#interview
Table of contents

Before we start

By the end of this page you will be able to:

  • See exactly why a number in the range 1..N already knows which index it belongs at.
  • Explain out loud why swapping into place always terminates and never loops forever.
  • Recognise this trick in "find missing number," "find duplicate," and "first missing positive" problems.

Stop at every Pause & Think box. Five seconds of genuine thought beats five minutes of passive reading.


Picture this first (no code yet)

A real-life story

Imagine a cloakroom with N numbered hooks on the wall — hook 1, hook 2, hook 3, …, hook N.

A pile of coats is on the floor. Each coat has a tag sewn inside: "I belong on hook 3", or "I belong on hook 7", and so on. Every tag number is between 1 and N.

A new attendant arrives. She picks up the coat on top of the pile. Reads the tag — say it says hook 3. She walks to hook 3. If it's free, she hangs the coat. Done.

But what if hook 3 already has a coat? She doesn't just drop the coat on the floor. She swaps — takes the coat that was on hook 3, brings it back, hangs the new coat in its correct spot, and now deals with the coat she just picked up. She repeats until the coat in her hands happens to already be on the right hook.

Eventually every coat is on its correct hook. She never walked more than N steps total — because each swap puts at least one coat in its permanent home.

That attendant's method — read the tag, swap if needed, repeat — is Cyclic Sort.


The actual problem

The simplest form: you are given an array containing every number from 1 to N exactly once, but in scrambled order. Sort it in-place.

input  : [3, 1, 5, 4, 2]
output : [1, 2, 3, 4, 5]

The key gift: the number k belongs at index k - 1 (because arrays are 0-indexed). Number 1 belongs at index 0, number 3 belongs at index 2, etc.

Every number already knows its home.


First, the slow way (so you feel the pain)

The instinctive answer: use a standard comparison sort.

arr.sort()   # O(n log n)

That works, but it ignores the most important fact about the input: the values are in the range 1..N. A general sort treats them as arbitrary numbers and pays O(n log n) for knowledge it could have gotten for free.

Cyclic Sort will do it in O(n) — faster than any comparison sort is theoretically allowed to be — precisely because it exploits the range constraint.


The turning point

Look at the number at position i = 0 in [3, 1, 5, 4, 2]. It is 3. Its correct home is index 3 - 1 = 2.

Pause & think

The number 3 is sitting at index 0 but belongs at index 2. What is the simplest thing you could do right now that moves at least one number to its correct home — and doesn't lose any information?

Swap arr[0] with arr[2]. The 3 goes to its home at index 2. Whatever was at index 2 (5) is now at index 0 — and it still has its tag, so it will eventually find its own home too.

We keep doing this from the same position i until the number currently sitting there is already at its correct index. Only then do we advance i.

This is the cloakroom attendant's method, written as an algorithm.


The one idea to remember

The entire pattern in one sentence

For each position i, if the number there doesn't belong at i, swap it to where it belongs — keep swapping from i until the right number arrives — then advance i.


Watch it happen, frame by frame

Array: [3, 1, 5, 4, 2]. Correct home for value v = index v - 1.

i=0 → value 3, belongs at index 2. arr[2]=53 → swap(0,2)
      [5, 1, 3, 4, 2]    (3 is now home ✓, but 5 is at i=0, still wrong)

i=0 → value 5, belongs at index 4. arr[4]=25 → swap(0,4)
      [2, 1, 3, 4, 5]    (5 is now home ✓, but 2 is at i=0, still wrong)

i=0 → value 2, belongs at index 1. arr[1]=12 → swap(0,1)
      [1, 2, 3, 4, 5]    (2 is now home ✓, and 1 landed at i=0)

i=0 → value 1, belongs at index 0. Already home! ✓ → advance i

i=1 → value 2, belongs at index 1. Already home! ✓ → advance i
i=2 → value 3, belongs at index 2. Already home! ✓ → advance i
i=3 → value 4, belongs at index 3. Already home! ✓ → advance i
i=4 → value 5, belongs at index 4. Already home! ✓ → advance i

Done: [1, 2, 3, 4, 5]

Pause & think

Cover the trace below. Run the algorithm on [2, 3, 1, 5, 4]. Write out each swap and the resulting array. What is the final result?

Check your trace
i=0value 2, home=1. arr[1]=32swap(0,1) → [3, 2, 1, 5, 4]
i=0value 3, home=2. arr[2]=13swap(0,2) → [1, 2, 3, 5, 4]
i=0value 1, home=0. Already home! → advance i
i=1value 2, home=1. Already home! → advance i
i=2value 3, home=2. Already home! → advance i
i=3value 5, home=4. arr[4]=45swap(3,4) → [1, 2, 3, 4, 5]
i=3value 4, home=3. Already home! → advance i
i=4value 5, home=4. Already home! → advance i
Result: [1, 2, 3, 4, 5]

Now, the code — line by line

def cyclic_sort(nums):
    i = 0
    while i < len(nums):
        correct = nums[i] - 1          # where does nums[i] belong?
        if nums[i] != nums[correct]:   # is it already there?
            nums[i], nums[correct] = nums[correct], nums[i]   # swap it home
        else:
            i += 1                     # it's home (or a duplicate) — move on
    return nums

Every line maps to the cloakroom:

  • correct = nums[i] - 1 — read the coat's tag; compute which hook it belongs on.
  • if nums[i] != nums[correct]: — is that hook free (or holding a different coat)? If so, swap.
  • nums[i], nums[correct] = nums[correct], nums[i] — hang this coat on its hook, bring the displaced coat to position i.
  • else: i += 1 — this coat is already on the right hook (or two identical tags — a duplicate). Either way, advance.

Why check nums[i] != nums[correct] instead of i != correct?

If there are duplicates, nums[i] and nums[correct] will both hold the same value. Checking i != correct would cause an infinite swap loop (swapping a value with itself at a different index forever). Checking nums[i] != nums[correct] breaks the loop correctly — "the right value is already at the right position, regardless of where this copy came from."


Why does it always terminate?

Each swap places exactly one new number at its correct index — a position it will never leave. The numbers that are already home are never disturbed (we only swap to correct, never away from a home position).

So after at most n swaps, every position holds the right number and the loop ends.

Total swaps ≤ n. Total iterations ≤ 2n. Time: O(n). Space: O(1).


Why is it so fast?

A comparison sort (quicksort, mergesort) must compare elements to decide their order. Information theory proves you can't sort arbitrary numbers faster than O(n log n) using comparisons.

But we are not sorting arbitrary numbers. We know exactly where each number goes without any comparison — directly from its value. That's free information the problem hands us, and Cyclic Sort spends it perfectly.

n = 100,000
comparison sort  →  ~1,700,000 operations   (n log n)
cyclic sort      →  ~200,000 operations     (2n)
ApproachTimeExtra memory
Comparison sortO(n log n)O(log n) stack
Cyclic SortO(n)O(1)

When should I reach for this? (the trigger list)

Reach for Cyclic Sort when you notice:

  • The array contains numbers in a continuous range — usually 1 to N, sometimes 0 to N.
  • You're asked to find a missing number, duplicate, or misplaced element — in O(n) time and O(1) space.
  • The problem says "the array has N elements containing values in [1, N]" or similar.
  • The brute-force involves a HashSet or sorting — and the interviewer hints "can you do O(1) space?"

If you see those cues, think: "each number knows its home — send it there."


The same trick in four disguises

Disguise 1 — Find the Missing Number (LC #268)

Array has N elements, values in [0, N], one number is missing. Sort cyclically (adjust for 0-indexed: value v belongs at index v). Then scan for the first position where nums[i] != i.

def missingNumber(nums):
    n = len(nums)
    i = 0
    while i < n:
        correct = nums[i]                        # value v belongs at index v
        if correct < n and nums[i] != nums[correct]:
            nums[i], nums[correct] = nums[correct], nums[i]
        else:
            i += 1

    for i in range(n):
        if nums[i] != i:
            return i
    return n                                     # missing number is N itself

Same skeleton — place each number at its correct index, then scan for the anomaly.

Disguise 2 — Find All Duplicates (LC #442)

Array has N elements, values in [1, N], some appear twice. After cyclic sort, any index where nums[i] != i + 1 reveals a duplicate (the number that should be there was pushed out by the impostor).

def findDuplicates(nums):
    i = 0
    while i < len(nums):
        correct = nums[i] - 1
        if nums[i] != nums[correct]:
            nums[i], nums[correct] = nums[correct], nums[i]
        else:
            i += 1

    duplicates = []
    for i in range(len(nums)):
        if nums[i] != i + 1:
            duplicates.append(nums[i])           # the value here is the duplicate
    return duplicates

Disguise 3 — First Missing Positive (LC #41)

The "boss fight" of this pattern. Values can be outside [1, N] — ignore those during sorting.

def firstMissingPositive(nums):
    n = len(nums)
    i = 0
    while i < n:
        correct = nums[i] - 1
        if 0 < nums[i] <= n and nums[i] != nums[correct]:
            nums[i], nums[correct] = nums[correct], nums[i]   # only sort values in range
        else:
            i += 1

    for i in range(n):
        if nums[i] != i + 1:
            return i + 1                         # first gap in the positive sequence
    return n + 1

Same three-phase structure: sort what you can → scan for the first anomaly → return it.

Level up — Find the Corrupt Pair (missing + duplicate)

Array [1,N] but one number is replaced by another, so one value appears twice and one is missing. After cyclic sort, the index where nums[i] != i + 1 reveals both: nums[i] is the duplicate; i + 1 is the missing.

def findCorruptPair(nums):
    i = 0
    while i < len(nums):
        correct = nums[i] - 1
        if nums[i] != nums[correct]:
            nums[i], nums[correct] = nums[correct], nums[i]
        else:
            i += 1
    for i in range(len(nums)):
        if nums[i] != i + 1:
            return [nums[i], i + 1]   # [duplicate, missing]

Traps that catch beginners

Watch out for these

  • Checking i != correct instead of nums[i] != nums[correct]. When duplicates exist, two different positions can hold the same value. i != correct would keep swapping them forever. Always compare the values, not the indices.
  • Using while i < n but incrementing i inside the swap branch. Don't! Only increment when the current element is home. The swap branch must stay at position i and re-evaluate.
  • Forgetting to handle out-of-range values (First Missing Positive). Values ≤ 0 or > n don't belong anywhere in [1,N] — they must be skipped (just do else: i += 1), not swapped to correct (which would be a negative or out-of-bounds index).
  • Off-by-one between 0-indexed and 1-indexed. Value v belongs at index v - 1 for the [1,N] variant. Value v belongs at index v for the [0,N] variant. Check the range before writing correct.
BugFix
Infinite loop with duplicatesCheck nums[i] != nums[correct], not i != correct
Advancing i after a swapOnly advance i in the else branch (when element is home)
Index error on out-of-range valuesGuard with 0 < nums[i] <= n before computing correct

Say it like a pro (interview one-liner)

"Since the values are in [1, N], each number implicitly knows its correct index — value v belongs at index v−1. I'll use Cyclic Sort: walk through the array, and for each position that holds the wrong value, swap it directly to its correct index. Each swap places at least one number permanently, so the total work is O(n) with O(1) space. After sorting, a single scan reveals any missing or duplicate."


Remember this forever

Cyclic Sort

For each position i, if nums[i] isn't home (home = index nums[i]-1), swap it there. Repeat at the same i until the right number arrives. Then advance.


Trigger: values in range [1,N] (or [0,N]) + find missing/duplicate in O(1) space

Key guard: compare nums[i] != nums[correct] (not i != correct) to handle duplicates

Cost: O(n) time, O(1) space

Skeleton:

while i < n:
    correct = nums[i] - 1
    if nums[i] != nums[correct]: swap(i, correct)
    else: i += 1

Check yourself

Why can't we just check `i != correct` to decide whether to swap?

When duplicates exist, two different indices can hold the same value. Checking i != correct would keep swapping those two positions forever — a and b both hold 3, so correct always points to the other, and the loop never terminates. Checking nums[i] != nums[correct] correctly identifies "the right value is already at index correct" and breaks the cycle.

Why do we stay at position `i` after a swap instead of advancing?

The swap brings a new value to position i — the coat we just displaced. That new value might also be at the wrong index. We must handle it before moving on. We only advance when the number at i is definitively home.

How does a single scan after sorting reveal the missing number?

After cyclic sort, a correctly sorted array would have nums[i] == i + 1 for every position. The missing number is the one that never arrived at its correct index — leaving nums[i] != i + 1 at that spot. The first such position i reveals that i + 1 is missing.

What changes for First Missing Positive vs. the basic version?

The input may contain values outside [1, N] (negatives, zeros, numbers bigger than N). We can't sort those — they have no valid home in the array. The guard 0 < nums[i] <= n skips them safely (goes to else: i += 1) so we only swap values that actually belong somewhere.


Practice problems

ProblemDifficultyWhat to noticeLink
Missing NumberEasyRange [0,N]; home index = value itselfLC #268
Find All Numbers Disappeared in an ArrayEasyMultiple missing; scan after sortLC #448
Find All Duplicates in an ArrayMediumIndex where nums[i]≠i+1 holds the duplicateLC #442
Set MismatchEasyBoth missing and duplicate from one scanLC #645
First Missing PositiveHardGuard out-of-range values; same three-phase structureLC #41

When you can solve First Missing Positive from memory and explain why the guard exists, you've fully learned this pattern.

Next up: Matrix Spiral Traversal — where we peel a 2D grid layer by layer like an onion.