Project 1: List::Sort()

MergeSort for Linked Lists

Note: This assignment is used to assess the required outcomes for the course, as outlined in the course syllabus. These outcomes are:

  1. analyze the computational complexity of algorithms used in the solution of a programming problem
  2. evaluate the performance trade-offs of alternative data structures and algorithms

These will be assessed using the following rubric:

  I E H  
Key:
  I = ineffective
  E = effective
  H = highly effective
Performance Analysis
  Runtime Analysis - - -
  Runspace Analysis - - -
Tradeoff Analysis
  Comparison Sorts - - -
  Numerical Sorts - - -

In order to earn a course grade of C- or better, the assessment must result in Effective or Highly Effective for each outcome.

Educational Objectives: After completing this assignment, the student should be able to accomplish the following:

Part 1: MergeSort Implementations

Operational Objectives: Implement List::Sort() using the MergeSort algorithm as an in-place, stable, O(n log n) sort.

Deliverables: Files

list_merge_sort.cpp     # contains implementations of Sort() and Sort(P&)
list_merge_sort_spy.cpp # instrumented version of list_merge_sort.cpp

Note these are intended to be slave files for LIB/list.h.

Procedural Requirements

  1. Develop and fully test the sort algorithms conforming to the requirements and specifications below.

  2. Turn in the deliverables using the script proj1submit.sh

    Warning: Submit scripts do not work on the program and linprog servers. Use shell.cs.fsu.edu to submit projects. If you do not receive the second confirmation with the contents of your project, there has been a malfunction.

Code Requirements and Specifications

  1. The official development, testing, and assessment environment is gnu g++ on the linprog machines. Code should compile without error or warning, when all warning flags are set.

  2. Develop and test an implementation of MergeSort for both overloads of the Sort method of fsu::List:

    template < typename T >
    List<T>::Sort();           // uses default order for type T
    
    template < typename T >
    template < class P >
    List<T>::Sort(const P& p); // uses p = predicate order for type T
    

    Be sure the implementations are in-place, stable, and have O(n log n) runtime. Place these implementations in the file list_merge_sort.cpp.

  3. Copy list_merge_sort.cpp to list_sort.cpp. Note that list_sort.cpp is a slave file, with master LIB/tcpp/list.h. It is recommended to change permissions on list_sort.cpp to read only after copying, to avoid inadvertantly editing the wrong file. Test thoroughly for correct functionality and failure bugs using the supplied test harness flist.cpp. Note that the sorts provided in LIB/tcpp/list_sort.cpp implement the InsertionSort algorithm. However when you have list_sort.cpp in your project directory, it will be used as long as the include path specs have the current directory ahead of the LIB directories:

    g++ -Wall -Wextra -I. -I/home/courses/cop4531p/fall12/cpp -I/home/courses/cop4531p/fall12/tcpp -oflist.x flist.cpp
    

    Copying, permission changes, and compiling are conveniently handled in the supplied makefile.

  4. Test thoroughly again for correct functionality on large data sets using the supplied test harness listsort.cpp. This test is not suitable for general compatibility, nor does it test the predicate version Sort(P&), but it does provide simple access to large-scale testing and evaluation. Note also that large test files can be created using the supplied ranuint.cpp file generator.

  5. Copy list_merge_sort.cpp to list_merge_sort_spy.cpp, and add instrumentation to count the number of order comparisons and the number of pointer advances. Calls to these versions of Sort result in output of the instrument data. (The normal versions are, of course, silent.) The differences between the two versions should be the spyware and reporting only:
    diff list_merge_sort.cpp list_merge_sort_spy.cpp should look something like this:

    16d15
    < 
    21a21,23
    >   size_t compares = 0;
    >   size_t ptrsteps = 0;
    > 
    59a62
    >         ++ptrsteps; // counts pointer advancement
    82a86
    >           ++compares; // counts true case
    88a93
    >           ++compares; // counts false case
    123a129,139
    > 
    >   size_t size = Size();
    >   size_t logsize = (size_t)log2(size);
    >   std::cout << "\n ****************************\n"
    >             << " ** Chris Lacher\n"
    >             << " ** List::Sort(P) as merge_sort \n"
    >             << " ** n        = " << size << '\n'
    >             << " ** n log n  = " << size * logsize << '\n'
    >             << " ** ptrsteps = " << ptrsteps << '\n'
    >             << " ** compares = " << compares << '\n'
    >             << " ****************************\n\n";
    128a145,146
    >   size_t compares = 0;
    >   size_t ptrsteps = 0;
    166a185
    >         ++ptrsteps; // counts pointer advancement
    189a209
    >           ++compares; // counts true case
    195a216
    >           ++compares; // counts false case
    230a252,262
    > 
    >   size_t size = Size();
    >   size_t logsize = (size_t)log2(size);
    >   std::cout << "\n ****************************\n"
    >             << " ** Chris Lacher\n"
    >             << " ** List::Sort() as merge_sort \n"
    >             << " ** n        = " << size << '\n'
    >             << " ** n log n  = " << size * logsize << '\n'
    >             << " ** ptrsteps = " << ptrsteps << '\n'
    >             << " ** compares = " << compares << '\n'
    >             << " ****************************\n\n";
    

    (with your name instead of "Chris Lacher"). To test with the instrumented version, copy list_merge_sort_spy.cpp to the slave file list_sort.cpp and re-compile. (Again, conveniently handled in the supplied makefile.) The spy data is interesting on its own. It will be used also in Part 2 below.

Hints

Part 2: Analysis of List::Sort()

Deliverables: One file

assign4.pdf

containing your analysis of List::Sort as answers to the first three questions and an analysis of efficiency of hash tables using HashTable::Analysis as answer to the fourth question.

Background: Curve Fitting

By a scalability curve for an algorithm implementation we shall mean an equation whose form is determined by known asymptotic properties of the algorithm and whose coefficients are determined by a least squares fit to actual timing data for the algorithm as a function of input size. For example, the merge sort algorithm, implemented as a generic algorithm named g_merge_sort(), is known to have asymptotic runtime Θ(n log n), and we will use the following for its form:

R = A + B n log n

where R is the predicted run time on input of size n. To obtain the concrete scalability curve, we need to obtain actual timing data for the sort and use that data to find optimal values for the coefficients A and B. Note this curve will depend on the implementation all the way from source code to hardware, so it is important to keep the compiler and testing platform the same in order to compare efficiencies of different sorts using their concrete scalability curves.

The method for finding the coefficients A and B is the method of least squares. Assume that we have sample runtime data as follows:

Input size:  n1n2...nk
Measured runtime:  t1t2...tk

and the scalability form is given by

f(n) = A + B g(n)

Define the total square error of the approximation to be the sum of squares of errors at each data point:

E = Σ [ti  -  f(ni)]2

where the sum is taken from i = 1 to i = k, k being the number of data points. The key observation in the method of least squares is that total square error E is minimized when the gadient of E is zero, that is, where all three partial derivatives DAE and DBE are zero. Calculating these partial derivatives gives:

DXE   =   2 Σ [ti  -  f(ni)] DXf
   =   2 Σ [ti  -  (A  +  B g(ni))] DXf

(where X is A or B). This gives the partial derivatives of E in terms of those of f, which may be calculated to be:

DAf = 1
DBf = g(n)

(because n and g(n) are constant with respect to A and B.) Substituting these into the previous formula and setting the results equal to zero yields the following equations:

A Σ 1   +   B Σ g(ni)    =    Σ ti
A Σ g(ni)   +   B Σ (g(ni))2    =    Σ ti g(ni)

Rearranging and using Σ 1 = k yields:

k A   +   g(ni)] B    =    Σ ti
g(ni)] A   +   [Σ (g(ni))2] B    =    Σ ti g(ni)

These are two linear equations in the unknowns A and B. With even a small amount of luck, they have a unique solution, and thus optimal values of A and B are determined. (Here is a link to a more detailed derivation in the quadratic case.)

Note that all of the coefficients in these equations may be calculated from the original data table and knowledge of the function g(n), in a spreadsheet or in a simple stand-alone program. The solution to the system of equations itself is probably easiest to find by hand by row-reducing the 2x3 matrix of coefficients to upper triangular form and then back-substitution.

Procedural Requirements

  1. Answer questions 1-4 in Assignment 4.