Quicksort Empirical Analysis StackOverflowError - java

I'm attempting to perform an analysis on Quicksort but I'm running into an inconsistent StackOverflowError. I believe it's probably due to unbound recursion with higher data sets but I can't figure out how to fix it. My partition might also be off but debugging hasn't worked out too well for me.
The data set I'm using is a set of randomly generated int arrays of size 10,000 using a pivot (arr[0])
Exception in thread "main" java.lang.StackOverflowError
at AlgorithmTiming.quickSort(AlgorithmTiming.java:78)
at AlgorithmTiming.quickSort(AlgorithmTiming.java:82)
private int[] quickSort(int[] arr, int low, int high, int pivotType){
if (low < high)
{
/* pi is partitioning index, arr[pi] is
now at right place */
int pi = partition(arr, low, high, pivotType);
// Recursively sort elements before
// partition and after partition
quickSort(arr, low, pi-1, pivotType);
quickSort(arr, pi+1, high, pivotType);
}
return arr;
}
private int partition(int arr[], int low, int high, int pivotType)
{
Random rand = new Random();
int pivot = arr[0]; //Default Pivot
if(pivotType == 2) pivot = arr[0] + arr[arr.length / 2] + arr[arr.length-1]; //Median Pivot
else if(pivotType == 3) pivot = arr[rand.nextInt(arr.length)]; //Random Pivot
int i = (low-1); // index of smaller element
for (int j=low; j<=high-1; j++)
{
// If current element is smaller than or
// equal to pivot
if (arr[j] <= pivot)
{
i++;
// swap arr[i] and arr[j]
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// swap arr[i+1] and arr[high] (or pivot)
int temp = arr[i+1];
arr[i+1] = arr[high];
arr[high] = temp;
return i+1;
}

I'm not going to attempt to debug your code. (For a start, you did not provide an MCVE ... or even a stacktrace.)
However, consider this:
Quicksort is a recursive algorithm that works by "dividing" the sorting problem into two smaller sorting problems until the problems are trivial. This is the partitioning step.
If you are sorting N elements and you correctly divide the sort problems in half each time, then you will only need to log2N (or less) divisions to get down to the (trivial) problem of sorting one element.
And then from the Java perspective:
A StackOverflowError means that your recursion has gotten too deep.
So ... without looking at the code ... it is almost a safe bet that the problem is in the way that you have implemented the partitioning. That's where you should focus your attention.
I would suggest the following:
Eyeball the code to see if you can figure out why the partitioning isn't choosing correct partition boundaries; i.e. low, pi, and hi.
Use a debugger to see what the code is doing.
Add traceprints to see what the values of low, pi, and hi are, and how they change as the algorithm progresses.
The other thing to note is that classic quicksort has an edge case where the choice of partition can lead to O(N2) rather than O(NlogN) behavior; see https://en.wikipedia.org/wiki/Quicksort#Worst-case_analysis. This corresponds to the case where sorting N elements requires N-deep recursion. That will lead to StackOverflowError problems in Java, for large enough N.

Related

Quicksort implementation bug

After a while, I was trying to implement quickSort on my own.
To me the implementation looks fine, but some test cases are apparently failing.
class MyImplementation {
public int[] sortArray(int[] nums) {
quicksort(nums, 0, nums.length - 1);
return nums;
}
private void quicksort(int[] nums, int lo, int hi){
if(lo < hi){
int j = partition(nums, lo, hi);
quicksort(nums, lo, j-1);
quicksort(nums, j+1, hi);
}
}
private int partition(int[] nums, int lo, int hi){
int pivot = nums[lo];
int head = lo + 1;
int tail = hi;
while(head < tail){
while(head < hi && nums[head] < pivot) ++head;
while(nums[tail] > pivot) --tail;
if(head < tail){
swap(nums, head, tail);
++head;
--tail;
}
}
if(nums[head] < pivot)
swap(nums, lo, head);
else
swap(nums, lo, head - 1);
return head;
}
private void swap(int[] nums, int i, int j){
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
It's passing in many cases, like these :-
[5,2,3,1]
[5,1,1,2,0,0]
[7,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5]
but it's failing in the following test cases :-
[5,71,1,91,10,2,0,0,13,45,7]
the output I am getting is :-
[0,0,1,2,5,10,7,71,13,45,91]
instead of :-
[0,0,1,2,5,7,10,13,45,71,91]
I know, I can copy from somewhere to get the correctly working code, but can we figure out why this code is not working from an Algorithmic point of view!!
What is the logical flaw in this code?
How can we fix this with minimum changes and get a perfectly working code?
The currently accepted answer fixes the issue, but actually, the Lomuto algorithm (which is the algorithm being implemented) should be able to use j+1 as start for the second partition, since the pivot is expected to be at j and therefore doesn't need to be part of recursive sorts.
The real problem is that your partition method doesn't always return the index of the pivot:
if(nums[head] < pivot)
swap(nums, lo, head);
else
swap(nums, lo, head - 1);
return head;
In case of the else, the pivot is not swapped to index head, but to head - 1, and therefore the returned index should be head - 1 in that case.
Here is a simple fix:
if(nums[head] < pivot)
swap(nums, lo, head);
else
swap(nums, lo, --head);
return head;
That's quite subtle. The bug is here:
private void quicksort(int[] nums, int lo, int hi){
if(lo < hi){
int j = partition(nums, lo, hi);
quicksort(nums, lo, j-1);
quicksort(nums, j+1, hi);
// ^^^ j+1 must be j
}
}
The most subtle part is that it is not in general necessary to use j as the starting point of the "high" part to recurse on, but your partitioning algorithm requires it. So you will see various examples that recurse on different partitions, and they can be correct as well, in combination with whatever partitioning algorithm they use, but there is not much freedom to mix.
Your partitioning algorithm is based on Hoare's partitioning algorithm (see below), not Lomuto's algorithm. Lomuto's partitioning algorithm puts the pivot in its final position and returns its index, so that index can be skipped in the recursion. Hoare's partitioning algorithm, or your variant of it, does not quite do that. Eg in the case that the else branch at the end of your partitioning algorithm is taken, the pivot ends up at head - 1, while element at head has some arbitrary value not-less-than the pivot, all we know is that it belongs in the "high" partition but it has not been sorted yet and cannot be skipped.
In combination with Hoare's partitioning, it is common to see the recursion on [lo .. p] (where p is the index returned by the partitioning algorithm) and [p+1 .. hi], but that is not suitable in this case because your version of it returns head instead of tail, effectively making p one higher, so the partitions become [lo .. p-1] and [p .. hi].
As for this being similar to Hoare partitioning, consider that the important part of this partitioning algorithm looks like this:
while(head < tail){
while(head < hi && nums[head] < pivot) ++head;
while(nums[tail] > pivot) --tail;
if(head < tail){
swap(nums, head, tail);
++head;
--tail;
}
}
Outer loop that iterates until the indexes "cross" (or meet)
Two inner loops that iterate inwards, skipping elements that are already in the correct partition, to find two elements that are both in the wrong partition
One swap to simultaneously put two elements into their respective partitions
And how does Hoare's partition algorithm work, well, the same way really (from wikipedia):
loop forever
// Move the left index to the right at least once and while the element at
// the left index is less than the pivot
do i := i + 1 while A[i] < pivot
// Move the right index to the left at least once and while the element at
// the right index is greater than the pivot
do j := j - 1 while A[j] > pivot
// If the indices crossed, return
if i >= j then return j
// Swap the elements at the left and right indices
swap A[i] with A[j]
This uses an early-exit instead of a while-loop as the outer loop, and therefore doesn't need an if around the swap, but apart from such minor details, it is the same thing.
If it is expressed this way, as opposed to how it is expressed in this question, it is less reasonable to modify it to always return the pivot index (expressed this way, it is also much less obvious what guarantee it actually makes on its return value). With the variant found in this question, that sort of naturally falls out as a possible modification, as seen in trincots answer.
By contrast, the important part of Lomuto's partitioning algorithm works like this (also from wikipedia)
for j := lo to hi - 1 do
// If the current element is less than or equal to the pivot
if A[j] <= pivot then
// Move the temporary pivot index forward
i := i + 1
// Swap the current element with the element at the temporary pivot index
swap A[i] with A[j]
This is based on a totally different principle.

Can someone explain this quicksort algorithm to me?

I'm a little confused on quicksort.
For example, with this algorithm taken from programcreek.com using the middle element as the pivot point:
public class QuickSort {
public static void main(String[] args) {
int[] x = { 9, 2, 4, 7, 3, 7, 10 };
System.out.println(Arrays.toString(x));
int low = 0;
int high = x.length - 1;
quickSort(x, low, high);
System.out.println(Arrays.toString(x));
}
public static void quickSort(int[] arr, int low, int high) {
if (arr == null || arr.length == 0)
return;
if (low >= high)
return;
// pick the pivot
int middle = low + (high - low) / 2;
int pivot = arr[middle];
// make left < pivot and right > pivot
int i = low, j = high;
while (i <= j) {
while (arr[i] < pivot) {
i++;
}
while (arr[j] > pivot) {
j--;
}
if (i <= j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
i++;
j--;
}
}
// recursively sort two sub parts
if (low < j)
quickSort(arr, low, j);
if (high > i)
quickSort(arr, i, high);
}
}
Can someone explain the 2 recursive calls at the bottom, as well as why there is a need to create an i and j variable to copy the left and right markers.
Also, can someone explain the difference between a quicksort algorithm using the middle element vs using the first or last element as the pivot point? The code looks different in a sense that using the last / first element as the pivot point is usually written with a partition method instead of the code above.
Thanks!
Quicksort is based on divide and conquer method, first we take a pivot element and put all elements that are less than this pivot element on the left and all the elements that are greater than this pivot element on the right and after that we recursively perform the same thing on both sides of pivot for left side Quicksort(array,low,pivot-1) for right side Quicksort(array,low,pivot+1)
This was the answer of your first question
and now what is the difference between choosing the middle or first element as pivot so
when we choose first element as pivot after sorting when i becomes greater than j we swap the pivot element(first element) with j so that the element that we chose as pivot comes at the place where all elements less than it comes at the left side and all elements greater than it comes it the right side.
and when we choose the middle element as pivot its already in the middle so there's no need to swap it.
This is a variation of Hoare partition scheme. The "classic" Hoare partition scheme increments i and decrements j before comparing to pivot. Both this example and the questions example include the partition logic in the main function.
void quickSort(int a[], size_t lo, size_t hi)
{
int pivot = a[lo+(hi-lo)/2];
int t;
if(lo >= hi)
return;
size_t i = lo-1;
size_t j = hi+1;
while(1)
{
while (a[++i] < pivot);
while (a[--j] > pivot);
if (i >= j)
break;
t = a[i];
a[i] = a[j];
a[j] = t;
}
QuickSort(a, lo, j);
QuickSort(a, j+1, hi);
}
The questions code increments i and decrements j after comparing to pivot.
The partition logic splits up a partition so that the left side <= pivot, right side >= pivot. The pivot and elements equal to pivot can end up anywhere on either side, and may not end up in their sorted position until a base case of a sub-array of size 1 is reached.
The reason for using the middle element for pivot is that choosing the first or last element for pivot will result in worst case time complexity of O(n^2) if the array is already sorted or reverse sorted. The example in this answer will fail if the last element is used for pivot (but the questions example will not).

How can I implement the recursion in my Quicksort algorithm?

I'm trying to implement quicksort in Java to learn basic algorithms. I understand how the algo works (and can do it on paper) but am finding it hard to write it in code. I've managed to do step where we put all elements smaller than the pivot to the left, and larger ones to the right (see my code below). However, I can't figure out how to implement the recursion part of the algo, so sort the left and right sides recursively. Any help please?
public void int(A, p, q){
if(A.length == 0){ return; }
int pivot = A[q];
j = 0; k = 0;
for(int i = 0; i < A.length; i++){
if(A[i] <= pivot){
A[j] = A[i]; j++;
}
else{
A[k] = A[i]; k++;
}
}
A[j] = pivot;
}
Big Disclaimer: I did not write this piece of code, so upvotes is not needed. But I link to a tutorial which explains quicksort in detail. Gave me a much needed refreshment on the algorithm as well! The example given has very good comments that might just help you to wrap your head around it.
I suggest you adapt it to your code and write som tests for it to verify it works
Quicksort is a fast, recursive, non-stable sort algorithm which works by the divide and conquer principle. Quicksort will in the best case divide the array into almost two identical parts. It the array contains n elements then the first run will need O(n). Sorting the remaining two sub-arrays takes 2 O(n/2). This ends up in a performance of O(n log n).
In the worst case quicksort selects only one element in each iteration. So it is O(n) + O(n-1) + (On-2).. O(1) which is equal to O(n^2).*
public class Quicksort {
private int[] numbers;
private int number;
public void sort(int[] values) {
// check for empty or null array
if (values ==null || values.length==0){
return;
}
this.numbers = values;
number = values.length;
quicksort(0, number - 1);
}
private void quicksort(int low, int high) {
int i = low, j = high;
// Get the pivot element from the middle of the list
int pivot = numbers[low + (high-low)/2];
// Divide into two lists
while (i <= j) {
// If the current value from the left list is smaller than the pivot
// element then get the next element from the left list
while (numbers[i] < pivot) {
i++;
}
// If the current value from the right list is larger than the pivot
// element then get the next element from the right list
while (numbers[j] > pivot) {
j--;
}
// If we have found a value in the left list which is larger than
// the pivot element and if we have found a value in the right list
// which is smaller than the pivot element then we exchange the
// values.
// As we are done we can increase i and j
if (i <= j) {
exchange(i, j);
i++;
j--;
}
}
// This is the recursion part you had trouble with i guess?
// Recursion
if (low < j)
quicksort(low, j);
if (i < high)
quicksort(i, high);
}
private void exchange(int i, int j) {
int temp = numbers[i];
numbers[i] = numbers[j];
numbers[j] = temp;
}
}
Link to tutorial

How can I avoid a stack overflow error with quicksorting algorithm implementation

//
If, like me, the goal is to use the algorithm in some other code (and not to code the algorithm itself), Arrays.sort (Documentation here) did the job!
//
I keep getting this error :
Exception in thread "main" java.lang.StackOverflowError
at language.LanguageDetection.sort(LanguageDetection.java:140)
at language.LanguageDetection.sort(LanguageDetection.java:141)
at language.LanguageDetection.sort(LanguageDetection.java:141)
And this line :
at language.LanguageDetection.sort(LanguageDetection.java:141)
Fills the rest of the console output.
I am a beginner in java and I am trying to implement a quick sort algorithm in order to sort a text file containing all dictionary words, so I can then quickly search through it quickly.
I really don't know if this is the way to go or if there would be a more suitable sorting algorithm for this problem, but here is the code :
public static void sort(String[] unsorted, int startIndex, int endIndex)
{
if(endIndex - startIndex <= 0) return; //Ends the recursion when unsorted array is of size 1
String temp;
char pivot = unsorted[endIndex].charAt(0);
int j = startIndex;
for(int i = startIndex; i < endIndex; i++)
{
if(unsorted[i].charAt(0) < pivot)
{
temp = unsorted[j];
unsorted[j] = unsorted[i];
unsorted[i] = temp;
j++;
}
}
temp = unsorted[j];
unsorted[j] = unsorted[endIndex];
unsorted[endIndex] = temp;
sort(unsorted, startIndex, j - 1);
sort(unsorted, j + 1, endIndex);
}
I don't want to have all the corrected version of the code, just some answers about, what I am coding wrong and if there is a better way of doing this.
Thanks in advance.
The simple solution:
Quicksort partitions the input array into two subarrays, and then sorts the subarrays. To sort the subarrays, first recurse to sort the smaller one, and then loop to sort the larger one.
Since each recursive call is <= half the size of its caller, the recursive depth is limited to log(N).

counting quicksort comparisons

I implemented a simple quick sort (code below) to count the average and worst comparison made by quicksort. I declared a global variable to hold the counter for comparisons. I placed 3 counters in different positions that I thought would count the comparisons, the problem is the counter sum does not match the theoretical value of the total number of comparisons made by quick sort. I tried to solve this problem for hours but came up short. I really appreciate if you can point me where I should put the counters and why I should place them there. I assumed a counter should go where ever a comparison is made. apparently I'm wrong.
public int[] quickSort(int[] array, int start, int end){
if (start < end){
counter++;//1st comparison here
int pivot;
pivot = Partition(array, start, end);
quickSort(array, start, pivot - 1);
quickSort(array, pivot + 1, end );
}
return array;
}
private int Partition(int[] array, int start, int end) {
int pivot = array[end];
int i = start - 1;
for(int j = start; j <= end - 1; j++){
counter++;//2nd comparison here
if (array[j] <= pivot){
counter++;//3rd comparison here
i = i + 1;
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
int temp = array[i+1];
array[i+1] = array[end];
array[end] = temp;
return i + 1;
}
For the theory, only the comparisons of array elements are counted, not the comparisons of indices to the bounds, so you should only leave the second counter++; (you need to increment the counter independently of the result of the comparison).
Then there is the question against which theoretical values you compare. There are different implementations of quicksort which use slightly different numbers of comparisons. In particular, your choice of the pivot makes no attempt to avoid extreme values, so this implementation will easily degrade to O(n^2) behaviour.

Categories

Resources