410 likes | 422 Views
CS 320 Principles of Programming Languages Simulating Recursion. Dr. Herbert G. Mayer, PSU Status 11/19/2017. Syllabus. Definition of Recursive Algorithm Recursion vs. Iteration Fact() and Fibo(), Recursive vs. Iterative Q-Sequence Ackermann Function Stack Data Structure
E N D
CS 320Principles of Programming Languages Simulating Recursion Dr. Herbert G. Mayer, PSU Status 11/19/2017
Syllabus • Definition of Recursive Algorithm • Recursion vs. Iteration • Fact() and Fibo(), Recursive vs. Iterative • Q-Sequence • Ackermann Function • Stack Data Structure • Simulate Recursion via Iteration • References
Definition of Recursive Algorithm • An algorithms is recursive, if it is partly defined by simpler versions of itself [1] • A recursive program is the implementation of a recursive algorithm • What is the problem for a programmer, using a language that is non-recursive (e.g. standard Fortran) if the algorithm to be implemented is recursive? --See later! • What then are the other parts of a recursive algorithm, aside from the partly? • Correct recursive algorithm requires a starting point, formally known as “base case” • Base case could be multiple steps • Simpler version means: the algorithm cannot use the original call. For example, if the original call was a(n), the recursive call cannot also be a(n), but perhaps a(n-1) • Recursive body can be indirectly recursive through intermediate function • a()-> b()-> a() – through intermediate function b() • Primitive examples are the factorial( n ) function; or Fibonacci( n ), for non-negative arguments n; Fibo( n ) shown here, discussed next: • Base case 1: Fibo(0) = 0 • Base case 2: Fibo(1) = 1 • Recursive Definition: Fibo( n ) for n > 1 = Fibo( n-1 ) + Fibo( n-2 )
Recursion vs. Iteration • Iteration is expressed in programming languages by loops; e.g. for-, while-, do-, or repeat loops • These are readable and efficient methods for expressing iteration, but are not strictly necessary • Recursion can replace iterative steps; yet for some people this seems counter-intuitive • Neophytes are sometimes unused to recursion; yet recursion can be as intuitive as simple iteration, only more so
Fact() and Fibo(), Recursive +Iterative • We’ll show both solutions for the factorial and the fibonacci functions • Factorial: fact1() iterative, fact2() recursive • Fibonacci: fibo1() iterative, fibo2() recursive • Sometimes the recursive algorithm is easier to see + understand, if the problem is inherently recursive • Fibonacci was Italian mathematician in Pisa, 1170 – c. 1250; introduced Hindu-Arabic numeral system to Europe • Also show the famous Towers of Hanoi
Fact() and Fibo(), Recursive +Iterative • Factorial(n) is the product of the first n unsigned numbers, with factorial(0) defined as = 1 • Fibonacci(n) is defined as 0 and 1 for arguments indexed 0, and 1. For all other n the result is the sum of the previous 2 Fibonacci numbers, i.e. Fibonacci(n) = Fibonacci(n-1) + Fibonacci(n-2)
Fact(), Recursive and Iterative • #include <iostream.h> • unsigned fact1( unsigned arg ) // iterative • { // fact1 • unsigned int result = 1; • for ( int i = 1; i <= arg; i++ ) { • result *= i; • } //end for • return result; • } //end fact1 • unsigned fact2( unsigned arg ) // recursive • { // fact2 • if ( arg < 2 ) { • return 1; • }else{ • return arg * fact2( arg-1 ); • } //end if • } //end fact2
Fact(), Recursive and Iterative int main( void ) { // main for ( int i = 0; i < 10; i++ ) { cout << "fact1(" << i << ") = " << fact1( i ) << endl << "fact2(" << i << ") = " << fact2( i ) << endl; } //end for return 0; } //end main fact1(0) = 1 fact2(0) = 1 fact1(1) = 1 fact2(1) = 1 fact1(2) = 2 fact2(2) = 2 . . . fact1(7) = 5040 fact2(7) = 5040 fact1(8) = 40320 fact2(8) = 40320 fact1(9) = 362880 fact2(9) = 362880
Fibo(), Recursive and Iterative • #include <iostream.h> • unsigned fibo1( unsigned arg ) // iterative • { // fibo1 • unsigned fm2 = 0; • unsigned fm1 = 1; • unsigned fm0 = 1; • for ( int i = 1; i <= arg; i++ ) { • fm2 = fm1; • fm1 = fm0; • fm0 = fm1 + fm2; • } //end for • return fm2; • } //end fibo1 • unsigned fibo2( unsigned arg ) // recursive • { // fibo2 • if ( arg < 2 ) { • return arg; • }else{ • return fibo2( arg-1 ) + fibo2( arg-2 ); • } //end if • } //end fibo2
Fibo(), Recursive and Iterative int main( void ) { // main for ( int i = 0; i < 10; i++ ) { cout << "fibo1(" << i << ") = " << fibo1( i ) << endl << "fibo2(" << i << ") = " << fibo2( i ) << endl; } //end for return 0; } //end main fibo1(0) = 0 fibo2(0) = 0 fibo1(1) = 1 fibo2(1) = 1 fibo1(2) = 1 fibo2(2) = 1 fibo1(3) = 2 fibo2(3) = 2 fibo1(4) = 3 fibo2(4) = 3 . . . fibo1(9) = 34 fibo2(9) = 34
Replace Iteration via Recursion • What is the problem, if programmed algorithm is recursive, yet language does not allow recursion? • Rewrite the algorithm! • Or simulate recursion via iteration! • Here we do the opposite: Use recursion to simulate all mathematical dyadic operations + - / * etc. • Using only function calls, generally recursive • Pre and post-increment/decrement operators ++ -- • Unary minus - • Conventional relational operators > >= < <= etc. • But all other common operators are dis-allowed, i.e. one cannot use: + - * / % **etc.
Recursion vs. Iteration: add() • // return a + b without + operation! • int add( int a, int b ) • { // add • if ( 0 == b ) { • return a; • }else if ( b < 0 ) { • return add( --a, ++b ); • }else{ • return add( ++a, --b ); • } //end if • } //end add
Recursion vs. Iteration: sub() • // return a – b; no dyadic – operation • int sub( int a, int b ) • { // sub • return add( a, -b ); • } //end sub
Recursion vs. Iteration: mult() • // return a * b, no * but add() • int mult( int a, int b ) • { // mult • if ( 0 == b ) { • return 0; • }else if ( 1 == b ) { • return a; • }else if ( b < 0 ) { • return -mult( a, -b ); • }else{ • // b > 0 • return add( a, mult( a, --b ) ); • } //end if • } //end mult
Recursion vs. Iteration: expo() • // return a ** b, no ** op in C++; requires mult( int, int ) • int expo( int a, int b ) • { // expo • if ( 0 == a ) { • if ( 0 == b ) { • printf( ”undefined value0^0\n" ); • }else if ( b < 0 ) { • printf( “0 to <0 power is undefined\n" ); • } //end if • return 0; • }else if ( 0 == b ) { • return 1; • }else if ( 1 == a ) { • return 1; • }else if ( -1 == a ) { • return b % 2 ? -1 : 1; • }else if ( b < 0 ) { • return 0; • }else{ • return mult( expo( a, --b ), a ); • } //end if • } //end expo
Q-Sequence, Definition • Q-Sequence defined by Douglas Hofstadter in [1] as a function q( n ) for positive integers n > 0 • Base case n = 1: q(1) = 1 • Base case n = 2: q(2) = 1 • Recursive definition of q(n), for positive n > 2 • q( n ) = q( n – q( n - 1 ) ) + q( n – q( n - 2 ) ) • Q-Sequence reminds us of Fibonacci( n ) function, but with surprising difference in the type of result: • Note: the function results of fibonacci( n )monotonically increase with increasing argument • Results of q( n )are non-monotonic! • Note # of calls: calls(q( 40 )) = 1,137,454,741
Q-Sequence, Coded in C #define MAX 100 // arbitrary limit; never reached!!!! int calls; // will be initialized each time int q( intarg ) { // q calls++; // track another call if ( arg <= 2 ) { return 1; // base case }else{ // now recurse! return q( arg - q( arg-1 ) ) + q( arg - q( arg-2 ) ); } // end if } // end q void main() { // main for( inti = 1; i < MAX; i++ ) { calls = 0; // initially no calls yet printf( "Q(%2d) = %3d, #calls = %10d\n", i, q(i), calls ); } // end for } // end main
Q-Sequence Results • Q( 1) = 1, #calls = 1Q( 2) = 1, #calls = 1Q( 3) = 2, #calls = 5Q( 4) = 3, #calls = 13Q( 5) = 3, #calls = 25Q( 6) = 4, #calls = 49Q( 7) = 5, #calls = 93Q( 8) = 5, #calls = 161Q( 9) = 6, #calls = 281Q(10) = 6, #calls = 481Q(11) = 6, #calls = 813 • . . . • Q(26) = 14, #calls = 1341433Q(27) = 16, #calls = 2174493Q(28) = 16, #calls = 3521137Q(29) = 16, #calls = 5700281Q(30) = 16, #calls = 9229053Q(31) = 20, #calls = 14941993Q(32) = 17, #calls = 24182797Q(33) = 17, #calls = 39137473Q(34) = 20, #calls = 63354153Q(35) = 21, #calls = 102525697Q(36) = 19, #calls = 165896537Q(37) = 20, #calls = 268460333Q(38) = 22, #calls = 434429737Q(39) = 21, #calls = 702952137Q(40) = 22, #calls = 1137454741 • . . . Will never reach Q(100) in your life time
Ackermann Definition Ackermann a( m, n ) is defined as a function of two non-negative (i.e. unsigned in C++) integers m and n Base case 1: a( 0, n ) = n + 1 Base case 2: a( m, 0 ) = a( m - 1, 1 ) Recursive definition of a( m, n ), m, n > 0 a( m, n ) = a( m - 1, a( m, n - 1 ) ) Ackermann complexity grows awfully fast; e.g. a(4,2) is an integer number with 19,729 decimal digits; greater than the national US debt!
Ackermann Definition Students, code now in C, volunteers shows result on white-board: Base case 1: a( 0, n ) = n + 1 Base case 2: a( m, 0 ) = a( m - 1, 1 ) Recursive definition of a( m, n ), m, n > 0 a( m, n ) = a( m - 1, a( m, n - 1 ) )
Ackermann Coded in C unsigned a( unsigned m, unsigned n ) { // a calls++; // global unsigned if ( 0 == m ) { // note operand order return n + 1; // first base case }else if ( 0 == n ) { // m > 0 return a( m - 1, 1 ); // other base case }else{ // m > 0, n > 0 return a( m-1, a( m, n-1 ) ); // recurse! } // end if } // end q void main() { // main for( inti = 0; i < MAX; i++ ) { printf( "\nFor m = %d\n", i ); for( int j = 0; j < MAX; j++ ) { calls = 0; printf( "a(%1d,%1d) = %10u, calls = %12u\n", i, j, a( i, j ), calls ); } // end for } // end for return 0; } // end main
Ackermann Results • For m = 0a(0,0) = 1, calls = 1. . . • For m = 1. . . • a(1,7) = 9, calls = 16For m = 2a(2,0) = 3, calls = 5a(2,1) = 5, calls = 14a(2,2) = 7, calls = 27a(2,3) = 9, calls = 44a(2,4) = 11, calls = 65a(2,5) = 13, calls = 90a(2,6) = 15, calls = 119a(2,7) = 17, calls = 152For m = 3a(3,0) = 5, calls = 15a(3,1) = 13, calls = 106a(3,2) = 29, calls = 541a(3,3) = 61, calls = 2432a(3,4) = 125, calls = 10307a(3,5) = 253, calls = 42438a(3,6) = 509, calls = 172233a(3,7) = 1021, calls = 693964For m = 4a(4,0) = 13, calls = 107 • don’t even dream about computing a(4,2) or higher!
Stack Data Structure • High-level language functions call each other in a nested fashion, recursively, even indirectly recursively • And return in strictly the reverse order (LIFO), but with any number of further calls in between • Stack is the natural data structure to track callée return information • Languages also allow local data per function call, of which formal parameters are just one variation • Natural to have locals also live and die on the run time stack, synchronized with call-return information • Possible but wasteful to have a separate stack for automatic locals: conclusion to have unified stack
Caller pushes formals bp Field 1: Function return value Field 2: return address Stack Marker Field 3: dynamic link Field 4: static link sp 0..32 registers saved Stack Frame 0 or more locals Other temps Stack Frame of callee hp Stack grows downwards Stack grows downward
Stack Data Structure • Stack is natural data structure for recursive call: • 1.) Before call: provide (push) all actual parameters • During the call, these are the formal parameters • 2.) Then execute call, provide the return address on stack • Provide space on stack for return value if needed (function) • And push bp register, pointing to the frame of caller: known as dynamic link • 3.) Before executing callée code: allocate locals on stack • And allocate temps, e.g. copies of all regs to be used, save them and later restore before return • Thus stack grows by 3 physical + logical sections: • Formal parameters, some of them just addresses for & • Stack Marker, AKA Activation Record: RA, RV, Dynamic Link • Locals + temps+ saved regs
Stack Data Structure • Stack is addressed by 2 HW resources, typically registers bp and sp (AKA top) • It is computable to have a single register address stack via current frame, since the compiler “knows” at each place, by how much stack must have grown • Actually so done by Greenhills compilers in 1990s for register-starved Intel architecture • Base Pointer register bp points to some defined place of stack marker, typically to dynamic link • The top of stack register sp points to the dynamically changing, momentary top of stack –dynamic = during the call • The bp stays invariant during the call; changes only at further calls and at any returns –static = during the call • The sp changes with each call preparation, each temp pushed on top, each intermediate result, etc.
Simulate Recursion via Iteration • Programmer to understand RT-stack and recursion! • What to do, if you implement a recursive algorithm using a language that does not support recursion? • Replace recursive by a non-recursive algorithm! • Or simulate recursion via non-recursive methods • After all, a computer chip has no notion of recursion; it is a sequential machine that “simulates recursion” via non-recursive methods; the compiler plus run-time system perform this transformation! • Done so at local industry in the past: FPS used Fortran!! to implement System SW and compilers • Here are the actual steps of simulating recursion via iteration; must use language with Goto –ask for forgiveness regarding this terrible sin
Steps of Simulating Recursion • consider directly-recursive calls of simulated function: • Define data structure struct stack_tp, to hold params, locals, etc. • Define explicit stack with top of stack (top) index, initially top=0; like a real stack identified by sp, may overflow, so include code to check; stack[ top ] holds parameters, function return value, return location (labels after a recursive call), and automatics • Define labels for each point of recursive call, more precisely at each point after the call; number these points of return, e.g. l1, l2, l3, l4 etc. There shall be branches=gotos to these points of return • At each point of recursive call: • Increment : i.e. top++, like HW recursion that grows + shrinks sp • Manually move parameters for “this call” onto stack; e.g. assign: stack[ top ].arg1 = actual1; stack[ top ].arg2 = actual2 . . • Store the place of return: stack[ top ].ret = 1, or 2, or 3 alluding to l1, l2, l3 . . . • Initialize local, automatic objects: stack[ top ].local1 = value1 . . . • Jump (Goto, the terrible sin!) to function head, not including initializing code
Steps of Simulating Recursion • 4. Point of return: In simple cases, all explicitly coded returns and the implied return at the end of the recursive function body can be re-coded into a single place; if not, the code to simulate a return is replicated: • Decrement the top of stack index: top-- • Check, to which of the stored labels the flow of control has to branch to simulate return (via goto) to continue execution; e.g.: if ( stack[ top+1 ].ret == xyz ) goto label_xyz; • And if no other branch is open, then fall through to the end • For void functions this is a literal fall-through • For true functions, the return value has to be computed before the fall-through, e.g.: return stack[ top ].return_val; // top is that of caller! • 5. For nested recursive calls or several recursive calls in a row or both: “be creative” ; see an example later; apply these steps with meticulous precision
Simulate Recursion, fact() #include <stdio.h> #define MAX_STACK 100 // never reached or exceeded! #define MAX 14 // higher factorial overflows 32bits unsigned calls = 0; // track # of calls typedefstructs_tp { unsigned arg; // for formal parameters unsigned fact; // function return value unsigned ret; // code address after call, return! } struct_s_tp; // first the recursive fact() function for reference // includes tracking # of calls unsigned fact( unsigned arg ) { // fact calls++; // gotta be global if ( 0 == arg ) { // why strange order? return 1; }else{ return fact( arg - 1 ) * arg; } // end if // there should be an assertion here! } // end fact
Simulate Recursion, fact() • unsigned nrfact( unsigned arg ) • { // nrfact • struct_s_tp s[ MAX_STACK ]; // simulate RT stack! • unsigned top = 0; // simulate sp register • s[ top ].arg = arg; // this call’s argument • s[ top ].ret = 3; // 3 alludes to label l3 • l1: if ( 0 == s[ top ].arg ) { • s[ top ].fact = 1; • }else{ • top++; // recursion! • s[ top ].arg = s[ top-1 ].arg-1; • s[ top ].ret = 2; // remember label l2 • goto l1; // now simulate recursion • l2: • // back from recursive call • top--; // sp-- • s[ top ].fact = s[ top + 1 ].fact * s[ top ].arg; • } // end if • if ( s[ top ].ret == 2 ) { // test, where to branch to • goto l2; // unstructured goto into if • } // end if • l3: • return s[ top ].fact; • } // end nrfact
Simulate Recursion, fact() Result • r_fact( 0) = 1, calls = 1 r_fact( 1) = 1, calls = 2 r_fact( 2) = 2, calls = 3 r_fact( 3) = 6, calls = 4 r_fact( 4) = 24, calls = 5 r_fact( 5) = 120, calls = 6 r_fact( 6) = 720, calls = 7 r_fact( 7) = 5040, calls = 8 r_fact( 8) = 40320, calls = 9 r_fact( 9) = 362880, calls = 10 r_fact(10) = 3628800, calls = 11 r_fact(11) = 39916800, calls = 12 r_fact(12) = 479001600, calls = 13 r_fact(13) = 1932053504, calls = 14nr_fact( 0) = 1nr_fact( 1) = 1nr_fact( 2) = 2nr_fact( 3) = 6nr_fact( 4) = 24nr_fact( 5) = 120nr_fact( 6) = 720nr_fact( 7) = 5040nr_fact( 8) = 40320nr_fact( 9) = 362880nr_fact(10) = 3628800nr_fact(11) = 39916800nr_fact(12) = 479001600nr_fact(13) = 1932053504
Simulate Recursion, fibo() • #define MAX_STACK 100 // never to be reached or exceeded! • #define MAX 30 // higher fibo(n) not computable! • unsigned calls; // in case we track # of calls • typedef struct s_tp { // type of stack • unsigned arg; // copy of fibo’s arg • unsigned fibo; // return value for fibo • unsigned ret; // to which label to goto? • } struct_s_tp; • // recursive function for reference: • unsigned fibo( unsigned arg ) • { // fibo • calls++; • if ( arg <= 1 ) { // base case? • return arg; // if so: done! • }else{ • return fibo( arg-1 ) + fibo( arg-2 ); • } // end if • } // end fibo
Simulate Recursion, fibo() unsigned nr_fibo( unsigned arg ) { //nr_fibo struct_s_tp s[ MAX_STACK ]; // stack can be local unsigned top = 0; // initially s[ top ].arg = arg; // copy arg to stack s[ top ].ret = 4; // if all fails, return l1: if ( s[ top ].arg <= 1 ) { s[ top ].fibo = s[ top ].arg; }else{ top++; // ready to recurse s[ top ].arg = s[ top - 1 ].arg - 1; s[ top ].ret = 2; // to place of 1. return goto l1; // recurse! l2: top++; // ready to recurse again s[ top ].arg = s[ top - 2 ].arg - 2; s[ top ].ret = 3; // to place of 2nd return goto l1; // recurse! l3: // two returns simulated top -= 2; // simulate 2 returns s[ top ].fibo = s[ top + 1 ].fibo + s[ top + 2 ].fibo; } // end if if ( 2 == s[ top ].ret ) { // second recursive call goto l2; }else if ( 3 == s[ top ].ret ) { gotol3; } // end if l4: return s[ top ].fibo; // all done } // end nr_fibo
Simulate Recursion, fibo() Result • r_fibo( 0) = 0, calls = 1 r_fibo( 1) = 1, calls = 1 r_fibo( 2) = 1, calls = 3 • r_fibo( 3) = 2, calls = 5 r_fibo( 4) = 3, calls = 9 . . . • r_fibo(22) = 17711, calls = 57313 r_fibo(23) = 28657, calls = 92735 r_fibo(24) = 46368, calls = 150049 r_fibo(25) = 75025, calls = 242785 r_fibo(26) = 121393, calls = 392835 r_fibo(27) = 196418, calls = 635621 r_fibo(28) = 317811, calls = 1028457 r_fibo(29) = 514229, calls = 1664079nr_fibo( 0) = 0nr_fibo( 1) = 1nr_fibo( 2) = 1nr_fibo( 3) = 2nr_fibo( 4) = 3 . . . • nr_fibo(22) = 17711nr_fibo(23) = 28657nr_fibo(24) = 46368nr_fibo(25) = 75025nr_fibo(26) = 121393nr_fibo(27) = 196418nr_fibo(28) = 317811nr_fibo(29) = 514229
Simulating Return of fibo() • Must the computation of the continuation place be after the if-statement? Or can we relocate it into the Else-Clause? • That would lead to a partial simulation, in which only the case arg > 1 continues correctly • Yet even cases for arg <= 1 must compute the right continuation via (unstructured) brute-force gotos: if ( 2 == s[ top ].ret ) { // second recursive call goto l2; }else if ( 3 == s[ top ].ret ) { goto l3; } // end if
Towers of hanoi() • The game of the “Towers of Hanoi” is a game to move a stack of n discs, while obeying certain rules • All n discs are of different sizes, residing on top of one another, a smaller disc always over a larger • The goal is to move the whole tower from start, to the goal position, using one additional buffer location • But only moving 1 single disc at a time • And never placing a larger disc on top of a smaller • During various times, any disc may be placed on the start position, the goal, or the buffer
Towers of hanoi(), Recursives • #include <iostream.h> • #define MAX … some small integer < 32 • void hanoi( int discs, char* start, char* goal, char* buff ) • { // hanoi • if ( discs > 0 ){ • hanoi( discs-1, start, buff, goal ); • cout << "move disc " << discs << " from " << start << " to “ • << goal << endl; • hanoi( discs-1, buff, goal, start ); • } // end if • } // end hanoi • int main() • { // main • for ( int discs = 1; discs <= MAX; discs++ ) { • cout << ” hanoi for " << discs << " discs" << endl; • hanoi( discs, "start", "goal ", "buff " ); • cout << endl; • } // end for • return 0; • } // end main
Towers of hanoi(), Recursive move disc 1 from start to goal < For 1 disc move disc 1 from start to buff < For 2 discs move disc 2 from start to goal move disc 1 from buff to goal move disc 1 from start to goal < For 3 discs move disc 2 from start to buff move disc 1 from goal to buff move disc 3 from start to goal move disc 1 from buff to start move disc 2 from buff to goal move disc 1 from start to goal move disc 1 from start to buff < For 4 discs move disc 2 from start to goal move disc 1 from buff to goal move disc 3 from start to buff move disc 1 from goal to start move disc 2 from goal to buff move disc 1 from start to buff move disc 4 from start to goal move disc 1 from buff to goal move disc 2 from buff to start move disc 1 from goal to start move disc 3 from buff to goal move disc 1 from start to buff move disc 2 from start to goal move disc 1 from buff to goal
Simulate Recursion, hanoi() • void nr_hanoi( unsigned discs, char* start, char* goal, char* buff ) • { // nr_hanoi • struct_h_type s[ MAX_STACK ]; unsigned top = 0; • s[ top ].discs = discs; • s[ top ].start = start; • s[ top ].buff = buff; • s[ top ].goal = goal; • s[ top ].ret = 4; • l1: if ( s[ top ].discs > 0 ) { • top++; • s[ top ].discs = s[ top-1 ].discs - 1; • s[ top ].start = s[ top-1 ].start; • s[ top ].buff = s[ top-1 ].goal; • s[ top ].goal = s[ top-1 ].buff; • s[ top ].ret = 2; • goto l1; • l2: cout << "nr move disc “ << s[ top ].discs << “ from “ • << s[ top ].start << “ to “ << s[ top ].goal << endl; • top++; • s[ top ].discs = s[ top-1 ].discs - 1; • s[ top ].start = s[ top-1 ].buff; • s[ top ].buff = s[ top-1 ].start; • s[ top ].goal = s[ top-1 ].goal; • s[ top ].ret = 3; • goto l1; • } // end if • l3: if ( 2 == s[ top ].ret ) { • top--; • goto l2; • }else if ( 3 == s[ top ].ret ) { • top--; • goto l3; • } // end if • } // end nr_hanoi
References • Douglas R. Hofstadter, “Gödel, Escher, Bach: an eternal golden braid”, Basic Books, 1999, ISBN 0465026567; came out in 1979 • Ackermann function at NIST: http://xlinux.nist.gov/dads/HTML/ackermann.html • Herbert G. Mayer: “Advanced C Programming on the IBM PC”, 1989, Windcrest, ISBN 0-8306-9163-4 • Non-recursive solution to Towers of Hanoi: http://portal.acm.org/citation.cfm?id=948602 • Herbert G. Mayer, Don Perkins, SIGLAN Notices, 1984, non-recursive Towers of Hanoi Solution: http://dl.acm.org/citation.cfm?id=948573