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 lefttoright or righttoleft 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 illformed.
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
illformed.)
4. Example: infix 1+(2+3)*(45) => 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
machinelanguage 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 runtime 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 wellformed:
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)*(45):
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. Errorchecking 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