Skip to content

Commit c712040

Browse files
committed
Revamp cylic sort
1 parent 2bc3a4a commit c712040

File tree

6 files changed

+122
-119
lines changed

6 files changed

+122
-119
lines changed
Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,27 @@
11
# Cyclic Sort
2-
Cyclic sort is a comparison-based, in-place algorithm that performs sorting in O(n^2) time.
3-
It is quite similar to selection sort, with both invariants ensuring that at the end of
4-
the ith iteration, the ith element is correctly positioned. But they differ slightly in how
5-
they ensure this invariant is maintained, allowing cyclic sort to have a time complexity of O(n)
6-
for certain inputs (the relative ordering of the elements are already known).
2+
3+
## Background
4+
Cyclic sort is a comparison-based, in-place algorithm that performs sorting (generally) in O(n^2) time.
5+
Though under some conditions (discussed later), the best case could be done in O(n) time.
6+
7+
### Implementation Invariant
8+
At the end of the ith iteration, the ith element
9+
(of the original array, from either the back or front depending on implementation), is correctly positioned.
10+
11+
### Comparison to Selection Sort
12+
Its invariant is quite similar as selection sort's. But they differ slightly in maintaining this invariant.
13+
The algorithm for cyclic sort does a bit more than selection sort's.
14+
In the process of trying to find the correct element for the ith position, any element that was
15+
encountered will be correctly placed in their positions as well.
16+
17+
This allows cyclic sort to have a time complexity of O(n) for certain inputs.
18+
(where the relative ordering of the elements is already known). This is discussed in the [**simple**](./simple) case.
19+
20+
## Simple and Generalised Case
21+
We discuss more implementation-specific details and complexity analysis in the respective folders. In short,
22+
1. The [**simple**](./simple) case discusses the non-comparison based implementation of cyclic sort under
23+
certain conditions. This allows the best case to be better than O(n^2).
24+
2. The [**generalised**](./generalised) case discusses cyclic sort for general inputs. This is comparison-based and is
25+
usually implemented in O(n^2).
26+
27+
Lines changed: 11 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,33 @@
11
package algorithms.sorting.cyclicSort.generalised;
22
/**
3-
* Implementation of cyclic sort in the generalised case where the input can contain any integer and even duplicates.
4-
* <p></p>
5-
*
6-
* Analysis: <br>
7-
* Time:
8-
* Best: O(n^2) even though no swaps, array still needs to be traversed in O(n) at each iteration <br>
9-
* Worst: O(n^2) since we need O(n) time to find the correct position of an element at each iteration <br>
10-
* Average: O(n^2)
11-
* Space: O(1) auxiliary space, this is an in-place algorithm <p></p>
12-
*
13-
* Invariant: <br>
14-
* At the end of the ith iteration, the ith element is correctly positioned
15-
* (smallest or largest depending on implementation). <br>
16-
*
17-
* This is exactly the same invariant as Selection sort. But the algorithm for cyclic sort actually does a bit
18-
* more. In the process of trying to find the correct element for the ith position, whatever elements that were
19-
* encountered will be correctly placed in their positions as well. Though, unlike the simple case where the
20-
* ordering of the elements are known between one another, the time complexity here remains at O(n^2) because
21-
* cyclic sort will still have to iterate over all the elements at each iteration to verify or find the correct
22-
* position of the element. <p></p>
23-
*
24-
* Illustration: <br>
25-
* Result: 6 10 6 5 7 4 2 1
26-
* Read: ^ 6 should be placed at index 4, so we do a swap,
27-
* Result: 7 10 6 5 6 4 2 1
28-
* Read: ^ 7 should be placed at index 6, so we do a swap. Note that index 5 should be taken up by dup 6
29-
* Result: 2 10 6 5 6 4 7 1
30-
* Read: ^ 2 should be placed at index 1, so swap.
31-
* Result: 10 2 6 5 6 4 7 1
32-
* Read: ^ 10 is the largest, should be placed at last index, so swap.
33-
* Result: 1 2 6 5 6 4 7 10
34-
* Read: ^ Correctly placed, so move on. Same for 2.
35-
* Read: ^ should be placed at index 4. But index 4 already has a 6. So place at index 5 and so on.
36-
* Result: 1 2 4 5 6 6 7 10
37-
* Read: ^ ^ ^ ^ ^ Continue with O(n) verification of correct position at each iteration
38-
*
3+
* Implementation of cyclic sort in the generalised case where the input can contain any integer and duplicates.
394
*/
405
public class CyclicSort {
41-
public static void cycleSort(int[] arr, int n) {
42-
for (int currIdx = 0; currIdx < n - 1; currIdx++) {
6+
public static void cyclicSort(int[] arr, int n) {
7+
for (int currIdx = 0; currIdx < n-1; currIdx++) {
438
int currElement = arr[currIdx];
44-
int rightfulPos = currIdx;
45-
for (int i = currIdx + 1; i < n; i++) { // O(n), find the rightful position of the curr element
46-
if (arr[i] < currElement) {
47-
rightfulPos++;
48-
}
49-
}
50-
if (rightfulPos == currIdx) { // verified currElement is already in its right position, go to next index
51-
continue;
52-
}
53-
54-
while (currElement == arr[rightfulPos]) { // when attempting to place currElement at rightful pos,
55-
rightfulPos++; // found that currElement is a duplicate, so place duplicates at next immediate positions
56-
}
57-
58-
int tmp = currElement; // swap, put item in its rightful position
59-
arr[currIdx] = arr[rightfulPos];
60-
arr[rightfulPos] = tmp;
61-
62-
/*
63-
The currElement has now been placed at its right position, with the element previously at that position
64-
at currIdx now. We shall continue this process until the element being swapped and placed at currIdx is the
65-
correct one. In other words, the rightfulPos of the element to be considered is the same as currIdx.
66-
Only then will we move on to the next index.
67-
*/
68-
while (currIdx != rightfulPos) {
69-
currElement = arr[currIdx];
70-
rightfulPos = currIdx;
71-
for (int i = currIdx + 1; i < n; i++) {
9+
int rightfulPos;
10+
do {
11+
rightfulPos = currIdx; // initialization since elements before currIdx are correctly placed
12+
for (int i = currIdx + 1; i < n; i++) { // O(n) find rightfulPos for the currElement
7213
if (arr[i] < currElement) {
7314
rightfulPos++;
7415
}
7516
}
76-
if (currIdx == rightfulPos) {
77-
break;
78-
}
79-
while (currElement == arr[rightfulPos]) {
17+
while (currElement == arr[rightfulPos]) { // duplicates found, so find next suitable position
8018
rightfulPos++;
8119
}
82-
tmp = currElement;
20+
int tmp = currElement; // swap, put item in its rightful position
8321
arr[currIdx] = arr[rightfulPos];
8422
arr[rightfulPos] = tmp;
85-
}
23+
} while (rightfulPos != currIdx);
8624
}
8725
}
8826

8927
/*
9028
Overloaded helper method that calls internal implementation.
9129
*/
9230
public static void sort(int[] arr) {
93-
cycleSort(arr, arr.length);
31+
cyclicSort(arr, arr.length);
9432
}
9533
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Generalized Case
2+
3+
## More Details
4+
Implementation of cyclic sort in the generalised case where the input can contain any integer and duplicates.
5+
6+
This is a comparison-based algorithm. In short, for the element encountered at the ith position of the original array,
7+
the algorithm does a O(n) traversal to search for its rightful position and does a swap. It repeats this until a swap
8+
placing the correct element at the ith position, before moving onto the next (i+1)th.
9+
10+
### Illustration
11+
Result: 6 10 6 5 7 4 2 1 <br> &nbsp;
12+
Read: ^ 6 should be placed at index 4, so we do a swap with 6 and 7, <br><br>
13+
Result: 7 10 6 5 6 4 2 1 <br> &nbsp;
14+
Read: ^ 7 should be placed at index 6, so we do a swap. Note that index 5 should be taken up by dup 6 <br><br>
15+
Result: 2 10 6 5 6 4 7 1 <br> &nbsp;
16+
Read: ^ 2 should be placed at index 1, so swap. <br><br>
17+
Result: 10 2 6 5 6 4 7 1 <br> &nbsp;
18+
Read: ^ 10 is the largest, should be placed at last index, so swap. <br><br>
19+
Result: 1 2 6 5 6 4 7 10 <br> &nbsp;
20+
Read: ^ Correctly placed, so move on. Same for 2. <br> &nbsp;
21+
Read: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
22+
^ 6 should be placed at index 4. But index 4 already has a 6. So place at index 5 and so on. <br><br>
23+
Result: 1 2 4 5 6 6 7 10 <br> &nbsp;
24+
Read: &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
25+
^ ^ ^ ^ ^ Continue with O(n) verification of correct position at each iteration
26+
27+
28+
## Complexity Analysis
29+
**Time**:
30+
- Best: O(n^2) even if the ith element is encountered in the ith position, a O(n) traversal validation check is needed
31+
- Worst: O(n^2) since we need O(n) time to find / validate the correct position of an element and
32+
the total number of O(n) traversals is bounded by O(n).
33+
- Average: O(n^2), it's bounded by the above two
34+
35+
**Space**: O(1) auxiliary space, this is an in-place algorithm

src/main/java/algorithms/sorting/cyclicSort/simple/CyclicSort.java

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,7 @@
22

33
/**
44
* Implementation of cyclic sort in the simple case where the n elements of the given array are contiguous,
5-
* but not in sorted order. Below illustrates the idea using integers from 0 to n-1. <p></p>
6-
*
7-
* Analysis: <br>
8-
* Time:
9-
* Best: O(n) since the array has to be traversed <br>
10-
* Worst: O(n) since each element is at most seen twice <br>
11-
* Average: O(n), it's bounded by the above two <br>
12-
* Space: O(1) auxiliary space, this is an in-place algorithm <p></p>
13-
*
14-
* Invariant: <br>
15-
* At the end of the ith iteration, the ith element is correctly positioned
16-
* (smallest or largest depending on implementation). <br>
17-
*
18-
* This is exactly the same invariant as Selection sort. But the algorithm for cyclic sort actually does a bit
19-
* more. In the process of trying to find the correct element for the ith position, whatever elements that were
20-
* encountered will be correctly placed in their positions as well. This leads to the O(n) complexity as
21-
* opposed to Selection sort's O(n^2). <p></p>
22-
*
23-
* NOTE: <br>
24-
* In this implementation, the algorithm is not comparison-based! (unlike the general case) <br>
25-
* It makes use of the known inherent ordering of the numbers. <br>
26-
*
27-
* It may seem quite trivial to sort integers from 0 to n-1 when one could simply generate such a sequence. But
28-
* this algorithm proves its use in cases where the integers to be sorted are keys to associated values and sorting
29-
* to be done in O(1) auxiliary space.
5+
* but not in sorted order. Below illustrates the idea using integers from 0 to n-1.
306
*/
317
public class CyclicSort {
328
/**
@@ -38,12 +14,12 @@ public static void sort(int[] arr) {
3814
while (curr < arr.length) { // iterate until the end of the array
3915
int ele = arr[curr]; // encounter an element that may not be in its correct position
4016
assert ele >= 0 && ele < arr.length : "Input array should only have integers from 0 to n-1 (inclusive)";
41-
if (ele != curr) { // verified that it is indeed not the correct element to be placed at this curr position
17+
if (ele != curr) { // verified that it is indeed not the correct element to be placed at curr position
4218
int tmp = arr[ele]; // go to the correct position of ele
4319
arr[ele] = ele; // do a swap
4420
arr[curr] = tmp; // note that curr isn't incremented because we haven't yet placed the correct element
4521
} else {
46-
curr += 1; // we found the correct element to be placed at pos curr, which in this example, is itself
22+
curr += 1; // found the correct element to be placed at curr, which in this eg, is itself, so, increment
4723
}
4824
}
4925
}

src/main/java/algorithms/sorting/cyclicSort/simple/FindFirstMissingNonNegative.java

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,7 @@
22

33
/**
44
* Cyclic sort algorithm can be easily modified to find first missing non-negative integer (i.e. starting from 0)
5-
* in O(n). <pr></pr>
6-
*
7-
* There are other ways of doing so, using a hash set for instance, but what makes cyclic sort stand out is that it is
8-
* able to do so in O(1) space. In other words, it is in-place and require no additional space. <pr></pr>
9-
*
10-
* The algorithm does a 2-pass iteration. In the 1st iteration, it places elements at its rightful position where
11-
* possible. And in the 2nd iteration, it will look for the first out of place element (element that is not supposed
12-
* to be in that position). The answer will be the index of that position. <br>
13-
* Note that the answer is necessarily between 0 and n (inclusive), where n is the length of the array,
14-
* otherwise there would be a contradiction. <br>
15-
* So, if a negative number or a number greater than n is encountered, simply ignore the number at the position and
16-
* move on first. It may be subject to swap later.
5+
* in O(n).
176
*/
187
public class FindFirstMissingNonNegative {
198
public static int findMissing(int[] arr) {
@@ -25,11 +14,11 @@ public static int findMissing(int[] arr) {
2514
curr += 1;
2615
continue;
2716
}
28-
int tmp = arr[ele]; // do the swap and place ele at its right position
17+
int tmp = arr[ele]; // do the swap and place ele at its right position first
2918
arr[ele] = ele;
3019
arr[curr] = tmp;
3120
} else {
32-
curr += 1; // found curr and placed at position curr, move on.
21+
curr += 1; // either found the correct element, or a number out of range to ignore first.
3322
}
3423
}
3524

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Simple Case
2+
3+
## More Details
4+
Cyclic Sort can achieve O(n) time complexity in cases where the elements of the collection have a known,
5+
continuous range, and there exists a direct, O(1) time-complexity mapping from each element to its respective index in
6+
the sorted order.
7+
8+
This is typically applicable when sorting a sequence of integers that are in a consecutive range
9+
or can be easily mapped to such a range. We illustrate the idea with n integers from 0 to n-1.
10+
11+
In this implementation, the algorithm is **not comparison-based**! (unlike the general case).
12+
It makes use of the known inherent ordering of the numbers, bypassing the nlogn lower bound for most sorting algorithms.
13+
14+
## Complexity Analysis
15+
**Time**:
16+
- Best: O(n) since the array has to be traversed
17+
- Worst: O(n) since each element is at most seen twice
18+
- Average: O(n), it's bounded by the above two
19+
20+
**Space**: O(1) auxiliary space, this is an in-place algorithm
21+
22+
## Case Study: Find First Missing Non-negative Integer
23+
Cyclic sort algorithm can be easily modified to find first missing non-negative integer (i.e. starting from 0) in O(n).
24+
The invariant is the same, but for numbers that are out-of-range (negative or greater than n),
25+
simply ignore the number at the position and
26+
move on first. It may be subject to swap later.
27+
28+
There are other ways of doing so, using a hash set for instance, but what makes cyclic sort stand out is that it is
29+
able to do so in O(1) space. In other words, it is in-place and require no additional space.
30+
31+
The algorithm does a 2-pass iteration.
32+
1. In the 1st iteration, it places elements at its rightful position where possible.
33+
2. In the 2nd iteration, it will look for the first out of place element (element that is not supposed
34+
to be in that position). The answer will be the index of that position.
35+
36+
Note that the answer is necessarily between 0 and n (inclusive), where n is the length of the array,
37+
otherwise there would be a contradiction.
38+
39+
## Misc
40+
1. It may seem quite trivial to sort integers from 0 to n-1 when one could simply generate such a sequence.
41+
But this algorithm is useful in cases where the integers to be sorted are keys to associated values (or some mapping)
42+
and sorting needs to be done in O(1) auxiliary space.
43+
2. The implementation here uses integers from 0 to n-1. This can be easily modified for n contiguous integers starting
44+
at some arbitrary number (simply offset by this start number).

0 commit comments

Comments
 (0)