| 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.
This laboratory exercises provides some practice with variations of the quicksort algorithm, and continues previous experimentation regarding the efficiency of these sorting 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).
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:
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:
Explain why each loop contains the condition right >= left. Given an example where the code would fail if each right >= left test were omitted.
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.
Write a few sentences explaining how this code works. What does the husk procedure do? Why is the husk needed?
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.
The second implementation of the quicksort is motivated by the following pictorial loop invariant.
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;
Review the code segment and explain why the "then" and "else" clauses of the if statement in the loop maintain the loop invariant.
Explain why the swap at the end of the code swaps a[first] with a[left-1] rather than with a[left].
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.
As noted, program SortQuick3.java provides a shell for testing this alternative quicksort. Run it with several sets of test data.
The third implementation of the quicksort is motivated by the following pictorial loop invariant.
Wrote a quicksort algorithm (following a husk and kernel) approach using this new loop invariant.
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.
The insertion sort, quicksort, refined quicksort, and merge sort each yield their own analyses of efficiency.
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.
Program SortShell.java, which was updated in the Lab on Merge Sort and Algorithmic Timing, provides a framework for testing the quicksort.
Modify this program to include timing for both the quicksort and refined quicksort algorithms
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.
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?
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 |
|
| For more information, please contact Henry M. Walker at walker@cs.grinnell.edu. |