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 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:
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.
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.
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.
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...).
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.
Write a brief report answering questions (given below) about these programs and your experience creating them.
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.
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.
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.
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.)
RLoop should have a base case, a recursive call, and no loop structure.
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.
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.
Your new body for MergeSort should follow and implement this algorithm:
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.
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.
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.
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.
After copying the three .distribute files onto their respective .cpp files, build by issuing the make command. What executables are created?
Run each executable and describe its behavior.
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.
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.
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.)
Compare RFib and DFib.
Which of these is more efficient?
Which of these is more elegant?
Rate these as "good", "bad", and "ugly":
RFib
RLoop
MergeSort
Use each rating at least once.
Don't get confused about ranges: in C++, a range always includes the beginning index and excludes the end index, expressed as the "half open" interval [beg,end). This matches the standard pattern for a for loop:
for (size_t i = beg; i < end; ++i) // loops over the range [beg,end) { Whatever(); }
and for array/vector indexing:
for (size_t i = 0; i < v.size(); ++i) // loops over the entire vector { Whatever(); }
Note that an array or vector has valid indices in the range [0,size).
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