CMPS 144 Recursion Lecture Following the ideas of Velazquez-Iturbide (SIGCSE'00 Proceedings, pp. 310-314) we introduce recursion in a gradual way here. Recursion has to do with defining/describing something in terms of itself. It is used in describing kinds of data (e.g., bit strings), mathematical functions, and algorithms. (1) (Context-free) Grammars. A (context-free) grammar consists of "terminal" symbols, "nonterminal" symbols, and "rules", each of which describes how a specified nonterminal symbol can be replaced/rewritten by a specified string of symbols. One of the nonterminal symbols is designated as the "start" symbol. Any string of terminals that can be generated, beginning with the start symbol, via applications of the rules, is said to be in the "language" of the grammar. Example 1: A grammar generating the set of all (non-empty) bit strings: --------- --> 0 (1) --> 1 (2) --> 0 (3) --> 1 (4) The parenthesized numbers to the right are not part of the grammar; rather, they are simply for the purpose of identifying each rule. Here, the only nonterminal symbol is . (We shall follow the convention, which is widely used, that nonterminals are enclosed in brackets.) The two terminal symbols are '0' and '1'. Rule (1) says that 0 is a bit string. Rule (2) says that 1 is a bit string. These are the "base" rules. Rule (3) says that 0 followed by any bit string is itself a bit string. Rule (4) says that 1 followed by any bit string is itself a bit string. These are the "recursive" rules. What this grammar does, in effect, is to describe each bit string of length k+1 in terms of a bit string of length k. To generate the string 01001: ==> 0 (by (3)) ==> 0 1 (by (4)) ==> 0 1 0 (by (3)) ==> 0 1 0 0 (by (3)) ==> 0 1 0 0 1 (by (2)) An equivalent grammar, in which is introduced as a nonterminal symbol but in which is still regarded as the start symbol, is: --> 0 --> 1 --> --> Notice that we could have reversed the order of the nonterminals in the last rule (obtaining --> ) without changing the set of strings derivable from . In order to save vertical space, sometimes we list more than one rule on a line, separating their respective right-hand sides by vertical bars. Using this convention, the grammar above could be given by --> 0 | 1 --> | Example 2: ---------- Grammar generating the set of strings representing nonnegative integers: --> 0 | 1 | 2 | ... | 9 --> | Again, in the last rule, the order of nonterminals doesn't matter How about if we don't want to allow "leading zeros" in our strings? A grammar by which precisely such strings can be generated is this: --> 1 | 2 | ... | 9 --> 0 | --> --> 0 | Example 3: Grammar generating all well-formed (nonempty) strings of parentheses (e.g., (()())() is such a string): --> () --> ( ) --> Example 4: Grammar generating all palindromes over {0,1}: --> lambda (lambda denotes the string of length zero) --> 0 --> 1 --> 0 0 --> 1 1 If the first rule were omitted, only odd-length palindromes would be generable. Example 5: Grammar generating all fully-parenthesized arithmetic expressions (FPAE's). --> + | - | * | / --> ( is from Example 2) --> ( ) The first rule indicates that the four arithmetic operators are those that denote addition, subtraction, multiplication, and division, respectively. The second rule indicates that a nonnegative integer is an FPAE. The third rule indicates that an expression that begins with a left parenthesis, which is followed by an FPAE, which is followed by an operator, which is followed by another FPAE, which is followed by a right parenthesis, is an FPAE. ------------------------------------------------------------------------ (2) "Functional Programming" Recursion: Here, the value of the expression can be calculated without concern for parameter passing, etc. ?? Example 1: Factorial Function You may have seen it defined like this: n! = 1 * 2 * ... * n But this is BAD, because "..." is ambiguous! A better way to define it is this: 0! = 1 n! = n * (n-1)! (n>0) Another way of writing it: n! = { 1 if n = 0 (1) { n * (n-1)! otherwise (if n > 0) (2) As an example of evaluation: 5! = 5 * (5-1)! (by (2)) = 5 * 4! (by 5-1 = 4) = 5 * 4 * (4-1)! (by (2)) = ... = ... = 5 * 4 * 3 * 2 * 1! = 5 * 4 * 3 * 2 * 1 * (1-1)! (by (2)) = 5 * 4 * 3 * 2 * 1 * 0! (by 1-1=0) = 5 * 4 * 3 * 2 * 1 * 1 (by (1)) Example 2: Exponentiation: x^0 = 1 x^n = x * x^{n-1} (n>0) Evaluate something like 3^5. Another approach (which happens to be "more efficient"): x^0 = 1 (1) x^n = sqr(x^(n/2)) (n>0 and n even) (2) x^n = sqr(x^((n-1)/2)) * x (n>0 and n odd) (3) (where sqr(y) is an abbreviation for y*y) Example evaluation: 2^9 = sqr(2^4) * 2 (by (3)) = sqr( sqr(2^2) ) * 2 (by (2)) = sqr( sqr( sqr(2^1) ) ) * 2 (by (2)) = sqr( sqr( sqr( 2^0 * 2 ) ) ) * 2 (by (3)) = sqr( sqr( sqr(1 * 2) ) ) * 2 (by (1)) = sqr( sqr( sqr(2))) * 2 (by 1 * 2 = 2) = sqr( sqr( 2*2 )) * 2 (by defn. of sqr) = sqr( (2*2)*(2*2)) * 2 (by defn. of sqr) = ((2*2)*(2*2) * (2*2)*(2*2)) * 2 (by defn. of sqr) = 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 * 2 (by associativity of *) Example: Sum of elements in an array segment Sum(a,low,high) = { 0 if low >= high { a[low] + Sum(a,low+1,high) ) otherwise Example: Is a specified element in an array segment? In(a,low,high,x) = { false if low >= high { (x == a[low]) OR In(a,low+1,high,x) otherwise ----------------------------------------------------------------------- (3) Writing (Java) methods that are recursive (which call themselves) Use some of the examples above. /* pre: k >= 0 * post: value returned is k! */ public int factorial(int k) { int result if (k == 0) { result = 1; } else { result = k * factorial(k-1); } return result; } Here it might be useful to step through an execution of this method, showing the contents of the run-time stack. Among important ideas are that each call to the method results in a new "instance" of it on the run-time stack, so that each instance has its own version of the local variables and arguments of the method. For other examples (e.g., binary search, quicksort), see other web pages.