Recursion

Chapter 4

Understanding recursion

      To really understand what is happening with recursion, we need to look at what happens to a program on the lowest level; i.e. on the machine level

      We will take a brief look at what the compiler does, and what the loader (a component of the operating system) does.

      Then we will look at function calls and the activation record.

 

The compiler

      Remember, a program consists of two parts:

    The instructions to the machine

    The data your program works with

    Both of these require space in RAM when you program is loaded so it will execute

      Counting bytes

    When the compiler translates your source code into machine language, it keeps track of how many bytes are in the program.

    In addition, it counts the bytes the program uses to store data (this is the data stored in static RAM).

      Communicating with the operating system

    Part of the compiled program is a header which tells the loader the total number of bytes needed

    This header also contains information from the linker, so all the information for the executable program is here (in the header)

 

The loader

      The loader is the component of the operating system that loads the executable program into RAM

      The entire program with all its functions is not necessarily loaded contiguously.

      The loader puts the address where execution should begin into the program counter (a special register in the CPU).

      Then the operating system turns control of the computer over to the program (i.e. it executes).

      The instruction that is executed is the instruction whose address is in the program counter.

      For the program to loop or branch, or jump to a function, the address where execution should go must be put into the program counter

 

Function calls & Activation records

      When a function is called the system must keep tract of where to return when the function completes execution.

      It also needs to keep tract of the local environment for each function call.  This includes:

    The values of the arguments of the function

    Any local variables

    The value to be returned

      This information is kept on the system stack and is called an activation record.

      Example on overhead.

 

Recursive functions

      A function is recursive if it contains a call to itself

      Recursion breaks a problem down into several smaller problems

        A recursive solution solves a problem by solving smaller and smaller instances of the same problem

      Eventually, the new problem will be so small that its solution is known.

      This known solution helps solve each of the larger problems

 

Example: Finding the factorial of a number

      What is a factorial?

    Factorial(5) = 5*4*3*2*1

    Factorial(0) =1

    Factorial(n) = n*(n-1)*(n-2)*…*1

    The factorial of a negative integer is undefined

      Note: a recursive solution of this problem is not at all efficient

    But it is a good first example

      We will write this first as a function, then convert the function to an algorithm for a computer

 

Recursively defined functions

      To define any function recursively:

      1. Specify the value of the function at 0 or 1

            - this is the problem so small that you know the
         answer; i.e. the base case

      2. Give a rule for finding its value from its values at smaller cases

      To define the factorial function recursively:

  1. factorial(1) = 1;   (base case)

  2. factorial(n) = n * factorial(n-1)

 

Converting the function to an algorithm

   1. factorial(1) = 1;   (base case)

     2. factorial(n) = n * factorial(n-1)

 

int fact(int n)
 if (n = 1) return 1;
 else return n*fact(n-1);

      Look at the activation record for a call of  fact(4)

 

Decimal to Binary conversions

      One way to do this makes use of the fact that:

    The rightmost bit has the value of n%2;

    The second rightmost bit (n/2)%2

    Etc.

    For example: convert 29 to binary using this method

void writeBinary(int n)
{    if ( n==0 || n==1)  cout << n;
      else { writeBinary(n/2);
                cout << n%2; }
}

      Look at the execution of writeBinary(29) on the overhead.

void writeBinary(int n)

{   if (n==0 || n==1) cout<<n;

     else { writeBinary(n/2);

void writeBinary(int n)

{     if (n==0 || n==1)  cout<<n;

      else { writeBinary(n/2);

void writeBinary(int n)

{     if (n==0 || n==1)  cout<<n;

      else { writeBinary(n/2);

void writeBinary(int n)

{     if (n==0 || n==1)  cout<<n;

      else { writeBinary(n/2);

void writeBinary(int n)

{      if (n==0 || n==1)  cout<<n;

        else { writeBinary(n/2);

                                                                  cout << n%2; }                                        

}

      cout << n%2; }                                                                            

}

                              cout << n%2; }                                                    

}

                  cout << n%2; }           

}

     cout << n%2; }

}

 

 

Form of recursive solutions

      1. Every recursive call must make the problem smaller

      2. Each smaller problem must be exactly the same as the original problem, only smaller

      3. The problem must become small enough that you know the solution

   this is called the base case

      4. This strategy is called divide and conquer

 

Questions to ask about recursive solutions

      Can you define the problem in terms of a smaller problem of the same type?

       Does each recursive call diminish the size of the problem?

      Do you know the instance of the problem to be the base case?

      As the problem size diminishes, will it reach the base case?

 

Euclid's algorithm: finding the greatest common divisor

      How would you find the greatest common divisor  of 42 and 147?

    Most people would find the common factors.

      Euclid was a Greek who discovered a long time ago that

    If a and b are positive integers with a>b such that b is not a divisor of a, then    gcd(a,b) = gcd(b, a mod b)

      Writing this as a recursive function

      Base case:  gcd(a, b) = b if a mod b = 0

       Rule              gcd(a,b) = gcd(b, a mod b)

      Algorithm:

    int gcd(int a, int b)
 {  if  (a % b = = 0)  return b; 
    else    return gcd(b, a % b); 

     }       

Recursion and iteration

      Recursion is an alternative to iteration

      Anything that can be written recursively can also be written iteratively.

      Often recursive solutions are more elegant and simpler than iterative solutions

      But not all recursive solutions are better than iterative solutions

      Some are so inefficient that it may take years on the fastest computers to solve.

 

Counting things

      This problem illustrates two things

    More than one base case

    Tremendously inefficiency

      But right now we are interested in the process of recursion, not its efficiency

      Counting rabbits.  Assume:

    At two months of age rabbits can reproduce

    Rabbits are always born in male/female pairs

    Every month each mature male/female pair reproduces exactly one male/female pair

    Rabbits never die

      Problem:  start with one pair of young rabbits; how many will you have after 6 months?

 

How to compute rabbit(n)

      This is called the Fibonacci sequence

      We would like to be able to write a recursive function for number of rabbit pairs at month n

      Since this function is recursive, we know we will use month
(n-1)  but we also need to use month (n-2)

      Base case:  Month 1 there is one pair of rabbits; 
           rabbbit(1) = 1

      Rule:  rabbit(n) = rabbit(n-1) + rabbit(n-2)

    int rab(int n)
   if n <= 2  return 1;
   else return rab(n-1) + rab(n-2);