← All Posts
DSA Series · Arrays · Cyclic Sort · Part 1

Cyclic Sort

The Core Idea

Cyclic sort applies whenever an array contains numbers drawn from a known contiguous range, typically [1, n] or [0, n-1], where n is the array length (or close to it). The key insight is simple: every number has exactly one correct index, so you can place each value where it belongs by swapping.

Think of a shelf of numbered books that have been knocked out of order. Book 3 belongs in slot 3, Book 7 belongs in slot 7. You pick up the book in front of you, look at its number, and swap it directly into its correct slot. You keep doing this until the book in front of you is already correct, then move on. One pass through the shelf and every book is home.

The mapping from value to index:

For values in [1, n]: value v belongs at index v - 1
For values in [0, n-1]: value v belongs at index v

After placing every number at its correct index, any position where the value does not match the expected number reveals a missing or duplicate value. This is the foundation for an entire family of interview problems.

The Algorithm

The cyclic sort algorithm uses a single pointer i that walks from left to right. At each position:

  1. Compute the correct index for arr[i]. For [1, n] values, that is arr[i] - 1.
  2. If arr[i] is not at its correct index, swap arr[i] with arr[correctIndex].
  3. After the swap, arr[i] has a new value. Repeat from step 1 without advancing i.
  4. When arr[i] is already in the right place (or is out of range, or is a duplicate), advance i++.
i = 0
while i < n:
  correct = arr[i] - 1
  if arr[i] != arr[correct]:
    swap(arr[i], arr[correct])
  else:
    i++

Why O(n)? Each swap places at least one element at its correct position, and a correctly placed element is never moved again. So there are at most n swaps total across the entire algorithm, giving $O(n)$ time regardless of how many inner swaps happen at any single index.

Step-Through Animation

Watch cyclic sort in action on the array [3, 5, 2, 1, 6, 4] with values in [1..6]. At each step, the algorithm checks whether arr[i] is at its correct position. If not, it swaps the value to where it belongs. Cells turn green once correctly placed. Use the Step button to advance one operation at a time.

Cyclic Sort: values [1..6]

Manual Walkthrough

Let us trace cyclic sort on [3, 5, 2, 1, 6, 4]. Values are in [1..6], so value v belongs at index v - 1.

i = 0, arr[0] = 3

Value 3 belongs at index 2. arr[0] != arr[2], so swap them.

3 5 2 1 6 4 0 1 2 3 4 5 i

After swap: [2, 5, 3, 1, 6, 4]. Value 3 is now at index 2 (correct). Stay at i = 0.

i = 0, arr[0] = 2

Value 2 belongs at index 1. arr[0] != arr[1], so swap.

2 5 3 1 6 4 0 1 2 3 4 5 i

After swap: [5, 2, 3, 1, 6, 4]. Value 2 is now at index 1 (correct). Stay at i = 0.

i = 0, arr[0] = 5

Value 5 belongs at index 4. Swap arr[0] and arr[4].

After swap: [6, 2, 3, 1, 5, 4]. Value 5 at index 4 (correct). Stay at i = 0.

i = 0, arr[0] = 6

Value 6 belongs at index 5. Swap arr[0] and arr[5].

After swap: [4, 2, 3, 1, 5, 6]. Value 6 at index 5 (correct). Stay at i = 0.

i = 0, arr[0] = 4

Value 4 belongs at index 3. Swap arr[0] and arr[3].

After swap: [1, 2, 3, 4, 5, 6]. Both values land correctly. Stay at i = 0.

i = 0, arr[0] = 1

Value 1 belongs at index 0. Already correct. Advance i to 1.

Indices 1 through 5 are all correct too. The array is sorted: [1, 2, 3, 4, 5, 6].

Observation: All the real work happened while i was stuck at index 0. Five swaps placed five elements. Then i walked from 0 to 5 with no more swaps. Total operations: 5 swaps + 6 advances = 11 operations for a 6-element array. Always $O(n)$.

C++ Implementation

The base algorithm is remarkably short. The inner loop keeps swapping until arr[i] lands in its correct slot:

#include <vector>
#include <algorithm>

// Cyclic sort for array with values in [1, n]
void cyclicSort(std::vector<int>& arr) {
    int n = arr.size();
    int i = 0;
    while (i < n) {
        int correct = arr[i] - 1;
        if (arr[i] >= 1 && arr[i] <= n
            && arr[i] != arr[correct]) {
            std::swap(arr[i], arr[correct]);
        } else {
            i++;
        }
    }
}

// After sorting: arr[i] == i + 1 for all valid positions
Infinite loop guard: The condition arr[i] != arr[correct] (not arr[i] != correct + 1) is critical when duplicates exist. If two copies of the same number face each other, we must advance i to avoid swapping them forever.

Find the Missing Number

Problem: Given an array of n distinct numbers in the range [0, n], find the one number that is missing.

Place each number at its correct index (value v goes to index v for the [0, n] variant). After sorting, scan to find the index where arr[i] != i.

Visual Walkthrough

Array: [3, 0, 1]. Range is [0, 3], so one number is missing.

Find the Missing Number

int missingNumber(std::vector<int>& nums) {
    int n = nums.size();
    int i = 0;
    while (i < n) {
        // Value v belongs at index v (for [0, n] range)
        if (nums[i] < n && nums[i] != nums[nums[i]]) {
            std::swap(nums[i], nums[nums[i]]);
        } else {
            i++;
        }
    }
    for (int i = 0; i < n; i++) {
        if (nums[i] != i) return i;
    }
    return n;
}

// Example: [3, 0, 1] => missing = 2
// After sort: [0, 1, 3]. Index 2 has value 3, not 2.
ApproachTimeSpace
Cyclic sortO(n)O(1)
Sum formulaO(n)O(1)
Hash setO(n)O(n)

Find the Duplicate Number

Problem: Given an array of n + 1 integers where each integer is in [1, n], find the single duplicate.

Cyclic sort with early detection. When a swap would place arr[i] at a slot that already holds the same value, that value is the duplicate.

Visual Walkthrough

Array: [1, 3, 4, 2, 2]. Values in [1, 4] but the array has 5 elements-one value must repeat.

Find the Duplicate

int findDuplicate(std::vector<int>& nums) {
    int i = 0;
    while (i < (int)nums.size()) {
        int correct = nums[i] - 1;
        if (nums[i] != nums[correct]) {
            std::swap(nums[i], nums[correct]);
        } else {
            if (i != correct) return nums[i];
            i++;
        }
    }
    return -1; // should not reach here
}

// Example: [1, 3, 4, 2, 2]
// At some point arr[i]=2 but arr[1]=2 already => duplicate is 2
Alternative: Floyd's Tortoise and Hare cycle detection also solves this in O(n) time and O(1) space without modifying the array. The cyclic sort approach is simpler but does modify the input.

Find All Missing Numbers

Problem: Given an array of n integers where each is in [1, n], find all numbers in that range that do not appear.

After cyclic sort, scan every index. If arr[i] != i + 1, then i + 1 is missing.

Visual Walkthrough

Array: [4, 3, 2, 7, 8, 2, 3, 1]. After cyclic sort: [1, 2, 3, 4, 3, 2, 7, 8].

After Cyclic Sort 1 2 3 4 3 2 7 8 idx 0 idx 1 idx 2 idx 3 idx 4 idx 5 idx 6 idx 7 3 != 5 2 != 6 Missing: 5 and 6
std::vector<int> findDisappearedNumbers(std::vector<int>& nums) {
    int n = nums.size();
    int i = 0;
    while (i < n) {
        int correct = nums[i] - 1;
        if (nums[i] != nums[correct]) {
            std::swap(nums[i], nums[correct]);
        } else {
            i++;
        }
    }
    std::vector<int> result;
    for (int i = 0; i < n; i++) {
        if (nums[i] != i + 1) {
            result.push_back(i + 1);
        }
    }
    return result;
}

// Example: [4, 3, 2, 7, 8, 2, 3, 1]
// After sort: [1, 2, 3, 4, 3, 2, 7, 8]
// Missing at index 4 (missing 5) and index 5 (missing 6)

Find All Duplicates

Problem: Given an array of n integers where each integer is in [1, n] and some elements appear twice, find all the duplicates.

The mirror image of "Find All Missing." After cyclic sort, one copy of each duplicate occupies its correct slot. The other copy is stranded at a wrong index. Scan for positions where arr[i] != i + 1 and collect arr[i] as a duplicate.

Visual Walkthrough

Same array as above. After sorting, the values at mismatched indices are the duplicates.

After Cyclic Sort 1 2 3 4 3 2 7 8 idx 0 idx 1 idx 2 idx 3 idx 4 idx 5 idx 6 idx 7 dup: 3 dup: 2 Duplicates: [3, 2]
std::vector<int> findDuplicates(std::vector<int>& nums) {
    int n = nums.size();
    int i = 0;
    while (i < n) {
        int correct = nums[i] - 1;
        if (nums[i] != nums[correct]) {
            std::swap(nums[i], nums[correct]);
        } else {
            i++;
        }
    }
    std::vector<int> result;
    for (int i = 0; i < n; i++) {
        if (nums[i] != i + 1) {
            result.push_back(nums[i]);
        }
    }
    return result;
}

// Example: [4, 3, 2, 7, 8, 2, 3, 1]
// After sort: [1, 2, 3, 4, 3, 2, 7, 8]
// Duplicates: [3, 2]

When to Use Cyclic Sort

Cyclic sort is the right tool when all three of these conditions hold:

  1. Contiguous range: The values come from a known range like [1, n] or [0, n-1].
  2. In-place requirement: You need O(1) extra space.
  3. Missing or duplicate detection: You need to find what is out of place after sorting.
Pattern recognition tip: If an interview problem says "array of length n with values in [1, n]" and asks about missing or duplicate numbers, cyclic sort is almost certainly the intended $O(n)$ / $O(1)$ solution.
ProblemRangeAfter Sort, Check
Missing Number[0, n]arr[i] != i
Find Duplicate[1, n]Detect during sort: arr[i] == arr[correct]
All Missing[1, n]arr[i] != i + 1 means i + 1 is missing
All Duplicates[1, n]arr[i] != i + 1 means arr[i] is a duplicate

Complexity Summary

All cyclic sort variants share the same fundamental complexity. The sort itself is $O(n)$ with $O(1)$ space. The post-sort scan is another $O(n)$. Total: $O(n)$ time, $O(1)$ space.

PhaseTimeSpace
Cyclic sort (swap phase)O(n)O(1)
Post-sort scanO(n)O(1) or O(k) for results
TotalO(n)O(1) extra

Key Takeaways