I understand the concept of quicksort, and most of the implementations I have seen largely make sense to me. However the one by Robert Sedgewick does not, his implementation is along the lines of:
private void sort(int lo, int hi) {
if (hi <= lo) return;
int j = partition(lo, hi);
sort(lo, j-1);
sort(j+1, hi);
}
private int partition(int lo, int hi) {
int start = lo;
int partition = hi + 1;
int pivot = theArray[lo];
while (true) {
// left to right
while (less(theArray[++start], pivot))
if (start == hi) break;
// right to left
while (less(pivot, theArray[--partition]))
if (partition == lo) break;
// check if pointers cross
if (start >= partition) break;
exch(theArray, start, partition);
}
// place pivot at partition
exch(theArray, lo, partition);
return partition;
}
private boolean less(int v, int w) {
return v < w;
}
private void exch(int[] a, int i, int j) {
int swap = a[i];
a[i] = a[j];
a[j] = swap;
}
I understand all of it except for the partition function. Particularly what happens in the while(true) portion
Let's make some "brain debug"
int pivot = theArray[lo];
Fixes 1st element as pivot value
while (less(theArray[++start], pivot))
if (start == hi) break;
Finds the first element for exchange - the leftmost one bigger than pivot
while (less(pivot, theArray[--partition]))
if (partition == lo) break;
Finds the second element for exchange - the rightmost one less than pivot
if (start >= partition) break;
Breaks while (true) loop when there is no more elements to exchange
exch(theArray, start, partition);
Exchanges elements found
Example [5 3 8 9 2]:
5 is pivot
first 'bad' element is 8
second one is 2
they are exchanged
[5 3 2 9 8]
now first index stops at 4 (hi), second index stops at 2, loop breaks, and 5 exchanges with 2.
Result: [2 3 5 9 8]
Subranges (2,3,5) and (9,8) are new partitions
Related
I am working on evaluating the Merge Sort algorithm and counting the critical operations. While technically this is homework, it is from a prior assignment and the code is a scaled down version to only show the area in question. I am trying to better understand my issue and debug my critical operations count to be accurately portrayed.
The project was to implement Merge Sort and evaluate it. The area of concern is counting critical operations and determining the deviation of the count by randomly filling an array with 10 different sizes and each size ran 50 different times (with different random data). My findings were that for each size the number of critical operations always ended the same (e.g. array of size 10 came to 68 critical operations regardless of the data) leaving a critical operations deviation of 0.
The professor stated this was inaccurate and there was something wrong with my program as it should produce different counts for differing data for each array length. I am trying to figure out what in my program is inaccurate causing this issue. I have checked that each pass is producing different array data and that this data is being passed to the sorting algorithm and properly sorted.
Below is my code that I wrote that, again, regardless of the data it still produces the same critical operations count. Can anyone pin point my issue? Regardless of what I do to the count it always produces the same value.
public class MergeSortSingle {
public static int count = 0;
private MergeSortSingle() { }
private static void merge(Comparable[] a, Comparable[] aux, int lo, int mid, int hi) {
// copy to aux[]
for (int k = lo; k <= hi; k++) {
count++;
aux[k] = a[k];
}
// merge back to a[]
int i = lo, j = mid+1;
for (int k = lo; k <= hi; k++) {
if (i > mid) {
count++;
a[k] = aux[j++];
}
else if (j > hi) {
count++;
a[k] = aux[i++];
}
else if (less(aux[j], aux[i])) {
count++;
a[k] = aux[j++];
}
else {
count++;
a[k] = aux[i++];
}
}
}
private static void sort(Comparable[] a, Comparable[] aux, int lo, int hi) {
if (hi <= lo) return;
int mid = lo + (hi - lo) / 2;
sort(a, aux, lo, mid);
sort(a, aux, mid + 1, hi);
merge(a, aux, lo, mid, hi);
}
public static void sort(Comparable[] a) {
Comparable[] aux = new Comparable[a.length];
sort(a, aux, 0, a.length-1);
}
private static boolean less(Comparable v, Comparable w) {
return v.compareTo(w) < 0;
}
private static void show(Comparable[] a) {
System.out.println("\nAfter Sorting completed:");
System.out.print(Arrays.toString(a));
System.out.println();
}
public static void main(String[] args) {
int length = 10;
Comparable[] a = new Comparable[length];
for (int w = 0; w < length; w++) {
a[w] = (int) (Math.random() * 10000 + 1);
}
System.out.println("Before Sorting:");
System.out.print(Arrays.toString(a));
System.out.println();
MergeSortSingle.sort(a);
show(a);
System.out.println("\nCounter = " + count);
}
}
Sample Output 1:
Before Sorting:
[9661, 4831, 4865, 3383, 1451, 3029, 5258, 4788, 9463, 8971]
After Sorting completed:
[1451, 3029, 3383, 4788, 4831, 4865, 5258, 8971, 9463, 9661]
Counter = 68
Sample Output 2:
Before Sorting:
[9446, 230, 9089, 7865, 5829, 2589, 4068, 5608, 6138, 372]
After Sorting completed:
[230, 372, 2589, 4068, 5608, 5829, 6138, 7865, 9089, 9446]
Counter = 68
The merge sort code was utilized from:
http://algs4.cs.princeton.edu/22mergesort/Merge.java.html
You only counting while merging the sub-arrays - you use the counter for copying the array tro aux - that will always be the same number of operations, and then you use it again at the for loop - you have 4 paths there, and each of them increments the counter - again, a fixed number of times.
you have to count comarasions as well at sort - if (hi <= lo) - its an operations. If it fails - it's another operation.
So I think my partition method works but I cannot understand or figure out how to do the kthSmallest method. I no longer get out of bounds errors with my partition method which leads me to think that it works and with testing it seems to work. However, my kthSmallest method often gets stuck in an infinite loop and when it does return a value, it is never the correct value.
I have seen examples online that place the pivot between the two subarrays however for our assignment the pivot is always at the end so I often get confused looking at these examples.
Here is what I have:
class Median{
static int kthSmallest(int[] arr, int left, int right, int k){
int divider = partition(arr, left, right);
if(divider == k-1){
return arr[right];
}
else if(divider > k-1){
return kthSmallest(arr, left, divider-1, k);
}
else{
return kthSmallest(arr, divider, right, (k - divider-1));
}
}
static int partition(int[] arr, int left, int right){
int pivot = arr[right];
int index = left;
for(int i = left; i < right-1; i++){
if(arr[i] < pivot){
swap(arr, index, i);
index++;
}
}
printArr(arr);
System.out.println("divider: " + index);
return index;
}
static void swap(int[] arr, int i, int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
static void printArr(int[] arr){
System.out.println();
for(int index = 0; index < arr.length - 1; index++){
System.out.print(arr[index] + ", ");
}
System.out.print(arr[arr.length-1]);
System.out.println();
}
public static void main(String[] args){
int[] arr = {34,-1,0,5,3};
printArr(arr);
//System.out.println(partition(arr, 0, arr.length-1));
//printArr(arr);
System.out.println(kthSmallest(arr, 0, arr.length - 1, 2));
}
}
It seems like what you are trying to do is implement the QuickSelect algorithm, so I recommend taking a look at the Wikipedia article.
Looking at your code, I think you have misinterpreted the "pivot at the end" bit. I believe that what your requirements want is to select the pivot from the end, and then place it in between the two sublists so that your list looks right. For example,
34 -1 0 5 3
should become
-1 0 3 34 5
not
-1 0 34 5 3, where your pivot is not doing its job correctly.
There are also a few problems with your kthSmallest method. Double check the values you pass along in your recursion and you also are missing a case which might cause infinite recursion. Spoilers for if you are absolutely stuck:
You since you don't re-index the list, you should change '(k - divider -1)' to just k.
If left == right then you get unnecessary recursion, so you should just return in that case.
In your partition method, make sure you iterate far enough in the part of the list you are partitioning. Again, just in case you're really stumped:
for(int i = left; i < right-1; i++) should become for(int i = left; i <= right-1; i++)
For refreshing some Java I tried to implement a quicksort (inplace) algorithm that can sort integer arrays. Following is the code I've got so far. You can call it by sort(a,0,a.length-1).
This code obviously fails (gets into an infinite loop) if both 'pointers' i,j point each to an array entry that have the same values as the pivot. The pivot element v is always the right most of the current partition (the one with the greatest index).
But I just cannot figure out how to avoid that, does anyone see a solution?
static void sort(int a[], int left, int right) {
if (right > left){
int i=left, j=right-1, tmp;
int v = a[right]; //pivot
int counter = 0;
do {
while(a[i]<v)i++;
while(j>0 && a[j]>v)j--;
if( i < j){
tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
} while(i < j);
tmp = a[right];
a[right] = a[i];
a[i] = tmp;
sort(a,left,i-1);
sort(a,i+1,right);
}
}
When preforming a Quicksort I strongly suggest making a separate method for partitioning to make the code easier to follow (I'll show an example below). On top of this a good way of avoiding worst case run time is shuffling the array you're sorting prior to preforming the quick sort. Also I used the first index as the partitioning item instead of the last.
For example:
public static void sort (int[] a)
{
StdRandom.shuffle(a);
sort(a, 0, a.length - 1);
}
private static void sort(int[] a, int lo, int hi)
{
if (hi <= lo) return;
int j = partition(a, lo, hi) // the addition of a partitioning method
sort(a, lo, j-1);
sort(a, j+1, hi);
}
private static int partition(int[] a, int lo, int hi)
{
int i = lo, j = hi + 1, tmp = 0;
int v = a[lo];
while (true)
{
while (a[i++] < v) if (i == hi) break;
while (v < a[j--]) if (j == lo) break;
if (i >= j) break;
tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
tmp = a[lo];
a[lo] = a[j];
a[j] = temp;
return j;
}
On top of this if you want a really good example on how Quicksort works (as a refresher) see here.
This should work (will check for correctness in a bit, it works!):
EDIT: I previously made a mistake in error checking. I forgot to add 2 more conditions, here is the amended code.
public static void main (String[] args) throws java.lang.Exception
{
int b[] = {10, 9, 8, 7, 7, 7, 7, 3, 2, 1};
sort(b,0,b.length-1);
System.out.println(Arrays.toString(b));
}
static void sort(int a[], int left, int right) {
if (right > left){
int i=left, j=right, tmp;
//we want j to be right, not right-1 since that leaves out a number during recursion
int v = a[right]; //pivot
do {
while(a[i]<v)
i++;
while(a[j]>v)
//no need to check for 0, the right condition for recursion is the 2 if statements below.
j--;
if( i <= j){ //your code was i<j
tmp = a[i];
a[i] = a[j];
a[j] = tmp;
i++;
j--;
//we need to +/- both i,j, else it will stick at 0 or be same number
}
} while(i <= j); //your code was i<j, hence infinite loop on 0 case
//you had a swap here, I don't think it's needed.
//this is the 2 conditions we need to avoid infinite loops
// check if left < j, if it isn't, it's already sorted. Done
if(left < j) sort(a,left,j);
//check if i is less than right, if it isn't it's already sorted. Done
// here i is now the 'middle index', the slice for divide and conquer.
if(i < right) sort(a,i,right);
}
}
This Code in the IDEOne online compiler
Basically we make sure that we also swap the value if the value of i/j is the same as the pivot, and break out of the recursion.
Also there was a check in the pseudocode for the length, as if we have an array of just 1 item it's already sorted (we forgot the base case), I thought we needed that but since you pass in the indexes and the entire array, not the subarray, we just increment i and j so the algorithm won't stick at 0 (they're done sorting) but still keep sorting an array of 1. :)
Also, we had to add 2 conditions to check if the array is already sorted for the recursive calls. without it, we'll end up sorting an already sorted array forever, hence another infinite loop. see how I added checks for if left less than j and if i less than right. Also, at that point of passing in i and j, i is effectively the middle index we split for divide and conquer, and j would be the value right before the middle value.
The pseudocode for it is taken from RosettaCode:
function quicksort(array)
if length(array) > 1
pivot := select any element of array
left := first index of array
right := last index of array
while left ≤ right
while array[left] < pivot
left := left + 1
while array[right] > pivot
right := right - 1
if left ≤ right
swap array[left] with array[right]
left := left + 1
right := right - 1
quicksort(array from first index to right)
quicksort(array from left to last index)
Reference: This SO question
Also read this for a quick refresher, it's implemented differently with an oridnary while loop
This was fun :)
Heres some simple code I wrote that doesn't initialize to many pointers and gets the job done in a simple manner.
public int[] quickSort(int[] x ){
quickSortWorker(x,0,x.length-1);
return x;
}
private int[] quickSortWorker(int[] x, int lb, int ub){
if (lb>=ub) return x;
int pivotIndex = lb;
for (int i = lb+1 ; i<=ub; i++){
if (x[i]<=x[pivotIndex]){
swap(x,pivotIndex,i);
swap(x,i,pivotIndex+1);
pivotIndex++;
}
}
quickSortWorker(x,lb,pivotIndex-1);
quickSortWorker(x,pivotIndex+1,ub);
return x;
}
private void swap(int[] x,int a, int b){
int tmp = x[a];
x[a]=x[b];
x[b]=tmp;
}
As I was testing my quicksort I noticed another problem. Sometimes it arranges the array in alphabetical order and sometimes it does not. For example if I have p, o, j, l as my array it sorts it to j, o, l, p which is wrong because l should be before o However if I add a to the array it sorts to a, j, l, o, p which is correct. Why is this happening?
Code:
private ArrayList<String> sort(ArrayList<String> ar, int lo, int hi){
if (lo < hi){
int splitPoint = partition(ar, lo, hi);
sort(ar, lo, splitPoint);
sort(ar, splitPoint +1, hi);
}
return ar;
}
private int partition(ArrayList<String> ar, int lo, int hi){
String pivot = ar.get(lo);
lo--;
hi++;
while (true){
lo++;
hi--;
while (lo<hi && ar.get(lo).compareTo(pivot) < 0){
lo++;
}
while (hi>lo && ar.get(hi).compareTo(pivot) >= 0){
hi--;
}
if (lo<hi){
swap(ar, lo, hi);
}else {
return hi;
}
}
}
private ArrayList<String> swap(ArrayList<String> ar, int a, int b){
String temp = ar.get(a);
ar.set(a, ar.get(b));
ar.set(b, temp);
return ar;
}
As far as I can see, you are not changing the position of pivot element. You are just swapping hi with lo.
At the end, you should place the pivot at its correct position and then return it.
I think there is a danger in the last iteration of your loop.
Suppose you had an array 2,1.
Your pivot is 2.
The lo index increases until it finds an element above the pivot of 1 or meets hi.
In this case it will meet hi and both lo and hi will be pointing to 1.
At this point your partition function returns and reports that the array has been partitioned into:
{2},{1}
But this is incorrect because 1 is < than the pivot so should have been in the first partition.
Perhaps when lo meets hi you should perform an extra test to see whether the element at hi should be included in the left or right partition?
Here is an implementation of merge for mergesort, in Java that works:
void merge(int[] numbers, int low, int mid, int high) {
int helper[] = new int[numbers.length];
for (int i = low; i <= high; i++) {
helper[i] = numbers[i];
}
int lowone = low;
int lowtwo = mid + 1;
int count = low;
while (lowone <= mid || lowtwo <= high) {
if (lowone <= mid && lowtwo <= high) {
if (helper[lowone] <= helper[lowtwo]) {
numbers[count] = helper[lowone];
lowone++;
} else {
numbers[count] = helper[lowtwo];
lowtwo++;
}
}
else if (lowone <= mid) {
numbers[count] = helper[lowone];
lowone++;
}
else {
numbers[count] = helper[lowtwo];
lowtwo++;
}
count++;
}
}
//msort algorithm in case it's relevant
void msort(int[] arr, int low, int high) {
if (low < high) {
int mid = (low + high)/2;
msort(arr, low, mid);
msort(arr, mid + 1, high);
merge(arr, low, mid, high);
}
}
In my first attempt at merge I was trying to have the second half of the array contain the midpoint index instead of having it in the first half. Here is the relavent code (note there are only 4 changes from above):
int lowtwo = mid;
while (lowone < mid || lowtwo <= high) {
if (lowone < mid && lowtwo <= high) {
if (helper[lowone] <= helper[lowtwo]) {
numbers[count] = helper[lowone];
lowone++;
} else {
numbers[count] = helper[lowtwo];
lowtwo++;
}
}
else if (lowone < mid) {
numbers[count] = helper[lowone];
lowone++;
}
else {
numbers[count] = helper[lowtwo];
lowtwo++;
}
The modified version used with msort (above) does not correctly sort the list. Maybe it's obvious, but I can't seem to figure out why it doesn't work.
The problem comes because you change the meaning of mid in merge. The simplest way to look at it is an example. Let's say we have an array with indices:
[0 1 2 3 4]
Calling msort, you'll pass in 0 for low, and 4 for high. That means mid is computed as 2. So you now have the array divided as so (not in memory, just logically):
[0 1 2] [3 4]
Now, when merge is called it's passed in 2 for mid, which is the last index in array 1. However, in you're modified code you treat mid as the starting index for array 2. This makes the two arrays look like:
[0 1] [2 3 4]
Which is different then how everything treats it. An example where this falls down is if you're two array's (after sorting) look like (the numbers are now the values, not indices):
[8 12 14] [7 11]
However, under you're interpretation of mid, the arrays are:
[8 12] [14 7 11]
Which are no longer sorted. Thus you're merge function won't work.
It's because you've sorted the two sublists with the midpoint in the first subarray (in msort(...)) before doing the merge with the midpoint in the second subarray (in merge(...)).
Take the simplest example
[2,1]
is split like so (because mid == ((0+1)/2) == 0 )
[2] and [1]
which msort trivially sorts into
[2] and [1]
but then, by placing mid in the second list in the merge, you are in fact merging:
[] and [2,1]
which obviously results in [2,1], which is wrong!
The midpoint placement has to be consistent in both splitting and the merging of the two subarrays.