Homework 4: Recursion - the Good, the Bad, and the Ugly

Educational Objectives: After completing this assignment the student should have the following knowledge, ability, and skills:

Operational Objectives: Modify three distributed programs by supplying recursive and dynamic functions as specified. Write a report answering questions and reporting findings.

Deliverables: Four files fibo.cpp, loop.cpp, sort.cpp, and report.txt.

Recursion

Recursion is both a mathematical and a computational concept. I picked up the nearest "Discrete Mathematics for Computer Scientists" text and found the word "recursion" and its derivates indexed on pages 1, 2, 27, 33, 39, 40, 43-46, 50, 92, 112, 204, 263, 264, 281, 282, 313, 316, 402, 404, 405, and 435, all in a text of only 515 pages. (This is a "lighter-weight" text than the one FSU uses for its Discrete Mathematics series.) Suffice to say: You will study recursion in discrete math.

Recursion is an important concept and tool in computer science as well, and you will encounter it in many significant ways and places, including:

Note that at this point we have not said exactly what recursion is, but rather have pointed out that it is many things and that you will likely not have a complete understanding of recursion until later in your career.

For now, we will define recursion in C++: a function is said to be recursive if it calls itself in its implementation. Here is an example:

float Mystery (float * array, size_t arraySize)
{
  if (arraySize == 0)                                          // base case
    return 0;
  return array[arraySize - 1] + Mystery(array, arraySize - 1); // recursive call
}

Some important things to observe about this function are:

  1. There is no loop structure in the body
  2. There is a call to the function in its own body (the "recursive call")
  3. There is a conditional branch in the body one leg of which does not have a recursive call (the "base case")
  4. The code is layed out almost like a proof by "mathematical induction" - the question is, proof of what?
  5. What does the function Mystery calculate?

We can begin to answer the question by tracing the call Mystery (a,3) for the array a = [4,5,6]:

Mystery(a,3)
  return a[2] + Mystery(a,2) // apply recursive call
         = 6 + Mystery(a,2)  // substitute 6 = a[2]
         = 6 + (a[1] + Mystery(a,1)) // apply recursive call
         = 6 + (5 + Mystery(a,1))    // substitute 5 = a[1]
         = 6 + (5 + (a[0] + Mystery(a,0)))  // apply resursive call
         = 6 + (5 + (4 + Mystery(a,0)))     // substiture 4 = a[0]
         = 6 + (5 + (4 + 0)) // apply base case
         = 6 + (5 + 4)  // return
         = 6 + 9   // return
         = 15   // value to return

If you follow this trace, you can begin to see that the function Mystery returns the sum of all the elements of the array. In fact, the function body can serve as a model of a proof (using the Principle of Mathematical Induction) that Mystery(a,n) returns the sum of the first n elements of a.

Recursion is not always good. There are recursive solutions to some problems that are rather ugly - perhaps the example above is one of these. Why would we want a recursive calculation of a sum when a simple loop is more transparent and easier to implement? And recursive solutions can be genuinely bad, in the sense that they are extremely inefficient, requiring far more computational time than other equivalent methods.

See various portions of the course textbook for more about recursion, Fibonacci, and Mergesort.

Procedural Requirements

  1. Copy all of the files in ~cop3330p/fall08/hw4/ into your hw4 directory. You should now have these files:

    data1
    data2
    data3
    fibo.distribute
    loop.distribute
    sort.distribute
    makefile
    

    Note that you have three data files and a makefile. The three files suffixed ".distribute" are code files.

  2. Copy the three distribute files onto files suffixed ".cpp":

    cp fibo.distribute fibo.cpp
    cp loop.distribute loop.cpp
    cp sort.distribute sort.cpp
    

    These are three code files that form the point of beginning for your assignment. These are correct code and should compile to executables using the supplied makefile.

  3. Enter the command "make" and make sure you get three executables fibo.x, loop.x, and sort.x. Run each of these programs. Look at the source code for each of these programs. Spend some time understanding what each of these programs does (and what it does not do...).

  4. Modify fibo.cpp, loop.cpp, and sort.cpp according to the code requirements and specifications below. Make sure that your three programs are well written and perform as required, including "boundary" cases.

  5. Write a brief report answering questions (given below) about these programs and your experience creating them.

  6. Turn in four files fibo.cpp, loop.cpp, sort.cpp, and report.txt using the hw4submit.sh submit script.

    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 two confirmations, the second with the contents of your project, there has been a malfunction.

Code Requirements and Specifications: fibo.cpp

  1. Redefine the body of the function RFib [for "Recursive Fibonacci"] so that it implements the classic recursive definition of Fibonacci numbers, to wit:

    The Fibonacci numbers are the elements of the sequence f0, f1, f2, f3 ... of non-negative integers such that f0 = 0, f1 = 1, and for each n > 1, fn = fn-1 + fn-2.

    Be sure that you have a base case, a recursive call, and no loop structure. (Note: You may have more than one base case and/or more than one recursive call. However, at least one of each is required for all recursive functions.) Be sure that you have covered the trivial boundary cases. You can compile with make and test your code. Be sure it is getting correct values for these cases:

    RFib(0)  = 0
    RFib(1)  = 1
    RFib(2)  = 1
    RFib(3)  = 2
    RFib(4)  = 3
    RFib(5)  = 5
    RFib(6)  = 8
    RFib(7)  = 13
    RFib(8)  = 21
    RFib(9)  = 34
    RFib(10) = 55 
    RFib(20) = 6765
    RFib(30) = 832040
    RFib(40) = 102334155
    

    Of course you can test other input as well. It would be most unlikely for an incorrect program to generate the results above, however. NOTE: that the results returned by DFib will not be correct at this point.

  2. Now supply a new body for the function DFib [for "Dynamic Fibonacci"] that is a so-called dynamic programming calculation. Dynamic programming in this case takes advantage of the recursive equation defining the Fibonacci numbers but also uses a loop and assignment statements to have the last three numbers in memory during each iteration of the loop. For example, we could have three variables, one for the current number, a second for the previous number, and a third for the number two places back:

    size_t f,   // current fib number
           fp,  // previous fib number (1 back)
           fpp; // previous previous fib number (2 back)
    

    Then after appropriate initialization the loop body would redefine each of these by updating one at a time:

    fpp = fp;        // new previous previous becomes old previous
    fp  = f;         // new previous becomes old current
    f   = fp + fpp;  // new current becomes new previous plus new previous previous
    

    Be sure that DFib uses a for loop and also that it takes care of the base cases outside the loop. Also make sure that DFib is not recursive. Test DFib on all the input you used to test RFib. Now these should be producing identical results.

    The key observation that makes the dynamic programming approach work is that you don't need to calculate or remember the entire sequence just to get the next one, you only need the last two in the sequence to get the next. The three variables inside the dynamic programming loop are often called a "ladder" that supports the calculation.

Code Requirements and Specifications: loop.cpp

  1. Supply a new body for the function RLoop in this program. RLoop(n) should duplicate the results already correctly produced by ILoop(n), namely output n dots. RLoop should be a recursive refactorization of ILoop. (To refactor code is to rewrite the code in a manner that does not change what the code accomplishes - do the same thing but in a different way.)

  2. RLoop should have a base case, a recursive call, and no loop structure.

  3. RLoop should have identical behavior as ILoop. Note that you should be able to compile with make and test to see identical output for ILoop and RLoop.

Code Requirements and Specifications: sort.cpp

  1. Supply a new body for the function MergeSort(v, p, q) that implements recursive merge sort on an object of type std::vector. Note that the function header and three parameters are as follows:

    MergeSort
      (
        std::vector<int> & v, // a std::vector object whose elements have type int
        size_t beg,           // the beginning index of the range to be sorted
        size_t end            // the end index of the range to be sorted
      )
    

    which is designed to facilitate a recursive implementation - the recursive calls using different specifications of range. If a client wants to sort an entire vector, the call would be

    MergeSort (v, 0, v.size()); // sorts entire vector v
    

    Note also that the vector is passed by reference, so that anything the call to MergeSort does to the vector makes changes in the actual vector that is owned and passed by the calling process.

  2. Your new body for MergeSort should follow and implement this algorithm:

    1. Let mid be the middle index in the range [beg,end)
    2. Sort each of the two ranges [beg,mid) and [mid,end)
    3. Merge the two ranges back to [beg,end)

  3. The recursive MergeSort body should have at least one base case (perhaps 2) and two recursive calls. In addition there should be a call to the function fsu::Merge that is supplied in the file sort.cpp. Do not change any code anywhere except inside the body of MergeSort.

  4. The function fsu::Merge is set up to work with MergeSort - merge two sorted ranges into one (larger) sorted range. This function is correct and debugged - don't change it.

  5. Test your sort by invoking make to build executables. There are two data files that you can use to test with, and you should make up some of your own. Also be sure to test boundary cases ... the program should handle such things as empty files and bad file reads with aplomb.

Report

The report is a plain text file. Do not submit any file with special formatting in it, such as Word or rtf or pdf or html. The assessment process will be able to read text files only.

The report file must be named "report.txt" for the submit script.

Begin your report before you even start coding, because some of the questions pertain to the files as distributed and before modification.

Start your report with file header info as follows:

COP 3330 Homework 4: Recursion - The Good, the Bad, and the Ugly
<your name>
<your CS username>
<your FSU username>

Answer each of the following questions and/or supply evidence that you have performed the required tasks. Be sure to number the questions and repeat the question in the report prior to answering.

  1. After copying the three .distribute files onto their respective .cpp files, build by issuing the make command. What executables are created?

  2. Run each executable and describe its behavior.

  3. Open the source code files and try to understand the code. Take special note of the use of command line arguments in function main.

    What is "argc"?
    What is "argv[]"?
    What does "atoi" accomplish?
    What function calls are made by main in fibo?
    What function calls are made by main in loop?
    What function calls are made by main in sort?

    Answers provided by copy/paste screen shots are fine.

  4. The function fsu::Merge is set up specifically for the MergeSort application in sort.cpp. Note that fsu::Merge makes two calls to algorithms in the standard library. What are these two algorithms? What do they accomplish?

The remaining questions should be answered after completing the coding part of the assignment.

  1. You have created three different recursive functions.

    Which of these do you consider to be most elegant?
    Which of these do you consider to be most efficient?
    Are any of these a total waste of (human) time without any redeeming features?

    (Give brief justifications for each answer.)

  2. Compare RFib and DFib.

    Which of these is more efficient?
    Which of these is more elegant?

  3. Rate these as "good", "bad", and "ugly":

    RFib
    RLoop
    MergeSort

    Use each rating at least once.

Hints

Distributed Code

Below are copies of the code files distributed for this assignment. The portions highlighted in red are the only places where code needs to be modified.

File: fibo.cpp

/*
     fibo.cpp

     (put your individualized file header documentation here)

     The Good, The Bad, and The Ugly
     Part 1: Fibonacci: The XXXX 
                            ^^^^
			    (put correct choice here among {good, bad, ugly}

*/

#include <iostream>
#include <cstdlib>

unsigned long RFib (size_t n)
{
  return n;
}

unsigned long DFib (size_t n)
{
  return n;
}

int main( int argc, char* argv [] )
{
  if (argc != 2)
    {
      std::cout << " ** Error: please enter one non-negative integer argument\n";
      exit (EXIT_FAILURE);
    }
  size_t n = atoi(argv[1]);
 
  if (n > 46)
    {
      std::cout << " ** number too large - try again later\n";
      exit (EXIT_SUCCESS);
    }

  std::cout << " DFib(" << n << ") = " << DFib(n) << '\n'; 
  std::cout << " RFib(" << n << ") = " << RFib(n) << '\n'; 
}

File: loop.cpp

/*
     loop.cpp

     (put your individualized file header documentation here)

     The Good, The Bad, and The Ugly
     Part 2: Repitition: The XXXX 
                            ^^^^
			    (put correct choice here among {good, bad, ugly}

*/

#include <iostream>
#include <cstdlib>

void ILoop (size_t n)
{
  for (size_t i = 0; i < n; ++i)
    std:: cout << '.';
}

void RLoop (size_t n)
{
  // this code is bogus - it needs to be completely replaced
  // RLoop(n) should be a recursive function that accomplishes
  // exactly the same task as ILoop(n)
  if (n)
    std::cout << "duh";
}

int main( int argc, char* argv [] )
{
  if (argc != 2)
    {
      std::cout << " ** Error: please enter one non-negative integer argument\n";
      exit (EXIT_FAILURE);
    }
  size_t n = atoi(argv[1]);
 
  if (n > 500)
    {
      std::cout << " ** number too large - try again later\n";
      exit (EXIT_SUCCESS);
    }

  std::cout << "dots provided by ILoop: ";
  ILoop(n);
  std::cout << '\n';

  std::cout << "dots provided by RLoop: ";
  RLoop(n);
  std::cout << '\n';
}

File: sort.cpp

/*
     sort.cpp

     (put your individualized file header documentation here)

     The Good, The Bad, and The Ugly
     Part 3: MergeSort: The XXXX 
                            ^^^^
			    (put correct choice here among {good, bad, ugly}

*/

#include <iostream>
#include <fstream>
#include <vector>
#include <algorithm>

namespace fsu
{
  template < typename T >
  void Merge(std::vector<T>& v, size_t p, size_t q, size_t r)
  {
    T temp [r-p];                                       // temp space for merged copy of v
    typename std::vector<T>::iterator orig = v.begin(); // iterator to original vector
    std::merge(orig+p, orig+q, orig+q, orig+r, temp);   // merge the two parts of v to temp
    std::copy(temp, temp+(r-p), orig+p);                // copy temp back to v[p,r)
  }
} // namespace

void MergeSort(std::vector<int> & v, size_t beg, size_t end)
// sorts v in the range [beg,end)
{
  // this should be recursive merge sort
  // NOTE: the function Merge() above is complete and ready to be called here
  v[beg];
  v[end - 1];
}

int main( int argc, char* argv [] )
{
  if (argc != 3)
    {
      std::cout << " ** Error: please enter two file names - input and output\n";
      exit (EXIT_FAILURE);
    }
  std::ifstream ifs;
  ifs.open(argv[1]);
  if (ifs.fail())
    {
      std::cout << " ** Unable to open file " << argv[1] << '\n'
		<< "    Please check file name and try again\n";
      exit ( EXIT_FAILURE);
    }
  std::vector<int> v(0);
  int n;
  while (ifs >> n)
    v.push_back(n);
  ifs.close();
  std::cout << " data as entered:";
  for (size_t i = 0; i < v.size(); ++i)
    std::cout << ' ' << v[i];
  std::cout << '\n';
  MergeSort(v,0,v.size());
  std::cout << " data after sort:";
  for (size_t i = 0; i < v.size(); ++i)
    std::cout << ' ' << v[i];
  std::cout << '\n';
  std::ofstream ofs;
  ofs.open(argv[2]);
  if (ofs.fail())
    {
      std::cout << " ** Unable to open file " << argv[2] << '\n'
		<< "    Please check file name and try again\n";
      exit ( EXIT_FAILURE);
    }
  for (size_t i = 0; i < v.size(); ++i)
    ofs << ' ' << v[i];
  ofs.close();
}

File: makefile

#
# makefile for COP 3330 Homework 4:
#
# The Good, The Bad, and The Ugly
#

all: fibo.x loop.x sort.x

fibo.x: fibo.o
	g++ -ofibo.x fibo.o

loop.x: loop.o
	g++ -oloop.x loop.o

sort.x: sort.o
	g++ -osort.x sort.o

fibo.o: fibo.cpp
	g++ -I. -Wall -Wextra -c fibo.cpp

loop.o: loop.cpp
	g++ -I. -Wall -Wextra -c loop.cpp

sort.o: sort.cpp
	g++ -I. -Wall -Wextra -c sort.cpp