CS 261 University of Puget Sound Spring, 2020
 
Computer Science II
Abstract Data Types and their Implementations, Some Basic Algorithms,
Object-oriented Problem Solving, and Efficiency
 

Warning: This course is under development. Although the basic structure of this course is largely established, nothing on this Web site should be considered official or even possibly correct.
DO NOT MAKE PLANS BASED ON THE CONTENTS OF THIS SITE UNTIL JANUARY, 2020.

Quicksort and More Timing of Algorithms

Goals

This laboratory exercises provides some practice with variations of the quicksort algorithm, and continues previous experimentation regarding the efficiency of these sorting algorithms.

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 a husk-and-kernel programming style which arises frequently in many types of algorithms. The husk method provides a simple interface for a user, while the kernel includes additional parameters that may be helpful for a programmer.

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

  2. Program SortQuick1.java provides a shell for testing the quicksort. Program SortQuick2.java is the same as SortQuick1.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 SortQuick3.java. Explain why the recursive calls involve left-2 and left.

  4. As noted, program SortQuick3.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 SortQuick1.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.

Analysis and Timing

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

  1. For the quicksort and refined quicksort algorithms, identify circumstances under which the best case and the worst case analyses could occur. What is the order of these sorting algorithms in the best case and in the worst case? Explain.

  2. Program SortShell.java, which was updated in the Lab on Merge Sort and Algorithmic Timing, provides a framework for testing the quicksort.

    1. Modify this program to include timing for both the quicksort and refined quicksort algorithms

    2. When testing or running this code, issues can arise with a regular quicksort when the array size becomes large. For example, what happens if the array size is larger than 30000 or 80000 or 150000? If you encounter some troubles, identify what happens and consider why they might arise.

    3. If troubles are encountered with a regular quicksort for large array sizes, the timing program might be modified for the quicksort by inserting something like the following:

           if (size > 30000)
              System.out.print("         ---")
           else {
             /* time the quicksort algorithm */
           }
           

      Why might such a refinement be needed for the regular quicksort, but not for any of the other sorting algorithms?

  3. 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
revised and reorganized 23 January 2020
Valid HTML 4.01! Valid CSS!
For more information, please contact Henry M. Walker at walker@cs.grinnell.edu.