Skip to content

Commit 9a1f80b

Browse files
committed
Added three-way partitioning and renamed Hoare's to Lomuto's
1 parent 05876ce commit 9a1f80b

File tree

9 files changed

+273
-93
lines changed

9 files changed

+273
-93
lines changed

src/algorithms/sorting/quickSort/hoares/QuickSort.java renamed to src/algorithms/sorting/quickSort/lomuto/QuickSort.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
package src.algorithms.sorting.quickSort.hoares;
1+
package src.algorithms.sorting.quickSort.lomuto;
22

33
import java.lang.Math;
44

55
/**
6-
* Here, we are implementing Hoare's QuickSort where we sort the array in increasing (or more precisely,
6+
* Here, we are implementing Lomuto's QuickSort where we sort the array in increasing (or more precisely,
77
* non-decreasing) order.
88
*
99
* Basic Description:
@@ -16,6 +16,10 @@
1616
* Implementation Invariant:
1717
* The pivot is in the correct position, with elements to its left being <= it, and elements to its right being > it.
1818
*
19+
* We are implementing Lomuto's partition scheme here due to ease of implementation and alignment with CS2040S lecture
20+
* slides. This is opposed to Hoare's partition scheme, see more at
21+
* https://www.geeksforgeeks.org/hoares-vs-lomuto-partition-scheme-quicksort/.
22+
*
1923
* Complexity Analysis:
2024
* Time:
2125
* - Worst case (poor choice of pivot): O(n^2)
@@ -32,7 +36,11 @@
3236
*
3337
* In the worst case where the pivot selected is consistently the smallest or biggest element in the array, the
3438
* partitioning of the array around the pivot will be extremely unbalanced, leading to a recurrence relation of:
35-
* T(n) = T(n-1) + O(n) => O(n^2).
39+
* T(n) = T(n-1) + O(n) => O(n^2). We have reduced the likelihood of this happening by randomising pivot selection.
40+
*
41+
* However, if there are many duplicates in the array, e.g. {1, 1, 1, 1}, the 1st pivot will be placed in the 3rd idx,
42+
* and 2nd pivot in 2nd idx, 3rd pivot in the 1st idx and 4th pivot in the 0th idx. As we observe, the presence of many
43+
* duplicates in the array leads to extremely unbalanced partitioning, leading to a O(n^2) time complexity.
3644
*
3745
* Space:
3846
* - O(1) since sorting is done in-place

src/algorithms/sorting/quickSort/paranoid/QuickSort.java

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
* Here, we are implementing Paranoid QuickSort where we sort the array in increasing (or more precisely,
77
* non-decreasing) order.
88
*
9-
* This is basically Hoare's QuickSort, with an additional check to guarantee a good pivot.
9+
* This is basically Lomuto's QuickSort, with an additional check to guarantee a good pivot.
1010
*
1111
* Complexity Analysis:
1212
* Time:
13-
* - Expected worst case: O(nlogn)
13+
* - Worst case: does not terminate
1414
* - Average case: O(nlogn)
1515
* - Best case: O(nlogn)
1616
*
@@ -22,7 +22,11 @@
2222
* The number of iterations of pivot selection is expected to be <2 (more precisely, 1.25). This is because
2323
* P(good pivot) = 8/10. Expected number of tries to get a good pivot = 1 / P(good pivot) = 10/8 = 1.25.
2424
*
25-
* Therefore, the expected worst case time-complexity is: T(n) = T(n/10) + T(9n/10) + 1.25n => O(nlogn).
25+
* Therefore, the average time-complexity is: T(n) = T(n/10) + T(9n/10) + 1.25n => O(nlogn).
26+
*
27+
* However, the presence of this additional check and repeating pivot selection means that if we have an array of
28+
* length n >= 10 containing all duplicates of the same number, any pivot we pick will be a bad pivot and we will
29+
* enter an infinite loop of repeating pivot selection.
2630
*
2731
* Space:
2832
* - O(1) since sorting is done in-place
@@ -114,19 +118,28 @@ private static int random(int start, int end) {
114118
}
115119

116120
/**
117-
* Checks if the given index is a good pivot index for the Paranoid QuickSort algorithm.
121+
* Checks if the given pivot index is a good pivot for the QuickSort algorithm.
122+
* A good pivot is defined as an index that helps avoid worst-case behavior in QuickSort.
118123
*
119-
* A good pivot index is one that falls within the range (start - 1 + n/10) and (start + (9*n)/10),
120-
* where n is the number of elements in the array (end - start + 1).
124+
* For arrays of length greater than or equal to 10, a good pivot is an index that leaves at least
125+
* 1/10th of the array on each side.
126+
* *
127+
* If n < 10, such a pivot condition would be meaningless, therefore always return true. This would cause
128+
* the worst case recurrence relation to be T(n) = T(n-1) + O(n) => O(n^2) for small subarrays, but the overall
129+
* asymptotic time complexity of Paranoid QuickSort is still O(nlogn).
121130
*
122131
* @param pIdx The index to be checked for being a good pivot.
123132
* @param start The starting index of the current sub-array.
124133
* @param end The ending index of the current sub-array.
125134
* @return True if the given index is a good pivot, false otherwise.
126135
*/
127136
private static boolean isGoodPivot(int pIdx, int start, int end) {
128-
double n = end - start + 1;
129-
return pIdx > start - 1 + n / 10 && pIdx < start + (9 * n) / 10;
137+
double n = end - start + 1; // express n as a double so n/10 will be calculated as a double
138+
if (n >= 10) {
139+
return pIdx - start >= n / 10 && end - pIdx >= n / 10;
140+
} else {
141+
return true;
142+
}
130143
}
131144

132145
}

src/algorithms/sorting/quickSort/partitioning/ThreeWay.java

Lines changed: 0 additions & 54 deletions
This file was deleted.
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package src.algorithms.sorting.quickSort.threeWayPartitioning;
2+
3+
/**
4+
* Here, we are implementing QuickSort with three-way partitioning where we sort the array in increasing (or more
5+
* precisely, non-decreasing) order.
6+
*
7+
* Three-way partitioning is used in QuickSort to tackle the scenario where there are many duplicate elements in the
8+
* array being sorted.
9+
*
10+
* The idea behind three-way partitioning is to divide the array into three sections: elements less than the pivot,
11+
* elements equal to the pivot, and elements greater than the pivot. By doing so, we can avoid unnecessary comparisons
12+
* and swaps with duplicate elements, making the sorting process more efficient.
13+
*
14+
* Complexity Analysis:
15+
* Time:
16+
* - Worst case (poor choice of pivot): O(n^2)
17+
* - Average case: O(nlogn)
18+
* - Best case: O(nlogn)
19+
*
20+
* By isolating the elements equal to the pivot into their correct positions during the partitioning step, three-way
21+
* partitioning efficiently handles duplicates, preventing the presence of many duplicates in the array from causing
22+
* the time complexity of QuickSort to degrade to O(n^2).
23+
*
24+
* In the worst case where the pivot selected is consistently the smallest or biggest element in the array, the
25+
* partitioning of the array around the pivot will be extremely unbalanced, leading to a recurrence relation of:
26+
* T(n) = T(n-1) + O(n) => O(n^2). However, the likelihood of this happening is extremely low since pivot selection is
27+
* randomised.
28+
*
29+
* Space:
30+
* - O(1) since sorting is done in-place
31+
*/
32+
33+
public class QuickSort {
34+
/**
35+
* Sorts the given array in-place in non-decreasing order.
36+
*
37+
* @param arr array to be sorted.
38+
*/
39+
public static void sort(int[] arr) {
40+
int n = arr.length;
41+
quickSort(arr, 0, n - 1);
42+
}
43+
44+
/**
45+
* Recursively sorts the sub-array from index 'start' to index 'end' in non-decreasing order
46+
* using the QuickSort algorithm.
47+
*
48+
* @param arr the array containing the sub-array to be sorted.
49+
* @param start the starting index (inclusive) of the sub-array to be sorted.
50+
* @param end the ending index (inclusive) of the sub-array to be sorted.
51+
*/
52+
public static void quickSort(int[] arr, int start, int end) {
53+
if (start < end) {
54+
int[] newIdx = partition(arr, start, end);
55+
if (newIdx != null) {
56+
quickSort(arr, start, newIdx[0]);
57+
quickSort(arr, newIdx[1], end);
58+
}
59+
}
60+
}
61+
62+
/**
63+
* Partitions the sub-array from index 'start' to index 'end' around a randomly selected pivot element.
64+
* The elements less than or equal to the pivot are placed on the left side, and the elements greater than
65+
* the pivot are placed on the right side.
66+
*
67+
* @param arr the array containing the sub-array to be partitioned.
68+
* @param start the starting index (inclusive) of the sub-array to be partitioned.
69+
* @param end the ending index (inclusive) of the sub-array to be partitioned.
70+
* @return An integer array containing the indices that represent the boundaries
71+
* of the three partitions: [ending idx of the < portion of the array, starting idx of the > portion of the array]
72+
* if length of arr > 1, else null
73+
*/
74+
private static int[] partition(int[] arr, int start, int end) {
75+
// ___<___ ___=___ ___IP___ ___>___
76+
// ^ ^ ^ ^
77+
78+
if (arr.length > 1) {
79+
int pivotIdx = random(start, end); // pick a pivot
80+
int pivot = arr[pivotIdx];
81+
82+
swap(arr, pivotIdx, start); // swap the pivot to the start of the array, arr[start] is now our = portion
83+
// of the array
84+
85+
int eqStart = start;
86+
int eqEnd = start;
87+
int ipStart = start + 1;
88+
int ipEnd = end;
89+
90+
while (ipStart <= ipEnd) {
91+
int curr = arr[ipStart];
92+
// case 1: < pivot
93+
if (curr < pivot) {
94+
swap(arr, ipStart, eqStart); // do a swap with eqStart
95+
96+
// increment eqStart, ipStart, and eqEnd
97+
eqStart++;
98+
ipStart++;
99+
eqEnd++;
100+
// case 2: = pivot
101+
} else if (curr == pivot) {
102+
// simply increment eqEnd and ipStart
103+
eqEnd++;
104+
ipStart++;
105+
// case 3: > pivot
106+
} else {
107+
swap(arr, ipStart, ipEnd); // do a swap with ipEnd
108+
// decrement ipEnd
109+
ipEnd--;
110+
}
111+
}
112+
int[] result = {eqStart - 1, eqEnd}; //return
113+
return result;
114+
} else {
115+
return null;
116+
}
117+
}
118+
119+
/**
120+
* Swaps the elements at indices 'i' and 'j' in the given array.
121+
*
122+
* @param arr the array in which the elements should be swapped.
123+
* @param i the index of the first element to be swapped.
124+
* @param j the index of the second element to be swapped.
125+
*/
126+
private static void swap(int[] arr, int i, int j) {
127+
int temp = arr[i];
128+
arr[i] = arr[j];
129+
arr[j] = temp;
130+
}
131+
132+
/**
133+
* Generates a random integer within the range [start, end].
134+
*
135+
* @param start the lower bound of the random integer (inclusive).
136+
* @param end the upper bound of the random integer (inclusive).
137+
* @return a random integer within the specified range.
138+
*/
139+
private static int random(int start, int end) {
140+
return (int) (Math.random() * (end - start + 1)) + start;
141+
}
142+
143+
}

test/algorithms/quickSort/hoares/QuickSortTest.java renamed to test/algorithms/quickSort/lomuto/QuickSortTest.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
package test.algorithms.quickSort.hoares;
1+
package test.algorithms.quickSort.lomuto;
22

33
import org.junit.Test;
44

5-
import src.algorithms.sorting.quickSort.hoares.QuickSort;
5+
import src.algorithms.sorting.quickSort.lomuto.QuickSort;
66

77
import java.util.Arrays;
88

@@ -34,16 +34,22 @@ public void test_selectionSort_shouldReturnSortedArray() {
3434
int[] fifthResult = Arrays.copyOf(fifthArray, fifthArray.length);
3535
QuickSort.sort(fifthResult);
3636

37+
int[] sixthArray = new int[] {1,1,1,1,1,1,1,1,1,1,1,1,1,1};
38+
int[] sixthResult = Arrays.copyOf(sixthArray, sixthArray.length);
39+
QuickSort.sort(sixthResult);
40+
3741
Arrays.sort(firstArray); // get expected result
3842
Arrays.sort(secondArray); // get expected result
3943
Arrays.sort(thirdArray); // get expected result
4044
Arrays.sort(fourthArray); // get expected result
4145
Arrays.sort(fifthArray); // get expected result
46+
Arrays.sort(sixthArray); // get expected result
4247

4348
assertArrayEquals(firstResult, firstArray);
4449
assertArrayEquals(secondResult, secondArray);
4550
assertArrayEquals(thirdResult, thirdArray);
4651
assertArrayEquals(fourthResult, fourthArray);
4752
assertArrayEquals(fifthResult, fifthArray);
53+
assertArrayEquals(sixthResult, sixthArray);
4854
}
4955
}

test/algorithms/quickSort/paranoid/QuickSortTest.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ public void test_selectionSort_shouldReturnSortedArray() {
3434
int[] fifthResult = Arrays.copyOf(fifthArray, fifthArray.length);
3535
QuickSort.sort(fifthResult);
3636

37+
int[] sixthArray = new int[] {1,1,1,1,1,1,1,1,1,1,1,1,1,1};
38+
int[] sixthResult = Arrays.copyOf(sixthArray, sixthArray.length);
39+
// testing for duplicate arrays of length >= 10, stack overflow is expected to happen
40+
try {
41+
QuickSort.sort(sixthResult);
42+
} catch (StackOverflowError e) {
43+
System.out.println("Stack overflow occurred for sixthResult");
44+
}
45+
3746
Arrays.sort(firstArray); // get expected result
3847
Arrays.sort(secondArray); // get expected result
3948
Arrays.sort(thirdArray); // get expected result

0 commit comments

Comments
 (0)