Homework 1: The Good, Bad, and Ugly

Explorations of recursive algorithms

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

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, find.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.

Procedural Requirements

  1. Copy all of the files in [LIB]/hw1/ into your hw1 directory. You should now have these files:

    fibo.distribute
    loop.distribute
    find.distribute
    makefile
    

    The three files suffixed ".distribute" are code files. The makefile will build the three executables fibo.x, loop.x, and find.x.

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

    cp fibo.distribute fibo.cpp
    cp loop.distribute loop.cpp
    cp find.distribute find.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 find.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 find.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, find.cpp, and report.txt using the hw1submit.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. When you wrote DFib, you refactored RFib.)

  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: find.cpp

  1. Supply a new body for the template functions LowerBound and UpperBound that recursively calculate the lower bound and upper bound of a search value in an array. The input is a range determined by two pointers and a search value. The output is a pointer. Here is the header for LowerBound:

    template < typename T >
    T* LowerBound (T* low, T* hih, T val)
    // pre:    low and hih point into an array of T
    //         low + n = hih for some n >= 0
    //         low[0] ... low[n-1] are in non-decreasing order
    // post:   no state is changed
    // return: ptr = lower bound location of val in range; that is:
    //         ptr = low + i, where low[i-1] < val <= low[i]; or
    //         ptr = hih if no such i exists
    {
      // recursive body goes here
    }
    

    Note that this is a template function parametrized by the type of the elements in the search space. The return value should be a pointer satisfying the conditions in the header documentation. A typical call would be

    loc = LowerBound (a, a+size, val);
    if (loc == a + size) // not found
    else ...             // loc points to first occurence of val in a
    

    Note that (loc - a) (pointer arithmetic) is the array index of the location, if that is needed.

  2. The recursive LowerBound body should have at least one base case, at least one recursive call, and NO LOOP. You can get ideas for this from the lecture notes on iterative algorithm lower_bound, which should produce identical results.

  3. Similar requirements apply to UpperBound.

  4. Test your by invoking make to build executables. The type used in the test is char which makes it easy to visually check the correctness of the algorithm implementations. Be sure to test boundary cases ... the program should handle such things as empty search ranges 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 4530 Homework 1: 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 find?

    Answers provided by copy/paste screen shots are fine.

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

  1. You have created four 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
    LowerBound
    UpperBound

    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: find.cpp

/*
     find.cpp

     (put your individualized file header documentation here)

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

*/

#include <iostream>
#include <fstream>
#include <tvector.h>
#include <genalg.h>
#include <gheap.h>

template < typename T >
T* LowerBound (T* low, T* hih, T val)
// pre:    low and hih point into an array of T
//         low + n = hih for some n >= 0
//         low[0] ... low[n-1] are in non-decreasing order
// post:   no state is changed
// return: ptr = lower bound location of val in range; that is:
//         ptr = low + i, where low[i-1] < val <= low[i]; or
//         ptr = hih if no such i exists
{
  // This code is bogus - it needs to be completely replaced.
  // LowerBound should be a recursive function that accomplishes
  // exactly the same task as lower_bound in the lecture notes.

  if (val <= *low) return low;
  return hih;
}

template <typename T>
T* UpperBound (T* low, T* hih, T val)
// pre:    low and hih point into an array of T
//         low + n = hih for some n >= 0
//         low[0] ... low[n-1] are in non-decreasing order
// post:   no state is changed
// return: ptr = upper bound location of val in range, that is:
//         ptr =  low + i, where low[i-1] <= val < low[i]; or
//         ptr = hih if no such i exists
{
  // This code is bogus - it needs to be completely replaced.
  // UpperBound should be a recursive function that accomplishes
  // exactly the same task as lower_bound in the lecture notes.

  if (val < *low) return low;
  return hih;
}

int main()
{
  typedef char ElementType;
  bool BATCH = 0;
  ElementType * A;  // an array - this will be the search space

  ElementType e, s; // entry, sentinel
  fsu::TVector<ElementType> v; // input storage

  std::cout << "Begin test of binary search on arrays of char.\n"
            << "  You will enter characters for storage into a vector v. These will be\n"
            << "  copied to an array A and sorted. Then the program enters an interactive phase\n"
            << "  where you will be prompted to enter search values. The sentinel value\n"
            << "  will be used to terminate your original input and to end the program.\n\n"
            << "Enter sentinel: ";
  std::cin >> s;
  if (BATCH) std::cout << s << '\n';
  std::cout << "Enter elements ('"
       << s
       << "' to end): ";
  std::cin >> e;
  while (e != s)
  {
    if (BATCH) std::cout << e;
    v.PushBack(e);
    std::cin >> e;
  }
  if (BATCH) std::cout << e << '\n';

  // apply generic heapsort with default order and display
  std::cout << "data as entered:             " << v << '\n';
  fsu::g_heap_sort(v.Begin(), v.End());
  std::cout << "data after sort:             " << v << '\n';

  // copy to array
  size_t size = v.Size();
  A = new ElementType [size];
  fsu::g_copy(v.Begin(), v.End(), A);

  // now do binary search
  while(1)
  {
    std::cout << "Enter search value ('"
              << s
              << "' to quit): ";
    std::cin >> e;
    if (e == s) break;

    // display results LowerBound, UpperBound on array
    ElementType * altAloc = LowerBound(A, A + size, e);
    std::cout << "A = ";
    for (ElementType * Aitr = A; Aitr != A + size; ++Aitr) std::cout << *Aitr;
    std::cout << '\n';
    std::cout << "    ";
    for (ElementType * Aitr = A; Aitr != altAloc; ++Aitr) std::cout << ' ';
    std::cout << "^lb\n";

    // display results LowerBound, UpperBound on array
    altAloc = UpperBound(A, A + size, e);
    std::cout << "A = ";
    for (ElementType * Aitr = A; Aitr != A + size; ++Aitr) std::cout << *Aitr;
    std::cout << '\n';
    std::cout << "    ";
    for (ElementType * Aitr = A; Aitr != altAloc; ++Aitr) std::cout << ' ';
    std::cout << "^ub\n";
  }
  std::cout << "End test of recursive binary search\n";
}

File: makefile

#
#
# makefile for COP 4530 Homework 1a:
#
# The Good, The Bad, and The Ugly
#

HOME    = /home/courses/cop4530p/spring11
INCLUDE = -I. -I$(HOME)/cpp -I$(HOME)/tcpp
WARN    = -W -Wall -Wextra

all: fibo.x loop.x find.x

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

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

find.x: find.o
        g++ -ofind.x find.o

fibo.o: fibo.cpp
        g++ $(INCLUDE) $(WARN) -c fibo.cpp

loop.o: loop.cpp
        g++ $(INCLUDE) $(WARN) -c loop.cpp

find.o: find.cpp
        g++ $(INCLUDE) $(WARN) -c find.cpp