CSC 207 Grinnell College Fall, 2018
 
Algorithms and Object-Oriented Design
 

Sorting

Goals

This laboratory exercises provides some practice with the insertion sort, quicksort, and merge sort and outlines a mechanism for determining experimentally the efficiency of these sorting algorithms.

Introduction

Many applications require the maintenance of ordered data. In Java (and Scheme and C), such data often are structured in an array (or vector). The reading for this lab provides considerable detail on both the insertion sort and quicksort. You should read that material before proceeding further in this lab.

As with other labs, please submit your work for this lab on Github Classroom, using the URL link, https://classroom.github.com/g/f6wSxS49. Material submitted should contain three files, each clearly labeled:

The Insertion Sort (Review)

The insertion sort assumes that the first part of an array is ordered and then adds successive items to this array segment until the entire array is sorted.

A main helper method adds add one item to an ordered array segment. More specifically, suppose items A[0], ..., A[k-1] are ordered in array A. The following code inserts an item into the array, so that items A[0], ..., A[k] become ordered:


int i = k-1;
while ((i >= 0) && a[i] > item){
   a[i+1] = a[i];
   i--;
}
a[i+1] = item;
  1. Consider the array

    A:  2 3 5 7 9 10 13 18 24 27 33 35 37

    Review the steps in the above code for this array segment with k-1 = 7 and with item having the value 17. Explain what happens and why.

Using this basic insertion step, an array A can be sorted iteratively. This outline gives rise the the following code, called an insertion sort.


public static void insertionSort (int [] a) {
// method to sort using the insertion sort
   for (int k = 1; k < a.length; k++) {
      int item = a[k];
      int i = k-1;
      while ((i >= 0) && a[i] > item){
         a[i+1] = a[i];
         i--;
      }
      a[i+1] = item;
   }
}
  1. Write a few sentences explaining how this code works.

Quicksort: An Example of Divide-and-Conquer Algorithms

The Quicksort, originally devised by C. A. R. Hoare [Computing Journal, Volume 5 (1962), pages 10-15], proceeds by applying an initial step that partitions the array into two pieces. The algorithm then is applied recursively to each of the two pieces. This approach of dividing processing into pieces and applying recursion to the two pieces turns out to be extremely general and powerful. Such an approach is called a divide and conquer algorithm.

The reading for this lab identifies two main approaches for applying this divide-and-conquer approach to sorting. This lab reviews the two approaches quickly and then asks you to write the details for another variation (based on a modified pictoral loop invariant).

Quicksort Approach 1 (Review): Loop Invariant with Small Elements on Left; Large Elements on Right

The first approach follows the approach you implemented in the Lab on Pictoral Loop Invariants. In summary, sorting relied upon the function

   int partition (int a[], int left, int right)

which, in turn, was based on the following loop invariant:

quicksort details 1

Incorporating partition into the quicksort function yields code, such as the following:

public static void quicksort (int [] a) {
// method to sort using the quicksort
    quicksortKernel (a, 0, a.length-1);
}

private static void quicksortKernel (int [] a, int first, int last) {
    int left=first+1;
    int right=last;
    int temp;

    while (right >= left) {
        // search left to find small array item
        while ((right >= left) && (a[first] <= a[right]))
            right--;
        // search right to find large array item
        while ((right >= left) && (a[first] >= a[left]))
            left++;
        // swap large left item and small right item, if needed
        if (right > left) {
            temp = a[left];
            a[left] = a[right];
            a[right] = temp;
        }
    }
    // put a[first] in its place
    temp = a[first];
    a[first] = a[right];
    a[right] = temp;

    // recursively apply algorithm to a[first]..a[right-1] 
    // and a[right+1]..a[last], provided these segments contain >= 2 items
    if (first < right-1)
        quicksortKernel (a, first, right-1);
    if (right+1 < last)
        quicksortKernel (a, right+1, last);   
}

As a review, write careful responses to the following statements:

  1. Explain why each loop contains the condition right >= left. Given an example where the code would fail if each right >= left test were omitted.

  2. The code swaps a[left] and a[right] only if right > left. Give an example to show how the code could fail if the swap were done without the test for right > left.

This code illustrates the husk-and-kernel programming style which arose frequently earlier in CSC 151.

  1. Write a few sentences explaining how this code works. What does the husk procedure do? Why is the husk needed?

  2. Program SortShell2.java provides a shell for testing the quicksort. Program SortShell3.java is the same as SortShell2.java, except that a few lines have been added at the start of quicksortKernel.

    Write a few sentences explaining what this new code accomplishes; what is the logical effect of this change? For future reference in this lab, we will call this new version of quicksort a refined quicksort.

Quicksort Approach 2: Loop Invariant with Small Elements on Left; Large Elements Next

The second implementation of the quicksort is motivated by the following pictorial loop invariant.

quicksort details 2

This loop invariant motivates the following code to implement the basic step of the quicksort:

   // progress through array, 
   //     moving small elements to a[first+1] .. a[left-1]
   //     and moving large elements to a[left] .. a[right-1]
   while (right <= last)
     {
       if (a[first] < a[right])
          right++;
       else {
          // swap a[left] and a[right]
          temp = a[right];
          a[right] = a[left];
          a[left] = temp;
          left++;
          right++;
       }
     }
    // put a[first] in its place
    temp = a[first];
    a[first] = a[left-1];
    a[left-1] = temp;
  1. Review the code segment and explain why the "then" and "else" clauses of the if statement in the loop maintain the loop invariant.

  2. Explain why the swap at the end of the code swaps a[first] with a[left-1] rather than with a[left].

  3. The full code for this version of quicksort may be found in program SortShell4.java. Explain why the recursive calls involve left-2 and left.

  4. As noted, program SortShell4.java provides a shell for testing this alternative quicksort. Run it with several sets of test data.

Quicksort Approach 3: Loop Invariant with Small Elements in the Middle; Large Elements on the Right

The third implementation of the quicksort is motivated by the following pictorial loop invariant.

quicksort details 2
  1. Wrote a quicksort algorithm (following a husk and kernel) approach using this new loop invariant.

  2. Modify SortShell2.java to allow testing for this revised quicksort. Then describe the results of testing, and discuss how the testing might demonstrate that this variant of a quicksort works properly.

Merge Sort

The merge sort is motivated by the observation that merging two sorted arrays (or array segments) into another is reasonably straight forward:

In practice, some care is needed in coding, so that one copies just the relevant array segments and one does not go beyond the bounds of either initial array segment.

With this notion of merging, a merge sort proceeds as follows:

In this process, one needs to copy elements in one array into a second array. Since copying the array elements back to the original array takes time, the idea is to first merge from one array to a second, and then to merge from the second to the first. At the end, the sorted items will be in one of the two arrays, and some carefully bookkeeping can determine whether one copy is needed at the end to move data to the original array.

As already indicated, entire process depends upon merging two adjacent (and ordered) array segments into the corresponding place in a second array. In practice, this suggests the following method header:

      /** 
     * merge aInit[start1]..aInit[start1+mergeSize] with 
     *       aInit[start2]..Init[end2]
     *       with the result placed in aRes
     * Note:  it may be that start2 >= aInit.length, in which case, only the
     *        valid part of aInit[start1] is copied
     */
    private static void merge (int aInit [ ], int aRes [ ],
		   int start1, int start2, int end2)

Here, the ordered array segment aInit[start1]..aInit[start2-1] is merged with the following ordered array segment aInit[start2]..aInit[end2]

With this method implemented, the main merge sort algorithm becomes:

    /** 
     * sort the array a, using a merge sort
     */
    public static  void  mergeSort (int initArr [ ]) {
	int resArr [ ] = new int [initArr.length];
	int a0[ ] = initArr;
	int a1[ ] = resArr;
	boolean needCopyBack = false;  
	
	int mergeSize = 1;
	int start1;
	while (mergeSize < initArr.length)
	    {
		int end2;
		for (start1 = 0; start1 < initArr.length; start1 = end2) {
		    int start2 = start1 + mergeSize;
		    end2 = start2 + mergeSize;
		    merge (a0, a1, start1, start2, end2);
		}

		// swap a0, a1 pointers, so a0 is starting array for next merge
		int temp [ ] = a0;
		a0 = a1;
		a1 = temp;
		mergeSize *= 2;

		// keep track of which array holds initArr object
		needCopyBack = !needCopyBack;
		
	    }

	//copy result into initArr, as needed
	if (needCopyBack)
	    {
		for (int i = 0; i < initArr.length; i++)
		    initArr [i] = a0 [i];
	    }
    }
  1. Implement the merge method, as described.

  2. Annotate the code for the mergeSort method, explaining in some detail what each line (or small collection of lines) accomplishes.

Analysis and Timing

The insertion sort, quicksort, refined quicksort, and merge sort each yield their own analyses of efficiency.

  1. For each algorithm, identify circumstances under which the best case and the worst case analyses could occur. What is the order of each sorting algorithm in the best case and in the worst case? (While class discussion may help answer this question for the insertion sort, and the discussion provides an answer for the quicksort, you still need to tabulate those answers, and you will need to formulatae the answer for the refined quicksort and merge sort.)

Program SortShell5.java combines the code for the insertion sort, quicksort, refined quicksort, and merge sort into a single testing shell, although the merge sort is incomplete (the merge method is not implemented). Copy this program to your account.

  1. Insert your merge method, so that the program is complete. Review the program to determine how it works, and run it to observe its output.

    1. The program provides timings, but how might we know that the sorting algorithms are functioning properly? Not only should a resulting array be sorted, but it also should contain all of the original array values — just in a [possibly] different order. It seems unlikely that we would be able to check carefully output of an array with 100,000 values, so testing will have to be automated. Describe how one might perform tests to help confirm the output is correct in each case. Then, add these tests to the code. The resulting code might perform a range of checks, and just report if/when errors arise (certainly, the checking should produce rather little output).
    2. For its testing of quicksort, SortShell5.java contains code that may initially seem curious:
           if (size > 30000)
              System.out.print("         ---")
           else {
             /* time the quicksort algorithm */
           }
          

      What happens if this code is replaced by the usual code for testing the algorithm (i.e., the code with the if removed)?

      Why do you think this result might occur?

  2. In reviewing the various timings of algorithms, what algorithm(s) do you think should be used in various contexts (e.g., for ascending data, for random data, and for descending data). Explain your conclusions.


created 24 April 2001
revised 28 April 2006
revised 11-13 March 2012
revised 2-3 November 2018
Valid HTML 4.01! Valid CSS!
For more information, please contact Henry M. Walker at walker@cs.grinnell.edu.
[an error occurred while processing this directive]