Introduction to Alogrithms

advertisement
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.
Download