Recursion Self-Referencing Functions Problem # 1 Write a function that, given n, computes n! n! == 1 * 2 * ... * (n-1) * n Example: 5! == 1 * 2 * 3 * 4 * 5 == 120 Specification: Receive:n, an integer. Precondition: n >= 0 (0! == 1 && 1! == 1). Return: n!, a double (to avoid integer overflow). Preliminary Analysis At first glance, this is a counting problem, so we could solve it with a for loop: double Factorial(int n) { double result = 1.0; for (int i = 2; i <= n; i++) result *= i; return result; } But let’s instead learn a different approach... Analysis Consider: so: subsituting: n! == 1 * 2 * ... * (n-1) * n (n-1)! == 1 * 2 * ... * (n-1) n!== (n-1)! * n We have defined the ! function in terms of itself. Historically, this is how the function was defined before computers (and for-loops) existed. Recursion A function that is defined in terms of itself is called selfreferential, or recursive. Recursive functions are designed in a 3-step process: 1. Identify a base case -- an instance of the problem whose solution is trivial. Example: The factorial function has two base cases: if n == 0: n! == 1 if n == 1: n! == 1 Induction Step 2. Identify an induction step -- a means of solving the non-trivial (or “big”) instances of the problem using one or more “smaller” instances of the problem. Example: In the factorial problem, we solve the “big” problem using a “smaller” version of the problem: n! == (n-1)! * n 3. Form an algorithm from the base case and induction step. Algorithm // Factorial(n) 0. Receive n. 1. If n > 1: Return Factorial(n-1) * n. Else Return 1. Discussion The scope of a function begins at its prototype (or heading) and ends at the end of the file. Since the body of a function lies within its scope, nothing prevents a function from calling itself. Coding /* Factorial(n), defined recursively * ... */ double Factorial(int n) { if (n > 1) return Factorial(n-1) * n; else return 1.0; } Behavior Suppose the function is called with n == 4. int Factorial(int n) { if (n > 1) return Factorial(n-1) * n; else return 1; } Behavior The function starts executing, with n == 4. Factorial(4) n 4 return ? int Factorial(int n) { if (n > 1) return Factorial(n-1) * n; else return 1; } Behavior The if executes, and n (4) > 1, ... Factorial(4) n 4 return ? int Factorial(int n) { if (n > 1) return Factorial(n-1) * n; else return 1; } Behavior and computing the return-value calls Factorial(3). Factorial(4) n 4 return ? int Factorial(int n) { if (n > 1) return Factorial(n-1) * n; else return 1; } Behavior This begins a new execution, in which n == 3. Factorial(4) n 4 return ? Factorial(3) n 3 int Factorial(int n) return ? { if (n > 1) return Factorial(n-1) * n; else return 1; } Behavior Its if executes, and n (3) > 1, ... Factorial(4) n 4 return ? Factorial(3) n 3 int Factorial(int n) return ? { if (n > 1) return Factorial(n-1) * n; else return 1; } Behavior and computing its return-value calls Factorial(2). Factorial(4) n 4 return ? Factorial(3) n 3 int Factorial(int n) return ? { if (n > 1) return Factorial(n-1) * n; else return 1; } Behavior This begins a new execution, in which n == 2. Factorial(4) n 4 return ? Factorial(3) n 3 int Factorial(int n) return ? { if (n > 1) return Factorial(n-1) * n; else return 1; } Factorial(2) n 2 return ? Behavior Its if executes, and n (2) > 1, ... Factorial(4) n 4 return ? Factorial(3) n 3 int Factorial(int n) return ? { if (n > 1) return Factorial(n-1) * n; else return 1; } Factorial(2) n 2 return ? Behavior and computing its return-value calls Factorial(1). Factorial(4) n 4 return ? Factorial(3) n 3 int Factorial(int n) return ? { if (n > 1) return Factorial(n-1) * n; else return 1; } Factorial(2) n 2 return ? Behavior This begins a new execution, in which n == 1. Factorial(4) n 4 return ? Factorial(3) n 3 int Factorial(int n) return ? { if (n > 1) return Factorial(n-1) * n; else return 1; } Factorial(2) n 2 return ? Factorial(1) n 1 return ? Behavior The if executes, and the condition n > 1 is false, ... Factorial(4) n 4 return ? Factorial(3) n 3 int Factorial(int n) return ? { if (n > 1) return Factorial(n-1) * n; else return 1; } Factorial(2) n 2 return ? Factorial(1) n 1 return ? Behavior so its return-value is computed as 1 (the base case) Factorial(4) n 4 return ? Factorial(3) n 3 int Factorial(int n) return ? { if (n > 1) return Factorial(n-1) * n; else return 1; } Factorial(2) n 2 return ? Factorial(1) n 1 return 1 Behavior Factorial(1) terminates, returning 1 to Factorial(2). Factorial(4) n 4 return ? Factorial(3) n 3 int Factorial(int n) return ? { if (n > 1) return Factorial(n-1) * n; else return 1; } Factorial(2) n 2 return ? = 1 * 2 n 1 return 1 Behavior Factorial(2) resumes, computing its return-value: Factorial(4) n 4 return ? Factorial(3) n 3 int Factorial(int n) return ? { if (n > 1) return Factorial(n-1) * n; else return 1; } Factorial(2) n 2 return 2 = 1 * 2 Behavior Factorial(2) terminates, returning 2 to Factorial(3): Factorial(4) n 4 return ? Factorial(3) n 3 int Factorial(int n) return ? { if (n > 1) return Factorial(n-1) * n; else return 1; } = 2 * 3 n 2 return 2 Behavior Factorial(3) resumes, and computes its return-value: Factorial(4) n 4 return ? Factorial(3) n 3 int Factorial(int n) return 6 { if (n > 1) return Factorial(n-1) * n; else return 1; } = 2 * 3 Behavior Factorial(3) terminates, returning 6 to Factorial(4): Factorial(4) n 4 return ? = 6 * 4 n 3 int Factorial(int n) return 6 { if (n > 1) return Factorial(n-1) * n; else return 1; } Behavior Factorial(4) resumes, and computes its return-value: Factorial(4) n 4 return 24 = 6 * 4 int Factorial(int n) { if (n > 1) return Factorial(n-1) * n; else return 1; } Behavior Factorial(4) terminates, returning 24 to its caller. Factorial(4) n 4 return 24 int Factorial(int n) { if (n > 1) return Factorial(n-1) * n; else return 1; } Discussion If we time the for-loop version and the recursive version, the for-loop version will usually win, because the overhead of a function call is far more time-consuming than the time to execute a loop. However, there are problems where the recursive solution is more efficient than a corresponding loop-based solution. Problem # 2 Consider the exponentiation problem: Given two values x and n, compute xn. Example: 33 == 27 Specification: Receive: x, n, two numbers. Precondition: n >= 0 (for simplicity) && n is a whole number. Return: x raised to the power n. Analysis The loop-based solution requires @ n “steps” (trips through the loop) to solve the problem: double Power(double x, int n) { double result = 1.0; for (int i = 1; i <= n; i++) result *= x; } return result; There is a faster recursive solution. Analysis (Ct’d) How do we perform exponentiation recursively? Base case: n == 0 Return 1.0. Analysis (Ct’d) Induction step: n > 0 We might recognize that xn == x * x * ... * x * x // n factors of x and xn-1 == x * x * ... * x // n-1 factors of x and so perform a substitution: Return Power(x, n-1) * x. However, this requires @ n “steps” (recursive calls), which is no better than the loop version. Analysis (Ct’d) Induction-step: n > 0. Instead, we again begin with xn == x * x * ... * x * x // n factors of x but note that xn/2 == x * ... * x // n/2 factors of x and then recognize that when n is even: xn == xn/2 * xn/2 while when n is odd: xn == x * xn/2 * xn/2 Analysis (Ct’d) Induction step: n > 0. We can avoid computing xn/2 twice by doing it once, storing the result, and using the stored value: a. b. Compute partialResult = Power(x, n/2); If x is even: Return partialResult * partialResult. Else Return x * partialResult * partialResult. End if. This version is significantly faster than the loop-based version of the function, as n gets larger. Algorithm 0. Receive x, n. 1. If n == 0: Return 1.0; Else a. Compute partialResult = Power(x, n/2); b. if n is even: Return partialResult * partialResult; else Return x * partialResult * partialResult; end if. Coding We can then code this algorithm as follows: double Power(double x, int n) { if (n == 0) return 1.0; else { double partialResult = Power(x, n/2); } } if (n % 2 == 0) return partialResult * partialResult; else return x * partialResult * partialResult; which is quite simple, all things considered... How Much Faster? How many “steps” (recursive calls) in this version? Power(x, i) i Power(x, Power(x, Power(x, Power(x, Power(x, Power(x, Power(x, ... Power(x, 0) 1) 2) 4) 8) 16) 32) 0 1 2 4 8 16 32 n) n Steps (loop version) 0 1 2 4 8 16 32 n Steps (this version) 0 1 2 3 4 5 6 log2(n)+1 How Much Faster? (Ct’d) The larger n is, the better this version performs: Power(x, i) i Power(x, Power(x, Power(x, Power(x, ... 1024 2048 4096 8192 1024) 2048) 4096) 8192) Steps (loop version) 1024 2048 4096 8192 Steps (this version) 11 12 13 14 The obvious way to solve a problem may not be the most efficient way! Summary A function that is defined in terms of itself is called a recursive function. To solve a problem recursively, you must be able to identify a base case, and an induction step. Determining how efficiently an algorithm solves a problem is an area of computer science known as analysis of algorithms. Analysis of algorithms measures the time of an algorithm in terms of abstract “steps”, as opposed to specific statements.