Chapter 3 Introduction to Alogrithms Simple sorting and searching techniques Intuitive understanding of running-time analysys Function templates Introduction to recursion Analysis of an Algorithm Several internal sorting algorithms are presented in this section to emphasize efficiency of sorting in terms of - system efficiency - space efficiency ( amount of memory required ) - computational efficiency relative to a) the number of items in the collection, b) initial ordering to distinguish best, worst, average case, and c) the number of key operations of the algorithm Computational Efficiency -“Big-O” notation- measure of computational efficiency Definition: Function f (n) is O (g(n)) if there is a constant K and a count n0 such that f(n) < = K*g(n), for n >= n0 The function g dominates the function f as n gets sufficiently large - The computational complexity of the algorithm is O(g(n)). Common Orders of Magnitude When comparing the efficiency of algorithms some aspects of the algorithm can be safely neglected and other aspects are of great importance. In search algorithms the aspect that is most important is the number of comparisons that have to be made. The section of the algorithm outside the loop is only important if the list is very small. For large lists statements outside the loop contribute little to the running time of the algorithm and only the number of basic actions become important. The word asymptotic means the study of functions of a parameter "n" (the size of the list), as "n" becomes larger and larger without bound. Analysis of an Algorithm( continued ) What follows are the common orders of computing time expressed in the simplest form.The Common orders are: O(1) means computing time that is bounded by a constant ( not dependent on "n"); O(n) means that the time is directly proportional to n, and is called linear time. 2 3 n O(n ) is called quadratic time, O(n ) cubic, O(2 ) exponential. These five orders, along with logarithmic time O(log n) and O(n log n) are the ones most commonly used in analyzing algorithms. Note: On a list of length n sequential search has time O(n). On a list of length n binary search has time O(log2 n) . If the number of basic actions is proportional to the size of n then doubling n will double the running time. If the number of basic actions is proportional to log2 n then doubling n will hardly change the running time. Successful Search Sequential Search Binary search 1/2(n+1) lg2 n + 1 Big Oh notation: O(n) O(log n) The average number of comparisons for an unsuccessful search is almost the same as for a successful search. Big Oh notation : Sequential Search Binary search 1/2(n+1) log2 n + 1 O(n) O(log2 n) Selection Sort 3.0n + O (1) 2 0.5n + O(n) Assignment of items Comparisons of keys Insertion Sort 2 0.25n + O (n) 2 0.25n + O(n) Logarithmic Time Algorithms The logarithm of n, base 2, is commonly used when analyzing computer algorithms. Ex. log2(2) = 1 log2(75) = 6.2288 When compared to the functions n and n2, the function log2 n grows very slowly. n2 n log2n The Selection Sort - Objective: Minimize data movement - move item immediately to its final position. Ex: Array A with five integers. 50 20 20 20 20 20 50 35 35 35 40 40 40 45 45 75 75 75 75 50 35 35 50 50 75 Pass 0: Select 20 - Exchange 20 with A[0] Pass 1: Select 35 - Exchange 35 with A[1] Pass 2: Select 40 - Exchange 40 and A[2] Pass 3: Select 50 - Exchange 50 and A[3] Sorted List // Selection Sort template < class T> void SelectionSort ( T A[], int n ) { int smallIndex; // index of smallest item in each pass for ( i = 0; i < n-1 ; i ++ ) { // start with scan at index i - set smallIndex to i smallIndex = i; // j scans the sublist A[i+1]... A[n-1] for ( j = i+1; j < n; j ++ ) // update smallIndex if smaller element is found if ( A[j] < A [ smallIndex ] smallIndex = j; // Exchange smallest item and place it in its final position swap ( A[i], A [ smallIndex ] ); } } Analysis of Selection Sort - We are able to calculate in advance how many times each loop will iterate - worst case and best case take almost the same time - The original ordering is not taken into account - sorted lists or nearly sorted lists take longer Major disadvantage: Does many redundant comparisons Primary Advantage: Data movement time is reduced. Binary Search // Search a sorted array for a match with key using the binary search. // return index of the matching array element or -1 if a match does not occur int BinSearch ( DataType list [] , int low, int high, DataType key ) { int mid; DataType midvalue; while ( low <= high ) { mid = ( low + high ) / 2 // mid index in the sublist midvalue = list [ mid ] ; // value at mid index if ( key == midvalue ) return mid; // found match - return location else if ( key < midvalue ) high = mid-1; // go to lower sublist else low = mid + 1; / go to higher sublist } return -1; // item not found } The Insertion Sort The main idea of the insertion sort is to examine the list of items to be sorted on item at a time and insert it into the proper place. Clearly, a list of one item is automatically sorted. Once the first i - 1 items have been sorted , we take i and look through the sorted list to see where to insert i . To find the place where to insert the unsorted item one can use either a sequential or binary search technique. Example A = 50, 20, 40, 75, 35. // insertion sort orders sublists A[0] .. A[i], 1 <= i <= n - 1; template <class T> void InsertionSort ( T A[ ], int n ) { int i, j; T temp; // i identifies the sublist A[0] .. A [i] - for each i insert A[i] in the // correct position for ( i = 1; i < n ; i ++ ) { // index j scans down the list from A[i] looking for correct // position to locate target. assigns it to A[j] j=i; temp = A [ i ]; // locate insertion point by scanning downward as long as // temp < A [ j - 1 ] // and we have not encountered the beginning of the list while ( j > 0 && temp < A [ j - 1 ] { // shift elements up list to make room for insertion A[j]=A[j-1]; j -- ; } A [ j ] = temp; // the location is found - insert temp } } Insertion Sort 50 Start with 50 Processing 20 20 50 Insert 20 in location 0; 50 moves to location 1 Processing 40 20 40 50 Insert 40 in location 1; 50 moves to location 2 Processing 75 20 40 50 75 Element 75 is ok Processing 35 20 35 40 50 75 Insert 35 in location 1; the tail of the list shifts to the right. Analysis Insertion Sort - In the best case ( items are already sorted or almost sorted ) insertion sort does a minimum amount of comparisons, but is inefficient in moving items ( only one place at a time ). Even after most items have already been sorted, later items may require that the sorted items be moved. - moving items may not be a big disadvantage if the items are linked. - If the records are large and stored in contiguous memory the insertion sort would be inefficient. The average number of comparisons and the average number of assignments of items for insertion sort applied to a list of n items in random order is 2 1/4 n + O(n) Note: To verify that a list is already sorted takes n-1 comparisons and can be done with the Insertion sort as quickly as can be done by any method. Bubble Sort In each pass - adjacent elements are compared and items are exchanged when the first element is greater than the second. At the end of each pass the largest element has bubbled up to the top of the current sublist. Example: Array of five integers Pass 0: 50 20 20 20 20 20 50 40 40 40 40 40 50 50 50 75 75 75 75 35 35 35 35 35 75 Exchange 50 and 20 Exchange 50 and 40 50 and 75 are ordered Exchange 75 and 35 75 is the largest element LastExchangeIndex = 3 Pass 1: 20 20 20 20 40 40 40 40 50 50 50 35 35 35 35 50 75 75 75 75 20 and 40 are ordered 40 and 50 are ordered Exchange 50 and 35 50 is the largest element lastExchangeIndex = 2 Pass 2: 20 20 40 35 35 40 50 50 70 75 20 and 40 are ordered Exchange 40 and 35 lastExchangeIndex = 1 pass 3: 20 20 35 35 40 40 50 50 75 75 20 and 35 are ordered Ordered List lastExchangeIndex = 0 Bubble Sort template < class T> void BubbleSort ( T A [ ] , int n ) { int i, j; int lastExchangeIndex; i = n-1; // i i index of last element in sublist // continue to process until no exchanges are made while ( i > 0 ){ lastExchangeIndex = 0; // scan sublist A[0] to A[i] for ( j = 0; j < i ; ++ j ) // Exchange a pair and update lastExchangeIndex if ( A[j+1] < A [ j ] ) { swap ( A [ j ] , A [ j + 1 ] ); lastExchangeIndex = j; } // set i to index of the last exchange and continue sorting the sublist A[0] to A[i] i = lastExchangeIndex; } } Computational complexity of the Bubble Sort - bubble sort maintains a record of the last exchange so that redundant scanning is not required - makes a single pass over a list that is already sorted in ascending order therefore, best case is O(n) 2 2 - worst case is O(n ) comparisons and O(n ) exchanges. - Average number of passes is O(n) , thus the total number of comparisons is 2 O(n ) - requires more interchanges than the selection sort and its average performance is slower, and - The exchange sort outperforms the bubble sort since it requires fewer interchanges Lower Bounds - How fast can we sort ? To answer this question we can examine sorting methods that rely entirely on comparisons between pairs of keys as with search algorithms. The comparison tree of a sorting algorithm shows the number of comparisons that must be made. the height of the tree is the largest number of comparisons and shows the number of comparisons that need to be made in the worst case. Below is the comparison trees of the insertion sort when n = 3. a <= b a <= c b <= c c<b<a b <= c b < a <= c b<=c<a a<=c a <= b <= c c<a<=b a<=c <b Lower Bounds - The height of the tree determines the largest number of comparisons that will be made. The external path length divided by the number of leaves gives the average number of comparisons. The number of ways that a list containing n items could originally have been ordered is n!. any algorithm that sorts a list of n items by use of key comparisons must perform at least lg2 n ! comparisons in the average case. Approximation of log2 n! is : n log2 n + O(n) 2 Insertion Sort: Comparisons of keys 0.25 n + O (n ) n = 1000 --- > 250,000 comparisons 2 Selection Sort: comparisons of keys 0.5 n + O (n ) n = 1000 ---- > 500,000 comparisons Lower Bound: ( optimal method ) should run n log2 n + O(n) n = 1000 ---> 8,500 comparisons The divide and conquer algorithms come close to providing the best performance that the lower bounds will allow. Template Functions C++ provides the capability that allows general type parameters for functions and classes. Template function declarations begin with the use of the template parameter list followed by the function in the usual format. template <class T , class T2, . . . . . class Tn> type function ( parameter list ) { } 1 Ex: Template function declaration: ( read class as type ) template <class T> int SeqSearch ( T list [ ] , int n, T key ) { for ( int i = 0; i < n; i++ ) if ( list [i] == key ) // operator == must be native to all // types T return i; return -1; } When a program calls a template function - the compiler identifies the data types of the actual parameters and associates these types with items in the template parameter list. - the compiler creates a separate instance of the function for each different run time parameter list. - if a template based function is called, all operations must be defined for the type- or provide a user defined version or use a non-template version of the function. Ex: int A[10], Aindex, Mindex; float M[100] fkey = 4.5; Aindex = SeqSearch( A, 10, 25 ); // Sequential search - data type int Mindex = SeqSearch( M, 100, fkey ); // Sequential search - float Template Functions If an operator in a template function is not defined for a particular data type the user can overload the operator or define a non-template version of the function. Operator Overloading Ex: Using operator == for a structured data type struct Student { int studID; float gpa; } //overload == by comparing student id int operator == ( Student a, Student b) { return a.studID == b.studID; } Ex1: Use of non-template version of SeqSearch - passing pointers to strings int SeqSearch ( char * list[ ], in n, char *key ) { for (int = 0; i N n; i ++ ) // compare strings if ( strcmp ( list[i], key == 0 ) // cannot use = = sign here // otherwise pointers are being // compared return i; return -1; } A Generic Search // include template based sequential search and a sequential search function // specific to C++ strings #include “utils.h” // declaration of student and == for student #include “student.h” void main () { int list[10] = { 5, 9, 1, 3, 4, 8, 2, 0, 7, 6 }; student studlist[3] = {{ 1000, 3.4 }, {1555, 2.6 } , { 1625, 3.8 } }; char *strlist [ 5 ] = { “zero”, “one”, “two”, “three”, “four “ ); int i , index; student studentKey = {1555, 0 }; if ( ( i = SeqSearch ( list, 10, 8 ) ) >= 0 cout << “Item 8 is found at index “ << i << endl; else cout << “ Item 8 is not found ” << endl; index = SeqSearch (studlist, 3, studentKey ); cout << “Student 1555 has gpa “ << studlist[index].gpa << endl; cout << “String ‘two’ is at index “ << SeqSearch(strlist,5,”two”) <<endl; } /* Run of Program */ Item 8 is found at index 5] Student 1555 has gpa 2.6 String ‘two’ is at index 2 Template Classes C++ template - allows a generic declaration of a class. Template classes are written using generic name (ex: T ) for the data type handled by the collection. When an object is declared , the actual type for T is provided as a parameter. Declaration 1: Declaration 2: class Collection { ............. int A[10]; // Array of integers }; template <class T> Class Collection { T A[10]; Collection Object; // generic declaration; // of the array - type is // specified when // declaring the object }; Collection<int> Collection<char> object; // array of // integers object; // array of //characters Template Classes Defining a template class #include <iostream.h> #include <stdlib.h> template <class T > class Store { private: T item; int haveValue; public: // constructor Store (void ); // data retrieval and storage operations T GetElement ( void ); void PutElement(const T& x ); }; Declaring a template class Store<int> X; Store<char> S[10]; // array of 10 objects - char data Defining template class methods A template class method can be defined inline or outside the class body. For an External function the method is treated as a template function with the template parameter list included in the function definition. All references to the class as a data type must include the template types enclosed in angle brackets. ClassName<T> :: Defining Template Methods: // External constructor template <class T> Store<T>:: Store(void ) : haveValue ( 0 ) {} // set flag to 0 template <class T> T Store<T> :: GetElement ( void ) { if ( haveValue = = 0 ) { cerr << “No item present!” << end; exit ( 1 ); } return item; // item is of type T } template <class T> void Store<T> : : PutElement( const T & x ) { haveValue ++; // flag - haveValue is true item = x ; } #include <iostream.h> #include “store.h” #include “student.h” void main (void ) { Student graduate = { 1000, 3.5 }; Store<int> A, B; Store<Student>S; Store<double> D; A.PutElement ( 3 ); B.PutElement (-7 ); cout << A.GetElement ( ) << “ “ << B.GetElement ( ) << endl; S.PutElement ( graduate ); cout << “The student id is “ << S.GetElement() . studID << endl; cout << “retrieving object D “ << D.GetElement ( ) << endl; } /* run of program */ 3 -7 The student id is 1000 retrieving object D No item present! #ifndef SEARCH_FUNCTIONS #define SEARCH_FUNCTIONS #include <vector> #include <list> using namespace std; // perform a sequential search of an integer array for a target // in the index range [first, last). return the index of a // match or last if the target is not in arr int seqSearch(const int arr[], int first, int last, int target); // perform a sequential search for a target in the index range // [first, last). return the index of a match or last if the // target is not in arr template <typename T> int seqSearch(const T arr[], int first, int last, const T& target); // perform a binary search of an integer array for a target // in the index range [first, last). return the index of a // match or last if the target is not in arr int binSearch(const int arr[], int first, int last, int target); // perform a binary search of an array for a target // in the index range [first, last). return the index of a // match or last if the target is not in arr template <typename T> int binSearch(const T arr[], int first, int last, const T& target); // vector version of the sequential search template <typename T> int seqSearch(const vector<T>& v, int first, int last,const T& target); // vector version of the binary search template <typename T> int binSearch(const vector<T>& v, int first, int last,const T& target); // perform the sequential search for target in the list // iterator range [first, last). return an iterator pointing // at the target in the list or last if target is not found template <typename T> list<T>::iterator seqSearch(list<T>::iterator first, list<T>::iterator last, const T& target); // perform the sequential search for target in the container // iterator range [first, last). return an iterator pointing // at the target in the container or last if target is not // found template <typename Iterator, typename T> Iterator find(Iterator first, Iterator last, const T& target); // *********************************************************** // search function implementations // *********************************************************** int seqSearch(const int arr[], int first, int last, int target) { int i; // scan indices in the range first <= i < last for(i=first; i < last; i++) if (arr[i] == target) return i; // immediately return on a match return last; // return last if target not found } template <typename T> int seqSearch(const T arr[], int first, int last, const T& target) { int i; // scan indices in the range first <= i < last for(i=first; i < last; i++) if (arr[i] == target) // assume T has the "==" operator return i; // immediately return on a match return last; // return last if target not found } int binSearch(const int arr[], int first, int last, int target) { int mid; // index of the midpoint int midValue; // object that is assigned arr[mid] int origLast = last; // save original value of last while (first < last) // test for nonempty sublist { mid = (first+last)/2; midValue = arr[mid]; if (target == midValue) return mid; // have a match // determine which sublist to search else if (target < midValue) last = mid; // search lower sublist. reset last else first = mid+1;// search upper sublist. reset first } return origLast; } // target not found template <typename T> int binSearch(const T arr[], int first, int last, const T& target) { int mid; // index of the midpoint T midValue; // object that is assigned arr[mid] int origLast = last; // save original value of last while (first < last) // test for nonempty sublist { mid = (first+last)/2; midValue = arr[mid]; if (target == midValue) return mid; // have a match // determine which sublist to search else if (target < midValue) last = mid; // search lower sublist. reset last else first = mid+1;// search upper sublist. reset first } return origLast; // target not found } template <typename T> int seqSearch(const vector<T>& v, int first, int last, const T& target) { int i; // scan indices in the range first <= i < last for(i=first; i < last; i++) if (v[i] == target) // assume T has the "==" operator return i; // immediately return on a match return last; // otherwise return last } template <typename T> int binSearch(const vector<T>& v, int first, int last, const T& target) { int mid; // index of the midpoint T midvalue; // object that is assigned v[mid] int origLast = last; // save original value of last while (first < last) // test for nonempty sublist { mid = (first+last)/2; midvalue = v[mid]; if (target == midvalue) return mid; // have a match // determine which sublist to search else if (target < midvalue) last = mid; // search lower sublist. reset last else first = mid+1;// search upper sublist. reset first } return origLast; // target not found } template <typename T> list<T>::iterator seqSearch(list<T>::iterator first, list<T>::iterator last, const T& target) { // start at location first list<T>::iterator iter = first; // compare list elements with item until either // we arrive at last or locate item while(iter != last && !(*iter == target)) iter++; // iter either points at item or is last return iter; } template <typename Iterator, typename T> Iterator find(Iterator first, Iterator last, const T& target) { Iterator iter = first; // scan iterator range [first, last), stopping // if the loop locates target while (iter != last && *iter != target) iter++; // if target located, iter points at it; otherwise // is has value last return iter; } #endif // SEARCH_FUNCTIONS // // // // // // // // // // // // File: prg3_1.cpp the program compares the efficiency of the sequential and binary search by timing algorithm execution using the timer class. two integer arrays list1 and list2 of size ARRAY_SIZE are initialized with the same random integers in the range 0 to 999,999. initialize the array targetList having TARGET_SIZE elements to contain other random numbers in the same range. time the selection sort as it sorts list2 and output the result. using the elements in array targetList as target values for the sequential and binary searches, time each algorithm and output the results #include <iostream> #include #include #include #include "d_search.h" "d_sort.h" "d_random.h" "d_timer.h" // generic sequential and binary searches // generic selection sort // random number generation // time events using namespace std; int main() { const int ARRAY_SIZE = 100000, TARGET_SIZE = 50000; // arrays for the search int list1[ARRAY_SIZE], list2[ARRAY_SIZE], targetList[TARGET_SIZE]; int i; // t used for timing the search algorithms timer t; // random number object randomNumber rnd; // initialize the arrays with random numbers in the // range 0 to 999,999 for (i = 0; i < ARRAY_SIZE; i++) list1[i] = list2[i] = rnd.random(1000000); // initialize targetList with random numbers in the // same range 0 to 999,999 for (i=0;i < TARGET_SIZE; i++) targetList[i] = rnd.random(1000000); // sort list2 cout << "Timing the Selection Sort" << endl; t.start(); // start timer selectionSort(list2,ARRAY_SIZE); t.stop(); // stop timer cout << "Selection Sort takes " << t.time() << " seconds." << endl; cout << endl << "Timing the Sequential Search" << endl; t.start(); // start timer // perform sequential search with elements from list2 for (i = 0; i < TARGET_SIZE; i++) seqSearch(list1,0,ARRAY_SIZE,targetList[i]); t.stop(); // stop timer cout << "Sequential Search takes " << t.time() << " seconds." << endl; cout << endl << "Timing the Binary Search" << endl; t.start(); // start timer // perform binary search with elements from list1 for (i = 0; i < TARGET_SIZE; i++) binSearch(list2,0,ARRAY_SIZE,targetList[i]); t.stop(); // stop timer cout << "Binary Search takes " << t.time() << " seconds." << endl; return 0; } /* Run: Timing the Selection Sort Selection Sort takes 126.912 seconds. Timing the Sequential Search Sequential Search takes 132.889 seconds. Timing the Binary Search Binary Search takes 0.08 seconds. */ The Concept of Recursion Recursion occurs when a problem is solved by partitioning it into smaller sub-problems that are solved by using the same algorithm. An algorithm is defined recursively if its definition consists of 1. One or more stopping conditions which can be evaluated for certain parameters 2. A recursive step in which a current value in the algorithm can be defined in terms of a previous value. Eventually, the recursive step must lead to stopping conditions. Recursion provides simple and elegant solutions to a range of problems.. - Tower of Hanoi - Maze ( backtracking ) - look ahead in games - Combinatorics - Expression Trees ( in-order, pre-order, and post-order scan ) - compilation by recursive descent ( parsing ) Designing Recursive Functions Example : Computing the factorial of a non-negative integer. The factorial of a non-negative integer Factorial (n) is defined as the product of all positive integers less than or equal to N. N ! = N * ( N - 1 ) * ( N - 2 ) * .... * 2 * 1 Iterative Version long Factorial ( long n ) { int prod = 1, i; // for n == 0 return prod = 1; otherwise // compute the prod = 1 *2 * ... n if ( n > 0 ) for ( i = 1; i <= n ; i ++ ) prod *= i; return prod; } Recursive Form long Factorial ( long n ) { // Stopping Condition is n == 0 if ( n == 0 ) return 1; // 1 * 0! = 1 else // recursive step return n * Factorial ( n-1); } Illustration of Run-time Stack long FACTORIAL ( long n ) { int temp; if ( n == 0 ) return 1; // pop activation record else { // push activation record with call FACTORIAL( n - 1 ) // RetLoc2 is the address of the computation // n * FACTORIAL( n - 1 ); temp = n * FACTORIAL ( n - 1 ); |_ RetLoc2 return temp; // pop activation record } } BINARY SEARCH In the following example, the binary search takes a specified key and scans an ordered array with N items looking for a match with the key. // Recursive version template < class T> int BinSearch ( T[ ] , int low, int high, T key ) { int mid; T midvalue; // stopping condition - key not found if ( low > high ) return -1; // compare against list midpoint and subdivide , if a match does not // occur, apply binary search again to the appropriate sublist else { mid = (low + high ) / 2; midvalue = A[mid]; // stopping condition - key found if ( key == midvalue ) return mid; // key found at index mid else if ( key < midvalue ) // recursive step return BinSearch ( A, low, mid-1, key ); else // recursive step return BinSearch ( A, mid+1, high, key ) ; } } Combinatorics: Permutations - Postponing the work - One part of the problem is resolved without recursion and the work of the remainder of the problem is postponed to the recursive call. Permutation ( N ) = N* (N-1) * (N-2 ) * ...... * * * 1 = N! // Permute an n element array of integers. Generate the permutations of the // elements whose indices are in the range start <=i <= n-1. When a // permutation is complete, print the entire array. To permute all n // elements, begin with start = 0. void permute ( int permlist[ ], int start, int n ) { int temparr [ UpperLimit ]; int temp i; // stopping condition - land at last array element if ( start == n - 1 ) { // print the permutation for ( i = 0, i < n ; i ++ ) cout << permlist [ i ] << “ “ ; cout << endl; }else // recursive step: exchange permlist [ start ] and // permlist [ i ] , make a copy of the array in temparr, // and permute elements of tmparr from start+1 // through end of array for ( i = start; i < n ; i ++ ) { // exchange permlist[i] with permlist [start ] temp = permlist [i]; permlist [ i ] = permlist [ start ]; permlist[ start ] = temp; // create a new list and call permute for ( int i = 0; i < n ; i ++ ) temparr[i] = permlist [ i ] ; permute ( tmparr, start+1, n ); } } Permutations List of all the permutations of N items . If N = 4 there are 24 permutations. 1234 1243 2134 2143 3124 3142 4123 4132 1324 1342 2314 2341 3214 3241 4213 4231 1423 1432 2413 2431 3412 3421 4312 4321 The hierarchy (recursion tree ) contains the ordered paths that correspond to the permutations and illustrates the algorithm for computing the number of permutations. On the first level there are four choices 1, 2, 3, 4 - corresponding to the four columns. On the next level there are three items, then two and one item at the lowest level. the total number of paths ( permutations ) is 4 * 3 * 2 * 1 Tower of Hanoi - the puzzle board contains three pegs called the start peg, middle peg, and end peg. On the start pegs are placed N disks Tower of Hanoi Algorithm - In general, the algorithm is a three step recursive algorithm. In the parameter list the order of the variables are startpeg - middlepeg - endpeg Disks are moved from start peg to endpeg using the middle peg to temporarily store the disks. If N = 1 ( a special stopping condition ) a single disk is moved, from the start peg to the end peg. Otherwise, Step 1 - Move N-1 disks from startpeg to middlepeg using endpeg for temporary storage Step 2- move largest disk from startpeg to endpeg Step 3: Move N-1 disks from middlepeg to endpeg using startpeg for temporary storage. Note : The order of the parameters in the recursive function call are middlepeg, startpeg, and endpeg // move n disks to middlepeg, move bottom disk // to endpeg, then move n-1 disks from middlepeg to endpeg void hanoi ( int n, string startpeg, String middlepeg, String endpeg) { // stopping condition: move one disk if ( n == 1 ) { cout << “move “ << startpeg << “ to “ << endpeg << endl; // move n-1 disks to middlepeg, move bottom disk // to endpeg, the move n-1 disks from middlepeg to endpeg else { hanoi ( n-1, startpeg, endpeg , middlepeg ); cout << “ move “ << startpeg << “ to “ << endpeg << endl; hanoi ( n - 1, middlepeg, startpeg, endpeg ); } } GUIDELINES FOR USING RECURSION Recursion allows the programmer to concentrate on the key step of an algorithm without having initially to worry about coupling that step with all the others. 1. Consider several simple examples. 2. Attempt to formulate a method that will work more generally and ask how the problem can be divided into parts. 3. Ask whether the remainder of the problem can be done in the same or a similar way. 4. Find a stopping rule that will indicate that the problem or a suitable part of it is done. 5. Verify that the recursion will always terminate. 6. Be sure that your algorithm correctly handles extreme cases. 7. The key tool for the analysis of recursive algorithms is the recursion tree. Draw the recursion tree for one or two simple examples appropriate to your problem. 8. Use the recursion tree to determine if you should write the algorithm in recursive form. Evaluating Recursion Often, recursion is not an efficient way to solve a problem and an iterative version would be more efficient. When not to use Recursion- Consider the following functions used to calculate factorials: // Factorial: recursive version for n! int Factorial ( int n ) { if ( n <= 1 ) return 1; else return n * Factorial ( n - 1 ); } // Factorial : Iterative version int factorial ( int n ) { int i, p; p = 1; for ( i = 2; i <= n ; i ++ ) p *= i; return p; } Both programs (interactive and recursive versions ) are equally simple. However, the recursive program uses much more storage space. The recursive program will have to set up a stack and store the calling parameters before each recursion and evaluate the numbers the same way as the iterative version. Recursion tree: n Progress of Execution n = 6 Factorial(6) = 6*Factorial(5) n-1 = 6*(5*Factorial(4)) n-2 = 6*(5*(4*Factorial(3))) n-3 = 6*(5*(4*(3*Factorial(2)))) 2 = 6*(5*(4*(3*(2*Factorial(1)))) 1 = 6*(5*(4*(3*(2*1)))) = 6*(5*(4*(3*2))) = 6*(5*(4*6)) = 6*(5*24) = 6*120 = 720 General Guidelines: - Recursion should be used freely in the initial design of algorithms especially where the main step toward a solution consists of reducing a problem to one or more smaller cases. - Verify that the algorithm terminates and handles trivial cases correctly. - Tail recursion should be removed if space considerations are important. Tail recursion is a special case when a recursive call is the last executed statement of the function. However, execution time ( with most modern compilers ) may not be much different. - The recursion tree should be the guide to detect duplication of work or inefficient division of the work. - If the recursion tree shows complete regularity that can be determined in advance, then sometimes this regularity can be built into the algorithm in a way that will improve efficiency and perhaps remove the recursion. - recursive functions and iterative functions using stacks accomplish exactly the same tasks. - Recursion can always be translated into iteration and stacks, but will often result in a more complicated program. - However, when a program involves stacks, the use of recursion should be considered to eliminate the stack. - Iteration is considered a bottom up approach to problem solving (it begins with what is known and constructs a solution). Recursion is a top down approach (it divides the problem into pieces or selects out one key step, postponing the rest.