Please make sure you adhere to the policies on academic honesty.
Please see the general programming webpage for details about the programming environment for this course, and specifically for directions in how to submit your programming assignment electronically.
The files you need for this assignment can be downloaded here.
Our goal in this assignment is to be able to compare the relative efficiency of radix-sort versus quick-sort on a variety of input scenarios involving (long) integer keys in a specified range. We will ask you to run experiments measuring the system time used for these methods. Note that the experimental phase will take significant time, so please plan on developing your code well ahead of the submission deadline.
For quick-sort, we will use an implementation provided by our text. For radix-sort, we will ask you to write the implementation based on the method outlined in Chapter 10.5 of the text, using an underlying stable bucket-sort.
Radix-sort is described as a lexicographical sort of keys which are d-tuples, such as (k1, k2, k3, ..., kd). In class, we discussed how such a sort can be used on integral keys by breaking an original integer into digits. An example involved using digits base 10, where we considered separately the ones digit, the tens digits, the hundreds digits, the thousands digits, etc. Though humans are trained very well to work with decimal notation, this technique can be used with bases other than 10. In particular, computers are much more efficient in dealing with numbers represented using powers of two.
For this assignment, you will write a procedure which implements radix sort by representing numbers using base 2b for some parameter b. That is, each digit is comprised of b bits and thus has value between [0,2b-1]. As an example, the decimal number 2471 would be stored in binary as 100110100111, because
Alternatively, we can view the binary number considering four bits at a time as a digit, in which case we get a hexadecimal (base 16) representation,
The key parameters for the method are the number of bits to consider per digit, the overall number of bits which make up the full key, and the number of keys which are to be sorted.
The driver for this program will take all of its input from command line arguments. In particular, you will need to run the program with three command line arguments, specified in this order:
The driver will prepare an experiment by storing n randomly chosen
long's from the range in an array. Then it will
call the routine
Radix.sort(long A[ ], int n, int m, int b)
which you are to write in the file Radix.java.
Please note that this routine has no return value. Instead, the
driver maintains its own reference to array A, and it expects that
your routine places the sorted results into this very same array. If
you were to place results in a newly constructed array, the driver
would not be able to see those results.
You will want to imagine breaking each key into digits, where each digit is comprised of b bits and thus has value between . When comparing to the book's notation, we have that for each bucket sort, and is the number of digits used in representing each key and thus the number of passes of bucket sort.
As we have done in the past, there is an optional fourth argument, which is the seed for the random number generator, allowing you to repeat experiments while debugging.
Your task is to understand and correctly implement the radix-sort method. Getting a correct implementation is indeed necessary. However there is more to this assignment. We ask that you perform experiments comparing the running time of your method versus quick-sort on a variety of input settings. As the name ``quick-sort'' suggests, that method is a reasonably quick sorting method. You might have to work very hard to get your sorting method to be competitive with quick-sort. You might also work very hard and still end up losing to quick-sort on many settings. Do the best that you can.
Consider whether there are simple ways to clean-up your code. Consider whether there are major design decisions which effect the overall efficiency. You will want to concern yourself not only with the number of instructions you need, but also with the amount of memory which your program uses. The reason for this is three-fold. Eventually, you may run out of memory on large enough inputs. Even if you do not run out, as you use more and more memory this taxes Java's memory management system and the overall computer system, resulting in an overall slower running time. Finally, if you choose to use data structures which require more memory, this generally means you also will have to execute more operations to initialize and maintain the information stored in that memory. So often reducing the memory to the bare minimum structures helps you reduce unnecessary time spent maintaining the structures.
Your readme file should contain the results of the following experiments, reporting the running times for all settings. Additionally, your readme file should include a paragraph or two in which you interpret the significance of your experimental results.
For the experiments, there are three parameters which we want you to vary: n, m, b. Please structure your experiments as follows.
Set m=8 and test your program as follows:
If you have trouble with time or memory for the largest values of n, so be it; report as many results as possible. Now create a new version of the above table including a column b=16, and report experiment results for values m=16, m=32, m=48.
As you might notice there are a total of almost 140 different running times you are asked to report. Collecting such data will undoubtedly take a significant amount of your time even after your program is implemented. You should try, as best possible, to run all of the experiments on the same machine, under the same conditions.
There are two options for gathering this data. One is to sit in front
of the computer for an hour or two, running the driver over-and-over
by hand with different parameters. The driver relies on a routine
Experiment.doExperiment(long A[ ], int n, int m, int b, long seed)
which prepares the array of random values, starts a timer, and
then calls your sort.
The other option is to write yourself your own driver which has loops
to vary the parameters in calling
Experiment.doExperiment. Please note that the array sent to this
routine must be pre-constructed to have length as long as the maximum
value of n you will be using in your tests.
(if you instead kept allocating memory for a new array for each
such experiment, your own driver will start taxing the memory system
and this in turn might adversely effect the timings of the latter experiments).
Armed with such a driver, you can start it running to gather data and then go do something else. If you do create such a driver, you do not need to submit it - it is purely for your own benefit in gathering results.
The files you need for this assignment can be downloaded here.
We will provide you with the file Experiment.java which contains the driver. We will also provide you with a simple template file Radix.java which is the file you must modify and resubmit.
You should submit the files: readme and Radix.java. If you choose to define any additional classes, please submit those files too.
One technical key in the implementation will be your ability to pull out a particular ``digit'' from a key when working base . We will make it easy on you and tell you an efficient way to do this in Java. To be specific, let us assume that you have a long ``value'', and that you are interested in finding the least-significant digit of that value, where each digit consists of b bits. That is, j=0 will refer to the rightmost digit, j=1 to the second from the right, and so on. This calculation can be accomplished as follows in Java,
int digit = (int) ((value>>(b*j)) & ((1L<<b) - 1));
For those interested in understanding why this line does what it does, we will explain below. For those who are not interested, there is no need to read any further. You might first think about how to accomplish this in decimal notation. If you have a value ``x'' and want to know the seventh digit from the right (i.e. the millions digit), how can you calculate this? One way is by using the proper combination of division and modulus operations. You can take the original value x, divide it by 1000000, throwing away the remainder. At this point, the digit you want is the least significant digit of that result. You can calculate that by taking mod 10. We will use the same idea when working in base . You can get the least-significant digit of a value by calculating the expression:
However, we can take great advantage of the fact that computers offer efficient ways of dealing with numbers represented using powers of two. Although this may be new to many of you, Java offers several operators which allow you to view and modify a number by means of its binary representation. A list of these operators is given on page 21 of Chapter 1.3 of the text, and other Java reference books may contain more information. We will not repeat the information which is there; instead we will give a few of our favorite examples and tips.
We can use this mask by taking the expression of interest and doing a bit-by-bit ``and'' with the mask value. The effect of this bit-by-bit and will be that the final result will have 0's in all places where the mask had 0's; in all places where the mask had 1's, the final result will have the same bits which were in the original expression. Therefore, the java statement: (expr & (1L<<b)) calculates the equivalent of .
int digit = (int) ((value>>(b*j)) & ((1L<<b) - 1));
The reason we cast it to an int rather than leave it as a long is that we wish to use it as an array index for bucket sort, and such an index must be an int.
Hint: Most important will be the time and space efficiency of your choice of the bucket implementation. Think back to Programming Assignment for some ideas.