Top Posters
Since Sunday
5
a
5
k
5
c
5
B
5
l
5
C
4
s
4
a
4
t
4
i
4
r
4
A free membership is required to access uploaded content. Login or Register.

Ch06 Mathematical Induction and Recursion.docx

Uploaded: 6 years ago
Contributor: bio_man
Category: Programming
Type: Other
Rating: N/A
Helpful
Unhelpful
Filename:   Ch06 Mathematical Induction and Recursion.docx (129.17 kB)
Page Count: 15
Credit Cost: 1
Views: 129
Last Download: N/A
Transcript
CHAPTER 6 Mathematical Induction and Recursion Mark Allen Weiss, Data Structures and Algorithm Analysis, The Benjamin/Cummings Publishing Company, Redwood City, CA Alan Tucker, Applied Combinatorics, John Wiley & Sons, New York Mathematical Induction -- Two steps 1. Prove theorem is true for some small value (degenerate case), say a. 2. Assume theorem is true for some . (or even all values between a and k). Prove theorem is true for k+1. If we can do that, then we know theorem is true for all values . That is, we can "get" to any value n by starting at a, and then stepping up 1 until we reach n. \* mergeformat A simple induction proof A bad induction proof Another bad induction proof Proving a property about Fibonacci numbers Proof that any integer n>1 can be written as a product of primes A prime number is an integer p>1 that is divisible by no other integer besides 1 and p Theorem Any integer n>1 can be written as a product of prime numbers. Proof (by induction) The initial step is to prove that 2 can be written as a product of primes. But 2 is itself prime. Thus 2 is trivially the product of primes, i.e. it is itself a prime. Now assume that the numbers 2, 3,...,n-1 can be written as a product of primes. We will use this assumption to prove that n can also be written as a product of primes. If n is prime, then it can trivially be written as a product of primes. Suppose n is not prime. Then, there exists an integer m which divides n. If k=n/m, then n=k m. Since k and m must be less than n and greater than 1, they too can each be written as a product of primes. By multiplying these two prime products for k and m, one obtains the desired representation of n as a product of primes. Recursion -- introduction Reference: Brooks, Chapter 5 (5.4) Consider the definition of n! (i.e. n factorial) One way to look at it: Another way to look at it: To compute 4! using the second definition, we would proceed as follows: We employ the procedure of computing n! recursively until we reach the base case of its definition Recursion -- introduction (2) Our description of n! is an example of a recurrence relation A recurrence relation is simply a function which is defined recursively For example, the following gives a recurrence relation for n! To prove that f(n) = n!, we can use mathematical induction example Prove that f(n) = n! Recursion -- introduction (3) We can implement a function in C to compute n! using either iteration or recursion Iterative C implementation 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 long factorial (long n) { long i, result = 1; if (n < 0) return 0; if (n == 0) return 1; for (i = 1; I <= n; i++) result *= i; return result; } Recursive C implementation 1 2 3 4 5 6 7 8 9 long factorial (long n) { if (n < 0) return 0; else if (n == 0) return 1; else return n * factorial (n - 1); } Recursion -- introduction (4) Each block is given its own activation record containing all automatic variables Consider calling the recursive factorial(4) Let us see what the activation frame looks like at each level 1 2 3 4 5 6 7 8 9 long factorial (long n) { if (n < 0) return 0; else if (n == 0) return 1; else return n * factorial (n - 1); } Initial call to factorial(4) \* mergeformat Recursive call to factorial(3) 1 2 3 4 5 6 7 8 9 long factorial (long n) { if (n < 0) return 0; else if (n == 0) return 1; else return n * factorial (n - 1); } \* mergeformat Recursive call to factorial(2) 1 2 3 4 5 6 7 8 9 long factorial (long n) { if (n < 0) return 0; else if (n == 0) return 1; else return n * factorial (n - 1); } \* mergeformat Recursive call to factorial(1) 1 2 3 4 5 6 7 8 9 long factorial (long n) { if (n < 0) return 0; else if (n == 0) return 1; else return n * factorial (n - 1); } \* mergeformat Recursive call to factorial(0) 1 2 3 4 5 6 7 8 9 long factorial (long n) { if (n < 0) return 0; else if (n == 0) return 1; else return n * factorial (n - 1); } \* mergeformat Returning from the recursive call to factorial(0) 1 2 3 4 5 6 7 8 9 long factorial (long n) { if (n < 0) return 0; else if (n == 0) return 1; else return n * factorial (n - 1); } \* mergeformat Returning from the recursive call to factorial(1) 1 2 3 4 5 6 7 8 9 long factorial (long n) { if (n < 0) return 0; else if (n == 0) return 1; else return n * factorial (n - 1); } \* mergeformat Returning from the recursive call to factorial(2) 1 2 3 4 5 6 7 8 9 long factorial (long n) { if (n < 0) return 0; else if (n == 0) return 1; else return n * factorial (n - 1); } \* mergeformat Returning from the recursive call to factorial(3) 1 2 3 4 5 6 7 8 9 long factorial (long n) { if (n < 0) return 0; else if (n == 0) return 1; else return n * factorial (n - 1); } \* mergeformat Recursion -- how it works Recall that each block is given its own activation record containing all automatic variables The recursive implementation of factorial uses the system stack to store intermediate values On the other hand, the iterative implementation of factorial uses only one variable to store the intermediate results Thus, we observe that the amount of space required by the iterative version is constant The recursive version, though, requires a unique activation frame for each function call The number of activation frames (at the worst case) is n+1 Thus, the space requirement of the recursive version is linear in the number of recursive calls! To wit: the iterative version (in this case) uses a lot less system resources than the recursive version This is because the best algorithm for computing n! requires constant space because we only need to know the previous value Recursive implementations are appropriate where the amount of space consumed would have been consumed anyway i.e., we would have had to stack the input anyway Recursion -- concepts and program design Simple mathematical formulas Recursive formulas C implementation 1 2 3 4 5 6 7 long f (long x) { if (x == 0) /* base case */ return 0; else return 2 * f (x - 1) + x * x; } Lines 3 and 4 handle the base case, i.e. the value which is known without resorting to recursion Line 6 makes the recursive call Without lines 3 and 4, f would go into an infinite loop Note that an attempt to evaluate f(-1) will result in calls to f(-2), f(-3), and so on Because this never gets to a base case, the program will never terminate with any of those values Recursion (2) The following routine is a bad use of recursion because it never terminates 1 2 3 4 5 6 7 long bad (long n) { if (n == 0) return 0; else return bad (n / 3 + 1) + n - 1; } Calling bad(1) will call bad(1) at line 6 and go into an infinite loop Calling bad(2) will call bad(1) which will once again go into an infinite loop In addition, bad(3), bad(4), and bad(5) all call bad(2) which goes into an infinite loop The only value of n for which this program works is its special case, i.e. n=0 These examples illustrate the necessity for the first two fundamental rules of recursion 1. Base cases You must always have some base case which can be solved without recursion 2. Making progress For the cases that are to be solved recursively, the recursive call must always be to a case that makes progress toward a base case Recursion (3) -- printing out numbers Suppose we wish to write a program in a language to print out a positive integer n but where the only I/O routines available will take a single digit number and print it to the terminal Given: void print_digit (int digit); Implement: void print_number (int n); Solution (recursive) To print out the number 67234, we need to first print out 6723 followed by a 4 We accomplish the second step by calling print_digit (n % 10); We can achieve the first part by calling print_number (n/10); /* integer division */ 1 2 3 4 5 6 7 8 9 10 11 12 int print_number (int n) { if (n < 0) { /* error -- n is negative */ return –1; } else if (n < 10) { print_digit (n); } else { print_number (n / 10); /* recursive call */ print_digit (n % 10); } return 0; } Recursion (4) -- printing out numbers 1 2 3 4 5 6 7 8 9 10 11 12 int print_number (int n) { if (n < 0) { /* error -- n is negative */ return -1; } else if (n < 10) { print_digit (n); } else { print_number (n / 10); print_digit (n % 10); } return 0; } Recursion (5) Notice that the proof is almost identical to the algorithm description. This illustrates that when designing a recursive program, all smaller instances of a problem may be assumed to work correctly. The recursive program combines solutions to the smaller problems which had to be solved immediately. The mathematical justification is proof by induction. This is a very profound concept Think about the justification behind mathematical induction Think about what a computer does when it encounters a recursive call and when the actual work is done A lot of bookkeeping is done on the stack... Four fundamental rules when writing a recursive program 1. Base cases You must always have some base case which can be solved without recursion 2. Making progress For the cases that are to be solved recursively, the recursive call must always be to a case that makes progress toward a base case 3. Design rule Assume that all recursive calls works (like in mathematical induction) 4. Compound interest rule Never duplicate work by solving the same instance of a problem in separate recursive calls Analyzing recursive procedures The total time for a recursive procedure is usually a recurrence relation In some cases, the recursion is a thinly veiled for loop; in this case, analyze it as if it were a for loop example 1 2 3 4 5 6 7 int factorial (int n) { if (n == 0) return 1; else return n * factorial (n - 1); } By inspection, we can see that factorial is O(n) However, to prove it, we would need to derive a recurrence relation and then prove an upper bound on that recurrence Due to the bookkeeping costs required by recursion, it also uses O(n) space (on the stack) A loop version can be written using constant space Recursion is appropriate when you implicitly would want to use a stack to store intermediate results We will see that tree traversal is one good example Recursive descent parsing is another good example A formal derivation of the time bound for factorial 1 2 3 4 5 6 7 int factorial (int n) { if (n == 0) return 1; else return n * factorial (n - 1); } An example of how not to use recursion 1 2 3 4 5 6 7 int fib (int n) { if (n <= 1) return 1; /* 0 or 1 */ else return fib (n - 1) + fib (n - 2); } To analyze this program, let T(n) = running time for fib(n) If n is either 0 or 1, the running time is that of lines 3 and 4 which is constant, i.e. O(1) If n > 2, the time is equal to sum of the time it takes to execute line 3 the time it takes to do line 6 Line 6 consists of two function calls plus an addition Thus, line 6 requires T(n-1) + T(n-2) + 1 Consequently, for adding lines 3 and 6 yields T(n) = T(n-1) + T(n-2) + 1 + 1 = T(n-1) + T(n-2) + 2 We can prove by induction that We can also prove by induction that In addition, we can prove that Thus, , which implies that the running time of function fib grows exponentially This is very bad The reason why function fib is so bad 1 2 3 4 5 6 7 int fib (int n) { if (n <= 1) return 1; /* 0 or 1 */ else return fib (n - 1) + fib (n - 2); } There is a huge amount of redundant work being done This violates the fourth major rule of recursion the compound interest rule The first call on line 6, fib(n-1), computes fib(n-2) at some point and then throws it away This value is then recomputed by the second call to fib, i.e. fib(n-2) Basic maxim DO NOT COMPUTE ANYTHING MORE THAN ONCE The much faster iterative version of fib 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 long fib (long n) { long i, back_1, back_2, result; back_2 = 1; back_1 = 1; if (n <= 1) return 1; /* 0 or 1 */ for (i = 2; i <= n; i++) { result = back_1 + back_2; back_2 = back_1; back_1 = result; } return result; } In this example, we observe that we only need to know the last two computed values in order to compute the next value This follows directly from the recurrence relation The loop computes each Fibonacci number by starting at 2 and working its way upward Clearly, the number of iterations is bounded above by n The amount of space required is constant A good example of how to use recursion -- binary search The binary search problem is given as follows. One solution would be to scan through the input from left to right This algorithm, though, obviously runs in linear time It is far better to derive an algorithm which takes advantage of the property that the input is sorted and already in memory We want our program to search for the object the same way we search for a word in a dictionary The program will look in the middle of the input If it finds the element, it will return the index Otherwise, it will narrow its search to either the lower or upper half of the input In this sense, it is able to cut its search in half, thus its name binary search \* mergeformat The binary search function 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 /** ** To call this function, pass in the array ** a[], the lower bound (0) and the upper ** bound (n-1). This function returns the ** index of the key within a[]. **/ int binsearch (int a[], int low, int high, int key) { int mid = (low + high) / 2; if (low > high) { /* not in array */ return -1; } if (key < a[mid]) /* recursive call */ return binsearch (a, low, mid - 1, key); else if (key > a[mid]) /* recursive call */ return binsearch (a, mid + 1, high, key); else /* if (key == a[mid]) */ return mid; /* found it */ } \* mergeformat The complexity of this function equals the number of calls to this function Because this function always divides its input in half before proceeding, the number of calls to this procedure is bounded above by log(n) Also, the amount of space used on the stack is also bounded by log(n) It is clearly possible to write an equivalent iterative version which uses O(1) space the iterative version is marginally better than the recursive version because log(n) tends to be very small even for large input, the difference in efficiency is marginal indeed Input string reversal -- an even better example of recursion Consider the following two routines to reverse a line of input from the terminal One works and the other doesn't The one that works uses O(n) space (as it should) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include void reverse (void) { int ch = getchar (); switch (ch) { case '\n': putchar (ch); case EOF: break; default: reverse (); putchar (ch); } } void reverse2 (void) { static int ch = getchar (); switch (ch) { case '\n': putchar (ch); case EOF: break; default: reverse2 (); putchar (ch); } } Beware of “non-reentrant” functions… Precedence and order of evaluation Operator Associativity Order of Evaluation () [] -> . left to right - ! ~ ++ -- (unary) + - (indirection) * & ( type ) sizeof right to left - * / % left to right - + - left to right - << >> left to right - <= < > >= left to right - == != left to right - & left to right - ^ left to right - | left to right - && left to right left to right sequence point after first argument short circuit || left to right left to right sequence point after first argument short circuit ?: right to left first operand eval sequence point after first argument = += -= *= /= %= &= |= <<= >>= right to left - , left to right left to right sequence point after first argument Parse trees example: x = a + b * c We wish to draw the parse tree for this string First, we must fully parenthesize it: x = (a + (b * c)) Now we can draw the parse tree \* mergeformat The higher precedence operand is lower in the tree. To evaluate the expression, do a preorder (or postorder) traversal of the tree. preorder: visit root, traverse left subtree, traverse right subtree inorder: traverse left subtree, visit root, traverse right subtree postorder: traverse left subtree, traverse right subtree, visit root example -- continued \* mergeformat preorder: visit root, traverse left subtree, traverse right subtree inorder: traverse left subtree, visit root, traverse right subtree postorder: traverse left subtree, traverse right subtree, visit root prefix string: = x + a * b c infix string: x = a + b * c postfix string: x a b c * + = (like HP calculator) Standard exercises 1. Given expression, draw parse tree 2. Given parse tree, derive infix expression with minimal parentheses 3. Given either prefix or postfix expressions, derive infix expression (Hint: draw the parse tree) example \* mergeformat infix string (without parentheses) a * b - c - d + e * f - g - h - i C expression with required parentheses (a * b - (c - d) + e) * (f - g - h - i) example Give the parse tree for: a << b + c * d & e | f & g ? h == i : j answer 1. Fully parenthesize it (((a << (b + (c * d))) & e) | (f & g)) ? (h == i) : j 2. Draw the parse tree \* mergeformat

Related Downloads
Explore
Post your homework questions and get free online help from our incredible volunteers
  1402 People Browsing