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 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