Project 1: QuickSort

The goal of this project is reinforce the work on sorting arrays and lists by implementing the quicksort algorithm.

1. Introduction

Recall the difference between insertion and selection sorts. Although they share the same basic pattern (repeatedly move an element from the unsorted section to the sorted section), we have

Insertion

Selection

Now, consider the basic pattern of merge sort:

More specifically, merge sort splits the array simplistically (it's the "easy" part). The reuniting is complicated. What if there were a sorting algorithm that had the same basic pattern as merge sort, but stood in analogy to merge sort as selection stands towards insertion? In other words, could there be a recursive, divide-and-conquer, sorting algorithm for which the splitting is the hard part, but leaves an easy or trivial reuniting at the end?

To design such an algorithm, let's take another look at what merge sort does. Since the splitting is trivial, the interesting parts are the recursive sorting and the reuniting. The recursive call sorts the subarrays internally, that is, each subarray is turned into a sorted subarray. The reuniting sorts the subarrays with respect to each other, that is, it moves elements between subarrays, so the entire range is sorted.

To rephrase our earlier question, the analogue to mergesort would be an algorithm that sorts the subarrays with respect to each other before sorting each subarray. Then reuniting them is trivial. The algorithm is called "quicksort," and as its name suggests, it is a very good sorting algorithm.

Here's a big picture view of it. Suppose we have an array, as pictured below. We pick one element, x, and delegate it as the "pivot." In many formulations of array-based quicksort, the last element is used as the pivot (though it doesn't need to be that way).

Then we separate the other elements based on whether they are greater than or less than the pivot, placing the pivot in the middle.

In a closer look, while we do this partitioning of the array, we maintain three portions of the array: the (processed) elements that are less than the pivot, the (processed) elements that are greater than the pivot, and the unprocessed elements. (In a way, the pivot itself constitutes a fourth portion.) The indices i and j mark the boundaries between portions: i is the last position of the portion less than the pivot and j is the first position in the unprocessed portion. Initially, the "less than" and "greater than" portions are empty.

During each step in the partitioning, the element at position j is examined. It is either brought into the "greater than" portion (simply by incrementing j) or brought into the "less than" portion (by doing a swap and incrementing both i and j).

(Think about that very carefully; it's the core and most difficult part of this project, at least for the array version.)

At the end, the unprocessed portion is empty.

All that's left is to move the pivot in between the two portions, using a swap. We're then set to do the recursive calls on each portion.

2. Setup

Make a directory for this class (if you haven't already). Make a new directory for this project. Then copy into it SortUtil.java and Node.java from lab 2.

cd 245
mkdir proj1
cd proj1
cp /cslab.all/ubuntu/cs245/proj1/* .

This will give you the following files:

3. Quicksort for arrays

Write a class with a sort() method that uses the quicksort algorithm to sort arrays (and counts comparisons), similar to what we wrote in lab 1. Use the description of the algorithm above as a guide. Test your method to make sure it works using the SortArray class from the labs.

4. Mergesort and quicksort for lists

Now write another class, a list version of quicksort-- a method that takes a list (represented by a head node) and sorts that list using quicksort adapted to linked lists. (In list quicksort, you may want to use the first element, rather than the last, as a pivot this time.) Again, test your method with SortList.

A method sorting a linked list must return both the head of the sorted list and the number of comparisons. To do this, we will encapsulate the two objects with an instance of the IntListPair class. The return type of your sort() method in this class should be IntListPair. The end of the method might look something like

         return new IntListPair(comparisons, sorted);

5. Experiments

As we will be talking about in class while you work on this project, merge sort and quick sort are in different "complexity classes" in terms of the their worst case performance. Merge sort is O(n lg n), whereas quicksort is O(n^2). (O(n lg n) is faster). However, these are worst cases; we might find that experimentally one of the algorithms may behave better on average.

Write a program or programs to automate experiments to compare the running time and number of comparisons for quicksort and mergesort, both for arrays and lists. Use System.currentTimeMillis(). On several different arrays of several sizes, compare the running time of these two algorithms.

To reduce noise, make sure you are the only person using your workstation, and close all other programs. You can find out who else, if anyone, is remotely logged into your workstation by typing in the command finger into the terminal.

6. To turn in

Write a paragraph or two explaining how you did your experiment and what your findings are. You may want to include a table or graph.

Print out the files you wrote. The command to print files neatly, two to a page, is a2ps. The flag --file-align=fill tells it not to start each file on a new page, so less paper is wasted. For example, to print the files QuickA.java and QuickL.java, use

a2ps --file-align=fill QuickA.java QuickL.java

DUE: Wednesday, Jan 28, 5:00 pm.


Thomas VanDrunen
Last modified: Fri Jan 16 11:22:37 CST 2009