Stacks & Recursion
LIFO list - only top element is visible Stack top push pop LIFO list - only top element is visible
Defining the ADT "Stack" Data: Basic Operations: a linear collection of data items in which all operations occur at one end, called the top Basic Operations: construct a stack (usually starts empty) find out if stack is empty push: add an item on top of the stack accessing the topmost item Top: retrieve the top item of the stack Pop: remove the top item of the stack
Some Implementation choices fixed-size array capacity (max # elements) decided at compile-time could be too small for expected amount of data could be too large, so space is wasted size (current # elements used) fast dynamic array capacity decided at run-time size may be less than or equal to the capacity uses pointers linked list size changes as needed during program execution
Implementation example typedef Complx StackElement; const int CAPACITY = 8; int myTop; StackElement myStack[CAPACITY]; Complx X; push(&myStack,X); -1 myTop myArray 7 6 5 4 3 2 1 initially empty (myTop is negative)
Using the stack - 1 Model with an array Let position 0 be top of stack Problem … consider pushing and popping Requires much shifting lab 1: build a stack, push, top - display (reverses input), pop Nyhoff, ADTs, Data Structures and Problem Solving with C++, Second Edition, © 2005 Pearson Education, Inc. All rights reserved. 0-13-140909-3
Using the stack - 2 A better approach is to let position 0 be the bottom of the stack Thus our design will include An array to hold the stack elements An integer to indicate the top of the stack Nyhoff, ADTs, Data Structures and Problem Solving with C++, Second Edition, © 2005 Pearson Education, Inc. All rights reserved. 0-13-140909-3
Addressing the problem Too much or too little space Whatever size you pick, some day, there will be too much data In embedded systems, RAM is a premium Need a dynamic solution (later!) Size changes as needed No waste Slower when adding new elements
The "node" concept a "node" is one element of a stack more than just the data index of next and/or previous node trivial for array implementations each node is a struct data may be an item INSIDE the node struct struct node {int next; Complx data;}; node myStackNode[CAPACITY];
An alternative to loops Recursion An alternative to loops
What is recursion? a function calls itself a function calls its caller direct recursion a function calls its caller indirect recursion f1 f f2
Recursion-1 Inefficient if duplicate values exist Alternative to iteration (looping) often more "elegant" and concise than a loop sometimes very inefficient easier to program than loops Useful when a problem can be defined in terms of similar sub-problems eventually reach a "known" answer (base case) Inefficient if duplicate values exist
Head vs. Tail Recursion head recursion tail recursion requires "deepest" call to complete before any values are known current stack state must be preserved tail recursion compiler can "collapse" the stack
Head vs. Tail recursion Note: base case is ALWAYS 1st head(3) is: 2 3 tail(3) is: 3 2 1 void head(int n) { if(n == 1) return; else head(n-1); // printf("head - n=%i\n",n);); } void tail(int n) { if(n == 0) return; else printf("tail - n=%i\n",n); tail(n-1); // }
Caveats Static data is NOT on the "stack" Must have an "exit" never gets re-allocated same values for EVERY call Must have an "exit" prevent an infinite loop
Outline of a Recursive Function if (answer is known) provide the answer & exit else call same function with a smaller version of the same problem base case recursive case
Factorial (n) - looping fact (int n) {if (n<0) exit(1); answer=1; for (i=1; i<=n; i++) answer=answer*i; // loop n times return (answer); } This is a simple problem
Factorial (n) – head recursive definition: Factorial (n) = n * Factorial (n-1) int Fact (int n) {if (n<0) exit(1); if (n == 0 | n==1) return 1; return n * Fact (n-1); // stack must be saved // cannot do the *'s until last value is known }
Factorial "n" (tail recursive) tail_fact (n, sofar) // init "sofar" same as "n" { if (n == 0 | n==1) return sofar; else // nothing to save on the stack // because it is in the 2nd parameter /* that is, the recursion may not HAVE to be done, if we have reached the base case */ return tail_fact (--n, sofar * n); } // here's how to run it printf ("5!=%i",tail_fact(5,5));
Keeping track compiler builds code to: create a new stack pointer and stack space put local variables & parameters on stack each "return" returns control to caller's next instruction (the inst after the call) returned value, is in caller's stack space same as ANY function this is called "unwinding"
Recursive Call Tree int Fact (int n) { if (n == 0 | n==1) return 1; 24 6 2 int Fact (int n) { if (n == 0 | n==1) return 1; return n * Fact (n-1); }
Fibonacci Series Fibonacci sequence: 1, 1, 2, 3, 5, 8, 13, 21, …. 1 for n <= 2 fib(n) = fib(n-2) + fib(n-1) for n>2 for n=4: fib(2)+fib(3) 1 + 2 3
Tracing fib(6) 5 6 4 4 3 3 2 This is an animated slide!! 3 2 2 1 2 1 2 1 1 + 1 1
Ackermann's function A(0, n) = n + 1 A(m, 1) = A(m+1, 0) A(m+1, n+1) = A(m, A(m+1, n)) This function build a VERY deep stack very quickly
What Does a Compiler Do? Lexical analysis Parsing Generate object code divide a stream of characters into a stream of tokens total = cost + 0.08 * cost; if ( ( cond1 && ! cond2 ) ) Parsing do the tokens form a valid program, i.e. does it follow the syntax rules? Generate object code
BNF (Backus-Naur form) (also called Backus Normal Form) a language used to define the syntax rules of a programming language consists of productions – rules for forming some construct of the language meta-symbols – symbols of BNF that are NOT part of the language being compiled terminals – appear as shown non-terminals – syntax defined by another production
BNF Syntax Rules for a simplified boolean expression bexpr -> bterm || bexpr | bterm bterm -> bfactor && bterm | bfactor bfactor -> !bfactor | (bexpr) | true | false | ident ident -> alpha { alpha | digit|_} alpha -> a .. z | A .. Z digit -> 0 .. 9 meta-symbols are in red terminals are in blue non-terminals are in black special rules needed for "|" as part of a language
bexpr -> bterm || bterm | bterm What sequence of tokens is a valid bexpr? if (there is a valid bterm) if (nextToken is ||) if (there is a valid bterm) return true else return false else return true else return false Note: tokenizer must watch for the "||" without stopping at the first "|"