CPS222 Lecture: Stacks                                  - last revised 1/15/13

Materials:

1. Projectable version of definition of a stack as an ADT
2. reverse.cc and executable
3. palindrome.cc and executable
4. Projectable version of array implementation of stacks
5. Projectable version of linked list implementation of stacks

Objectives:

1. To define the concept of a stack and its operations: push, pop, top, empty
2. To show some way stacks can be used
3. To show how to create stacks using STL
4. To show two ways of implementing stacks directly
5. To show how to use stacks for Infix - postfix conversion and evaluation of 
   postfix expressions

I. Introduction to Stacks
-  ------------ -- ------

   A. One type of sequence useful in many problems is the stack.

   B. A stack is a dynamic list of items which grows and shrinks at one end,
      called the top.  Much of the nomenclature comes from an analogy to a
      stack of cafeteria trays.

      1. The operation create creates an empty stack.
      2. The operation empty reports whether there is anything on the stack.
      3. The operation push puts a new item at the top of the stack, "pushing
         down" all the others.
      4. The operation pop removes the top item from the stack, "popping up"
         those below it.
      5. The operation top reports what is on top without changing it.

   C. We can define the ADT stack formally as follows:

      1. Set of values: Sequences of objects of some type (we can have stacks
         of integers, or stacks of characters, or stacks of records, just as we
         can have arrays or files of these types.)  We assume that, for a given
         stack, all the objects are of the same type.

      2. Operations:                                    (PROJECT)

         a. CREATE returns stack

                     Preconditions - None
                     Postcondition - Stack is empty

         b. EMPTY (stack) returns boolean

                     Preconditions - None
                     Postconditions - The result is true iff the stack is empty.

         c. PUSH (item, stack) modifies the stack

                     Preconditions - None
                     Postcondition - Item is added to the stack

         d. POP (stack) modifies the stack

                     Precondition - Stack is not empty
                     Postcondition - Top item is removed from the stack

         e. TOP (stack) returns item

                     Precondition - Stack is not empty
                     Postconditions - The top item on the stack is returned,
                                      but the stack is not altered.

         f. Note: POP and TOP are often combined into a single POP operation 
            that combines both operations, as in the text.  In this case, we 
            have an operation I'll call TOPnPOP:

            TOPnPOP (stack) returns item and modifies the stack

                     Precondition - Stack is not empty
                     Postconditions - The top item of the stack is removed
                                      from the stack and returned.
                                      
         g. It is sometimes useful to also define an operation SIZE which
            gives the number of items on the stack

   D. The fundamental behavior of a stack is LIFO: last in first out.   
      
      1. Example: consider the following series of operations:

                        CREATE
                        PUSH A
                        PUSH B
                        PUSH C
                        POP
                        PUSH D
                        POP
                        What is the current value of TOP?

         We can visualize the stack after each operation as follows:

                CREATE          (Empty)

                PUSH A:         A
                PUSH B:         B
                                A
                PUSH C:         C
                                B
                                A
                POP:            B
                                A
                PUSH D:         D
                                B
                                A
                POP:            B
                                A
                So the current value of top is B

      2. This behavior is realized if we define our operations by the
         following axioms (which can be shown to be a necessary and sufficient
         set for "stack" behavior)

         Let S be any stack and I be any item.  Then:

         a. EMPTY(CREATE) ::= True
         b. EMPTY(PUSH(I,S)) ::= False
         c. TOP(CREATE) ::= Error
         d. TOP(PUSH(I,S)) ::= I
         e. POP(CREATE) ::= error
         f. POP(PUSH(I,S)) ::= S
         g. SIZE(CREATE) ::= 0
         h. SIZE(PUSH(I,S)) ::= SIZE(S) + 1;
         i. SIZE(POP(S)) ::= SIZE(S) - 1;

         For the example we just did, we have

         TOP(POP(PUSH('D',POP(PUSH('C',PUSH('B',PUSH('A',CREATE)))))))

         Using our axioms, we can simplify the inner portion 
         POP(PUSH('C',PUSH('B',PUSH('A',CREATE)))) to PUSH('B',PUSH('A',CREATE))
         Thus, our original expression is equivalent to:

         TOP(POP(PUSH('D',PUSH('B',PUSH('A',CREATE)))))

         A similar transformation can be applied to yield:

         TOP(PUSH('B',PUSH('A',CREATE))))))))

         But, by axiom d, this is 'B'

   E. Stacks are a natural representation for tasks to be performed in the
      reverse of the order in which they are encountered.  Examples:

      1. Managing function calls and returns.  Suppose we have this situation:

        void a()
        {  ... }

        void b()
        {   ...
            a();
            ...
        }

        void c()
        {   ...
            b();
            ...
        }

        int main(int argc, char * argv[])
        {   ...
            c();
            ...
        }

        The functions are called in this order:

        main calls c
        c calls b
        b calls a

        But they are completed in reverse order:

        a finishes first
        then b finishes
        then c finishes
        then main finishes

        We can keep track of this by using a stack.  When we call another
        function we PUSH the address of the next executable instruction
        on the stack.  When a procedure returns, it POPs an address from the
        stack and transfers control to that location.  Thus, when we are in
        the midst of b, the stack would look like this:

                b     <- top
                c
                main

        On most computers (including the Intel chips used in various brands of
        PC), the hardware subroutine call and return instructions do just this.

        Notice that this use of the stack arises directly from its LIFO 
        property - the place we return to is the one we were last at when the 
        subroutine was called.

        In the case of recursive functions, stacks have a further use - we
        must keep the parameters and local variables of the function on
        the stack as well, since each invocation of the function has its
        own set of these.  (Languages like C++ and Java that support
        recursion handle this automatically.)
        
      2. Another use of stacks, arising from their LIFO property, is to support
         the "undo" operation in various software.  
         
         a. We use the Command design pattern, in which each operation (e.g. 
            inserting a character or deleting a chunk of text in a word 
            processor) is represented by a Command object that stores all the 
            information needed to do or undo the command.
            
         b. A Command object supports two operations: do() [ perhaps called
            something else since do is a reserved word in languages like C++)
            and undo.
            
         c. The software maintains a stack of Command objects representing
            operations that have been done (which might be undone), plus another 
            stack representing operations that have been undone (which might be 
            redone.  In each case, the the top is the most recently done/undone 
            operation.
            
            i. An operation is undone by popping the top operation on the
               undoable stack, invoking its undo method, and then pushing it
               on the redoable stack.
               
           ii. An operation is redone by popping the top operation on the
               redoable stack, invoking its do method, and then pushing it
               on the undoable stack.

      3. Another use of stacks arising from their LIFO property is  
         reversing the order of items in a list.

         Example: a program to read in a line of text and print it out in 
         reverse order:

         SHOW, DEMO reverse.cc

      4. Stacks are also useful for determining whether a string is a
         palindrome (something that spells the same both forward and
         backward): 
      
         SHOW, DEMO palindrome.cc

   F. The C++ standard template library includes a stack template (which we
      have used in the previous two examples.)

      1. #include <stack>

      2. Declare stack variables by:    stack < type > variable;

      3. Or declare a stack type by:    typedef stack < type > typename;

      4. The following operations, among others, are defined by the STL
         stack template - where type is the item type specified when the
         template is instantiated:

         a. Constructor

         b. bool empty()

         c. void push(type)

         d. void pop()

         e. type top()

II. Implementing the Stack ADT Directly
--  ------------ --- ----- --- --------

   A. Although the STL provides a stack template that can be used in most
      places where we need a stack, it is useful to know something about
      how to implement one if necessary.

   B. To implement any ADT, we devise a representation for it using the
      primative types of the programming language, or other, simpler ADT's.
      Then we define each of the operations on the ADT by means of an
      appropriate function.  In the case of a stack, we will define a stack
      class with a constructor and methods empty(), push(), pop(), and top().

      1. It turns out that there are two good ways to approach this.

      2. One approach stores the stack in an array, with a separate variable
         indicating which position in the array holds the top element.  

         a. This index might be initialized to -1 indicate that there is
            no top element because the stack is empty

         b. This index is increased when we push an item.

         c. It is decreased when we pop an item.

         d. The top operation accesses the item at the position specified
            by this index.

         EXAMPLE: Stack holding the letters 'A', 'B', 'C' - C on top:

         theArray: A B C ? ? ? ? ? ?		topOfStack: 2

         PROJECT CODE

      3. The other approach stores the stack in a linked list, with an external
         pointer pointing to the node holding the top item.
         
         (Note: the approach I am developing here includes the list
          implementation directly, rather than embedding a list in a wrapper
          that handles only the actual stack operations as in the book)

         a. This pointer is initalized to NULL

         b. When we push an item, a new node is created and this external
            pointer is made to point to it.

         c. Each node holds a pointer to the node that WAS the top item
            when it was pushed.  (The link of the node representing the last 
            (bottom) item is NULL.

         d. To pop an item, we reset the external pointer to the node just
            after the top item.

         EXAMPLE: Stack holding the letters 'A', 'B', 'C' - C on top:

                -----   -----   -----
                | C |   | B |   | A |
                | o-|-->| o-|-->| o-|-+
                -----   -----   ----- |
                                     ---
                                      -
         PROJECT CODE

      4. Both approaches have O(1) completity for all operations except the
         destructor for the list approach (which is O(n)).  However, each
         has limitations:

         a. The array approach requires that we know ahead of time how big
            the stack might get.  If we underestimate this, we can have an
            attempt to push an item fail.  If we overestimate, we waste
            storage.
        
         b. The linked list implementation does not require a priori knowledge
            of the size of the stack.  However, the overhead of dynamic
            storage allocation/deallocation makes the operations slightly
            slower.  (Basic operations with both representations are O(1),
            but the constant of proportionality is higher for linked.

III. Processing Arithmetic Expressions
---- ---------- ---------- -----------

   A. A very important use of stacks is in the translation and execution of
      arithmetic expressions.  This is normally illustrated with arithmetic
      expressions such as the following:

        a + b * c / (d + e)

      but the same approach applies to other expressions - e.g. a sql query:
        
        select *
            from employees
            where title = 'supervisor' 
                  or salary > 50000 and hired < '01/01/90';

   B. Arithmetic (and other) expressions can be written using three
      different notation systems:

      1. Infix - the system we normally use.  Operators are written in between
         their operands: a + b.  While this system is familiar to us, it has
         a couple of key limitations:

         a. When an operand appears between two operators, it is not
            immediately clear which operator is done first - e.g.

                a + b * c

            This problem is handled in practice by some combination of the
            following:

            - A left-to-right or right-to-left rule
            - A table of operator precedence (e.g. * is usually done before +)
            - Parentheses

            Unfortunately, these rules are not always consistent from one
            programming language to another; and machine evaluation of such 
            expressions is cumbersome since look ahead is needed before deciding
            whether a given operator can be applied now.

         b. Infix notation can only use operators that have one or two
            operands - for three or more, an alternate notations such as
            functional notation (actually a form of prefix) must be used.

      2. Prefix or Polish notation - invented by Lukasiewicz.  An operator
         immediately preceeds its operands.  Ex:

         infix          prefix

         a+b            +ab
         a+b*c          +a*bc

         Note: precedence is never an issue, parentheses are never needed,
         any number of operands can be used for a given operator.

         Trivia question: where have you been using prefix notation for years?

         Answer: in functions like sin(x).  Sin is the operator; x its argument.

      3. Postfix or Reverse Polish notation (RPN):

         infix          postfix

         a+b            ab+
         a+b*c          abc*+

         Again: no precedence problems; an operator can have any number of
         operands.

      4. The latter is especially suited to machine evaluation of expressions.

   C. An expression written in postfix can be easily evaluated by using a
      stack, according to the following rules:

      1. When an operand is encountered in the postfix, push it on the stack.

      2. When an operator is encountered, pop the required number of operands,
         apply the operator, then push the result.  If there are not enough
         items on the stack to supply the operands needed, then the expression
         is ill-formed.

      3. At the end of the scan, the stack should contain a single value, which
         is the result of the expression.  (If not, then the expression is
         ill-formed.)

      4. Example: infix 1+(2+3)*(4-5)  => 1 2 3 + 4 5 - * +

        char scanned            Resultant stack

        1                       1
        2                       1 2
        3                       1 2 3
        +                       1 5
        4                       1 5 4
        5                       1 5 4 5
        -                       1 5 -1          Note: second operand popped 1st
        *                       1 -5
        +                       -4

   D. An expression in postfix can easily be converted by a compiler into
      machine-language code for evaluation.

      1. On a stack architecture machine the task is especially easy.  A
         stack machine has operations like the following:

        PUSH    operand
        POP     operand
        ADD     ; no operands - uses stack
        SUB     ; ditto
        MUL     ; ditto
        DIV     ; ditto

        The above expression translates into the following stack code:

        PUSH    1
        PUSH    2
        PUSH    3
        ADD
        PUSH    4
        PUSH    5
        SUB
        MUL
        POP     result          - corresponds to :=

      NOTE: This is actually the approach used by the Java compiler.  The
            Java Virtual machine uses a run-time stack, with primitive
            operations for pushing, popping, and doing arithmetic

   E. Converting an infix expression to postfix is obviously a necessary
      prelude to any of the above.  This task is a bit more complex, but
      is not terribly hard.  It also uses a stack - this time a stack of
      OPERATORS rather than operands.  Note, then, that for direct
      interpretation of an infix expression two stacks are used: an operator
      stack to convert from infix to RPN, and an operand stack to evaluate
      the RPN.

      1. We assign to each operator a precedence value.  For example, the
         following would work for a subset of the arithmetic operators of
         C/C++:
                operator        precedence

                +               1
                -               1
                *               2
                /               2
                %               2

         (For the above, we require that operators of equal precedence are 
          evaluated left to right).

         NOTE: The actual list of operators for C/C++ has 18 levels of
               precedence!

      2. One special issue we must deal with is parentheses.

         a. Note that infix is the only one of the three notations that needs
            to use parentheses - e.g.

            The infix expressions (1 + 2) * 3 and 1 + (2 * 3) are clearly
            different, and the parentheses are needed in at least one case to
            specify the intended interpretation.   The equivalent prefix and
            postfix forms are:

            Prefix:             * + 1 2 3               + 1 * 2 3
            Postfix:            1 2 + 3 *               1 2 3 * +

         b. Thus, our algorithm will have to handle parentheses in the incoming
            infix expression, but will never output parentheses to the outgoing
            postfix expression.

         c. We will treat parentheses as a special kind of operator.  In
            particular, the '(' will be given precedence value 0.  (The ')'
            doesn't actually need a precedence.)

      3. Our algorithm is as follows, assuming the expression is well-formed:

         for each character in the input do
           switch character scanned
              case operand:
                output it immediately to the postfix
              case operator:
                while stack is not empty and 
                      precedence (top operator on the stack) >= 
                        precedence (character scanned) do
                   pop top operator from the stack and output it to postfix
                push character scanned
              case '(':
                push character scanned
              case ')':
                while top of stack is not a '(' do
                   pop top operator from the stack and output it to postfix
                pop the '(' from the stack and discard it
                
         at end of input
           while stack is not empty do
                pop top character from the stack and output it to postfix

        Example: 1+(2+3)*(4-5):

        Input char      Stack                           Postfix

        1                                               1
        +               +                               1
        (               +(                              1
        2               +(                              1 2
        +               +(+                             1 2
        3               +(+                             1 2 3 
        )               +(                              1 2 3 +
                        +
        *               +*                              1 2 3 +
        (               +*(                             1 2 3 +
        4               +*(                             1 2 3 + 4 
        -               +*(-                            1 2 3 + 4 
        5               +*(-                            1 2 3 + 4 5 
        )               +*(                             1 2 3 + 4 5 -
                        +*                              1 2 3 + 4 5 -
        eol             +                               1 2 3 + 4 5 - *
                                                        1 2 3 + 4 5 - * +

      4. Error-checking may be added as follows:

         i. Use a variable expected of type enum { OPERAND, OPERATOR },
            initially set to OPERAND.

        ii. Use the following decision table:

        Current value of        Input           Additional action taken
        expected                                (in addition to basic algorithm)
        
        OPERAND                 operand         expected = OPERATOR
        OPERAND                 +, -, *, /      error - operand expected
        OPERAND                 (               (none)
        OPERAND                 )               error - operand expected
        OPERATOR                operand         error - operator expected
        OPERATOR                +, -, *, /      expected = OPERAND
        OPERATOR                (               error - operator expected
        OPERATOR                )               (none)
        (either value)          Any character   error - invalid character
                                not listed
                                above

       iii. In addition, the following must be handled:

          - When a ')' is seen, if the operator stack becomes empty before a 
            '(' is found then there is an error - '(' expected.

          - If the operator stack contains any '(' when the end of input is 
            seen (i.e. a '(' is popped from the stack during the final loop),
            then there is an error - ')' expected.

          - When end of input is reached, if expecting is not OPERATOR there
            is an error - operand expected