Array optimizations
When values are bounded by the array length, the values themselves can serve as indexes. This lesson covers in-place cycle decomposition of a permutation, index placement, cyclic sort, pigeonhole indexing with sign flipping, and finding missing or duplicate numbers in O(n) time and O(1) space.
Learning goals
- Place each value at the index it names
- Decompose a permutation into cycles
- Find missing and duplicate numbers without extra space
Prerequisites
In-place cycle decomposition
A permutation splits into disjoint cycles. To put a permutation in order in place, follow a cycle: swap the element at index i with the element at the index it points to, until position i holds i. Every swap fixes one element permanently, so a cycle of length L needs L - 1 swaps and the whole permutation needs n minus the number of cycles.
Enter any permutation of 0..n-1 and read the cycle count.
1static int sortPermutation(int[] p) {2int cycles = 0;3for (int i = 0; i < p.length; i++) {4if (p[i] == i) continue;5cycles++;6while (p[i] != i) { int j = p[i]; int t = p[i]; p[i] = p[j]; p[j] = t; }7}8return cycles;9}
Applying a permutation in place by following cycles
Interactive · edit the inputint[] arr · length 6
Each value points to the slot it must move to. Following those pointers walks a cycle; every swap puts one value in its final slot.
1static int sortPermutation(int[] p) { // p is a permutation of 0..n-12int cycles = 0;3for (int i = 0; i < p.length; i++) {4if (p[i] == i) continue; // fixed point or already placed5cycles++;6while (p[i] != i) swap(p, i, p[i]); // every swap fixes one element for good7}8return cycles; // cycles of length >= 29}
Live state
cycles
0
Execution trace (dry run table)Show 11 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | cycles=0 | Each value points to the slot it must move to. Following those pointers walks a cycle; every swap puts one value in its final slot. |
| 2 | L5 | i=0 p[i]=2 cycles=1 | Index 0 starts a new cycle (cycle 1). |
| 3 | L6 | i=0 j=2 swaps=1 | Swap p[0] with p[2]: the value 2 lands at index 2, its final slot. Index 0 now holds 1. |
| 4 | L6 | i=0 j=1 swaps=2 | Swap p[0] with p[1]: the value 1 lands at index 1, its final slot. The cycle is closed. |
| 5 | L4 | i=1 p[i]=1 | p[1] = 1: this slot is already correct. |
| 6 | L4 | i=2 p[i]=2 | p[2] = 2: this slot is already correct. |
| 7 | L5 | i=3 p[i]=4 cycles=2 | Index 3 starts a new cycle (cycle 2). |
| 8 | L6 | i=3 j=4 swaps=3 | Swap p[3] with p[4]: the value 4 lands at index 4, its final slot. The cycle is closed. |
| 9 | L4 | i=4 p[i]=4 | p[4] = 4: this slot is already correct. |
| 10 | L4 | i=5 p[i]=5 | p[5] = 5: this slot is already correct. |
| 11 | L8 | return=2 swaps=3 | 2 cycles of length 2 or more, 3 swaps: a cycle of length L needs exactly L - 1 swaps, so the total is n minus the number of cycles (counting fixed points). |
Index placement
Index placement puts each value at the index it names: value v goes to index v - 1 (or v for 0-based data). Swap the current element to its home; the element that arrives is re-examined. Each swap places one value for good, so the total number of swaps is at most n and the whole process is O(n).
Values outside 1..n have no home and are skipped.
1int i = 0;2while (i < a.length) {3int j = a[i] - 1; // home of a[i]4if (a[i] >= 1 && a[i] <= a.length && a[j] != a[i]) swap(a, i, j);5else i++;6}
Cyclic sort: put each value at index value - 1
Interactive · edit the inputint[] arr · length 4
A value v in 1..4 belongs at index v - 1. Keep swapping a[i] to its home until a[i] is out of range or its home already holds the same value.
1static int firstMissingPositive(int[] a) {2int i = 0, n = a.length;3while (i < n) {4int j = a[i] - 1; // home slot of a[i]5if (a[i] >= 1 && a[i] <= n && a[j] != a[i]) swap(a, i, j);6else i++;7}8for (i = 0; i < n; i++) if (a[i] != i + 1) return i + 1; // first slot holding the wrong value9return n + 1;10}
Live state
n
4
Execution trace (dry run table)Show 14 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | n=4 | A value v in 1..4 belongs at index v - 1. Keep swapping a[i] to its home until a[i] is out of range or its home already holds the same value. |
| 2 | L5 | i=0 a[i]=3 home=2 | 3 belongs at index 2, which holds -1: swap them. |
| 3 | L5 | i=0 swaps=1 | 3 is now home at index 2. Do not advance i: the value that arrived at index 0 still needs to be checked. |
| 4 | L5 | i=0 a[i]=-1 home=none | -1 is outside 1..4, so it has no home. Move on. |
| 5 | L5 | i=1 a[i]=4 home=3 | 4 belongs at index 3, which holds 1: swap them. |
| 6 | L5 | i=1 swaps=2 | 4 is now home at index 3. Do not advance i: the value that arrived at index 1 still needs to be checked. |
| 7 | L5 | i=1 a[i]=1 home=0 | 1 belongs at index 0, which holds -1: swap them. |
| 8 | L5 | i=1 swaps=3 | 1 is now home at index 0. Do not advance i: the value that arrived at index 1 still needs to be checked. |
| 9 | L5 | i=1 a[i]=-1 home=none | -1 is outside 1..4, so it has no home. Move on. |
| 10 | L5 | i=2 a[i]=3 home=2 | 3 is already at its home index 2. |
| 11 | L5 | i=3 a[i]=4 home=3 | 4 is already at its home index 3. |
| 12 | L8 | i=0 a[i]=1 | Index 0 holds 1. |
| 13 | L8 | i=1 a[i]=-1 | Index 1 should hold 2 but holds -1: 2 is missing. |
| 14 | L8 | return=2 swaps=3 | First missing positive: 2. Each swap places one value for good, so at most n swaps: O(n) time, O(1) space. |
Cyclic sort
Cyclic sort sorts an array that holds the numbers 1..n by index placement, in O(n) time and O(1) space, instead of O(n log n). It is the right tool when the values are a permutation of 1..n, or when you only need to know which are missing or repeated. The while loop does not advance after a swap, because a new, unplaced value now sits at i.
The first preset has a missing 2; the second has a duplicate and a missing value.
1static void cyclicSort(int[] a) { // a is a permutation of 1..n2int i = 0;3while (i < a.length) {4int j = a[i] - 1;5if (a[i] != a[j]) swap(a, i, j); else i++;6}7}
Cyclic sort: put each value at index value - 1
Interactive · edit the inputint[] arr · length 4
A value v in 1..4 belongs at index v - 1. Keep swapping a[i] to its home until a[i] is out of range or its home already holds the same value.
1static int firstMissingPositive(int[] a) {2int i = 0, n = a.length;3while (i < n) {4int j = a[i] - 1; // home slot of a[i]5if (a[i] >= 1 && a[i] <= n && a[j] != a[i]) swap(a, i, j);6else i++;7}8for (i = 0; i < n; i++) if (a[i] != i + 1) return i + 1; // first slot holding the wrong value9return n + 1;10}
Live state
n
4
Execution trace (dry run table)Show 14 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | n=4 | A value v in 1..4 belongs at index v - 1. Keep swapping a[i] to its home until a[i] is out of range or its home already holds the same value. |
| 2 | L5 | i=0 a[i]=3 home=2 | 3 belongs at index 2, which holds -1: swap them. |
| 3 | L5 | i=0 swaps=1 | 3 is now home at index 2. Do not advance i: the value that arrived at index 0 still needs to be checked. |
| 4 | L5 | i=0 a[i]=-1 home=none | -1 is outside 1..4, so it has no home. Move on. |
| 5 | L5 | i=1 a[i]=4 home=3 | 4 belongs at index 3, which holds 1: swap them. |
| 6 | L5 | i=1 swaps=2 | 4 is now home at index 3. Do not advance i: the value that arrived at index 1 still needs to be checked. |
| 7 | L5 | i=1 a[i]=1 home=0 | 1 belongs at index 0, which holds -1: swap them. |
| 8 | L5 | i=1 swaps=3 | 1 is now home at index 0. Do not advance i: the value that arrived at index 1 still needs to be checked. |
| 9 | L5 | i=1 a[i]=-1 home=none | -1 is outside 1..4, so it has no home. Move on. |
| 10 | L5 | i=2 a[i]=3 home=2 | 3 is already at its home index 2. |
| 11 | L5 | i=3 a[i]=4 home=3 | 4 is already at its home index 3. |
| 12 | L8 | i=0 a[i]=1 | Index 0 holds 1. |
| 13 | L8 | i=1 a[i]=-1 | Index 1 should hold 2 but holds -1: 2 is missing. |
| 14 | L8 | return=2 swaps=3 | First missing positive: 2. Each swap places one value for good, so at most n swaps: O(n) time, O(1) space. |
Pigeonhole indexing
If an array of n values has every value in 1..n, the array doubles as a seen-set: use the sign of a[v - 1] to record whether v appeared. Reading a value, flip a[v - 1] to negative; if it is already negative, v is a duplicate. By the pigeonhole principle a duplicate exists exactly when some value is missing. O(n) time, O(1) space, but the input is modified (restore it by taking absolute values).
Run it and read both the duplicate list and the missing list.
1static List<Integer> findDuplicates(int[] a) {2List<Integer> dups = new ArrayList<>();3for (int x : a) {4int idx = Math.abs(x) - 1;5if (a[idx] < 0) dups.add(Math.abs(x)); else a[idx] = -a[idx];6}7return dups;8}
Pigeonhole indexing: find duplicates by flipping signs
Interactive · edit the inputint[] arr · length 8
duplicates found
Every value is a valid index in 1..8, so the array doubles as a seen-set: the sign of a[v - 1] records whether v appeared.
1static List<Integer> findDuplicates(int[] a) { // every value is in 1..n2List<Integer> dups = new ArrayList<>();3for (int x : a) {4int idx = Math.abs(x) - 1;5if (a[idx] < 0) dups.add(Math.abs(x)); // slot already flagged: seen before6else a[idx] = -a[idx]; // flag value idx + 1 as seen7}8return dups;9}10// afterwards a[i] > 0 means i + 1 never appeared
Live state
Execution trace (dry run table)Show 18 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | Every value is a valid index in 1..8, so the array doubles as a seen-set: the sign of a[v - 1] records whether v appeared. | |
| 2 | L5 | i=0 x=4 idx=3 a[idx]=7 | a[3] is positive: first time we see 4. Flip its sign. |
| 3 | L6 | i=0 x=4 | a[3] = -7 (flagged). |
| 4 | L5 | i=1 x=3 idx=2 a[idx]=2 | a[2] is positive: first time we see 3. Flip its sign. |
| 5 | L6 | i=1 x=3 | a[2] = -2 (flagged). |
| 6 | L5 | i=2 x=2 idx=1 a[idx]=3 | a[1] is positive: first time we see 2. Flip its sign. |
| 7 | L6 | i=2 x=2 | a[1] = -3 (flagged). |
| 8 | L5 | i=3 x=7 idx=6 a[idx]=3 | a[6] is positive: first time we see 7. Flip its sign. |
| 9 | L6 | i=3 x=7 | a[6] = -3 (flagged). |
| 10 | L5 | i=4 x=8 idx=7 a[idx]=1 | a[7] is positive: first time we see 8. Flip its sign. |
| 11 | L6 | i=4 x=8 | a[7] = -1 (flagged). |
| 12 | L5 | i=5 x=2 idx=1 a[idx]=-3 | a[1] is already negative, so 2 appeared before: it is a duplicate. |
| 13 | L5 | i=5 x=2 | Record 2 as a duplicate. |
| 14 | L5 | i=6 x=3 idx=2 a[idx]=-2 | a[2] is already negative, so 3 appeared before: it is a duplicate. |
| 15 | L5 | i=6 x=3 | Record 3 as a duplicate. |
| 16 | L5 | i=7 x=1 idx=0 a[idx]=4 | a[0] is positive: first time we see 1. Flip its sign. |
| 17 | L6 | i=7 x=1 | a[0] = -4 (flagged). |
| 18 | L8 | return=[2, 3] missing=[5, 6] | Duplicates: [2, 3]. Slots that stayed positive mean the value index + 1 never appeared: missing [5, 6]. O(n) time and O(1) extra space, but the input is modified. |
Missing/duplicate numbers
Typical tasks: find the smallest missing positive, find all missing numbers in 1..n, or find the duplicate. After cyclic sort every index i that does not hold i + 1 reveals a missing value i + 1, and the value found there is a duplicate. Sign marking gives the same information with positive entries marking the missing values.
Compare the two methods: cyclic sort rearranges, sign marking flips signs.
1static int firstMissingPositive(int[] a) {2int n = a.length;3for (int i = 0; i < n; ) {4int j = a[i] - 1;5if (a[i] >= 1 && a[i] <= n && a[j] != a[i]) { int t = a[i]; a[i] = a[j]; a[j] = t; }6else i++;7}8for (int i = 0; i < n; i++) if (a[i] != i + 1) return i + 1;9return n + 1;10}
Cyclic sort: put each value at index value - 1
Interactive · edit the inputint[] arr · length 4
A value v in 1..4 belongs at index v - 1. Keep swapping a[i] to its home until a[i] is out of range or its home already holds the same value.
1static int firstMissingPositive(int[] a) {2int i = 0, n = a.length;3while (i < n) {4int j = a[i] - 1; // home slot of a[i]5if (a[i] >= 1 && a[i] <= n && a[j] != a[i]) swap(a, i, j);6else i++;7}8for (i = 0; i < n; i++) if (a[i] != i + 1) return i + 1; // first slot holding the wrong value9return n + 1;10}
Live state
n
4
Execution trace (dry run table)Show 14 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | n=4 | A value v in 1..4 belongs at index v - 1. Keep swapping a[i] to its home until a[i] is out of range or its home already holds the same value. |
| 2 | L5 | i=0 a[i]=3 home=2 | 3 belongs at index 2, which holds -1: swap them. |
| 3 | L5 | i=0 swaps=1 | 3 is now home at index 2. Do not advance i: the value that arrived at index 0 still needs to be checked. |
| 4 | L5 | i=0 a[i]=-1 home=none | -1 is outside 1..4, so it has no home. Move on. |
| 5 | L5 | i=1 a[i]=4 home=3 | 4 belongs at index 3, which holds 1: swap them. |
| 6 | L5 | i=1 swaps=2 | 4 is now home at index 3. Do not advance i: the value that arrived at index 1 still needs to be checked. |
| 7 | L5 | i=1 a[i]=1 home=0 | 1 belongs at index 0, which holds -1: swap them. |
| 8 | L5 | i=1 swaps=3 | 1 is now home at index 0. Do not advance i: the value that arrived at index 1 still needs to be checked. |
| 9 | L5 | i=1 a[i]=-1 home=none | -1 is outside 1..4, so it has no home. Move on. |
| 10 | L5 | i=2 a[i]=3 home=2 | 3 is already at its home index 2. |
| 11 | L5 | i=3 a[i]=4 home=3 | 4 is already at its home index 3. |
| 12 | L8 | i=0 a[i]=1 | Index 0 holds 1. |
| 13 | L8 | i=1 a[i]=-1 | Index 1 should hold 2 but holds -1: 2 is missing. |
| 14 | L8 | return=2 swaps=3 | First missing positive: 2. Each swap places one value for good, so at most n swaps: O(n) time, O(1) space. |
Pigeonhole indexing: find duplicates by flipping signs
Interactive · edit the inputint[] arr · length 8
duplicates found
Every value is a valid index in 1..8, so the array doubles as a seen-set: the sign of a[v - 1] records whether v appeared.
1static List<Integer> findDuplicates(int[] a) { // every value is in 1..n2List<Integer> dups = new ArrayList<>();3for (int x : a) {4int idx = Math.abs(x) - 1;5if (a[idx] < 0) dups.add(Math.abs(x)); // slot already flagged: seen before6else a[idx] = -a[idx]; // flag value idx + 1 as seen7}8return dups;9}10// afterwards a[i] > 0 means i + 1 never appeared
Live state
Execution trace (dry run table)Show 18 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | Every value is a valid index in 1..8, so the array doubles as a seen-set: the sign of a[v - 1] records whether v appeared. | |
| 2 | L5 | i=0 x=4 idx=3 a[idx]=7 | a[3] is positive: first time we see 4. Flip its sign. |
| 3 | L6 | i=0 x=4 | a[3] = -7 (flagged). |
| 4 | L5 | i=1 x=3 idx=2 a[idx]=2 | a[2] is positive: first time we see 3. Flip its sign. |
| 5 | L6 | i=1 x=3 | a[2] = -2 (flagged). |
| 6 | L5 | i=2 x=2 idx=1 a[idx]=3 | a[1] is positive: first time we see 2. Flip its sign. |
| 7 | L6 | i=2 x=2 | a[1] = -3 (flagged). |
| 8 | L5 | i=3 x=7 idx=6 a[idx]=3 | a[6] is positive: first time we see 7. Flip its sign. |
| 9 | L6 | i=3 x=7 | a[6] = -3 (flagged). |
| 10 | L5 | i=4 x=8 idx=7 a[idx]=1 | a[7] is positive: first time we see 8. Flip its sign. |
| 11 | L6 | i=4 x=8 | a[7] = -1 (flagged). |
| 12 | L5 | i=5 x=2 idx=1 a[idx]=-3 | a[1] is already negative, so 2 appeared before: it is a duplicate. |
| 13 | L5 | i=5 x=2 | Record 2 as a duplicate. |
| 14 | L5 | i=6 x=3 idx=2 a[idx]=-2 | a[2] is already negative, so 3 appeared before: it is a duplicate. |
| 15 | L5 | i=6 x=3 | Record 3 as a duplicate. |
| 16 | L5 | i=7 x=1 idx=0 a[idx]=4 | a[0] is positive: first time we see 1. Flip its sign. |
| 17 | L6 | i=7 x=1 | a[0] = -4 (flagged). |
| 18 | L8 | return=[2, 3] missing=[5, 6] | Duplicates: [2, 3]. Slots that stayed positive mean the value index + 1 never appeared: missing [5, 6]. O(n) time and O(1) extra space, but the input is modified. |
Practice ladder
Now try it yourself
Easy
- Cyclic sort {3, 1, 2}
- How many swaps sort the permutation {1, 2, 0}?
Medium
- Find the missing number in {3, 0, 1} (values 0..n)
- Find all duplicates in {4, 3, 2, 7, 8, 2, 3, 1}
Advanced
- Find the duplicate in an array of n + 1 values in 1..n without modifying it (Floyd's cycle detection)
Key takeaways
- Bounded values can be used as indexes: place each at its home slot.
- A cycle of length L needs L - 1 swaps; each swap fixes one value.
- Sign flipping and cyclic sort find missing and duplicate numbers in O(n) time and O(1) space.