You're waiting in line to eat at the cafeteria. As you draw nearer to the service counter, you pass by a stack of trays. Following the lead of those ahead of you in line, you grab the tray on top.
After obtaining your meal, you happen to sit at a table not far from the waiting line. You notice that a cafeteria employee ---who has emerged from the kitchen walking alongside a battery-powered cart of freshly-washed trays--- is about to replenish the stack of trays, which by now has become quite low. The employee, who is old and frail, can lift only one tray at a time. She places the freshly-washed trays onto the top of the stack, one by one.
From these experiences, you make the following observations: When a tray is removed from a stack, it is the one on top. When a tray is placed onto a stack, it is placed on top. Thinking somewhat more deeply, you arrive at the following conclusion: Among all the trays on a stack at a given moment, the one among them that will be removed first is the one that was placed onto the stack last. (This is not quite as trivial as it may sound, because any number of insertions/removals may occur in the meantime.) That is, a stack of trays obeys a LIFO ("last in, first out") insertion/deletion pattern.
The concept of a stack in computer science is analogous to a stack of trays in a cafeteria. A stack is simply a collection of items such that arrivals and departures conform to a LIFO pattern. Another way to look at it is this: A stack is a sequence of items such that all insertions and deletions occur at the same end. Keeping the cafeteria trays in mind, we call this end the "top" of the stack. Commonly, it is also taken as part of the definition that, among the items currently on a stack, the only one that can be observed is the one at the top. (Imagine that each cafeteria tray has a serial number etched into it, such that it is visible only if no other tray is sitting on top of it.)
For historical reasons, the insertion operation on stacks is usually called push and the remove/delete operation is usually called pop. Modeling this stack concept as a generic Java interface (where the generic parameter indicates the data type of the items allowed to be inserted into the stack), we get the following:
public interface Stack<T> {
/* <<<<< o b s e r v e r s >>>>> */
/* pre: none
post: returns true if the stack is empty, false otherwise
*/
public boolean isEmpty();
/* pre: !this.isEmpty()
post: returns item at top of the stack
*/
public T topOf();
/* <<<<< m u t a t o r s >>>>> */
/* pre: let s == this
post: this.topOf() == item & s == this.pop()
*/
public void push( T item );
/* pre: !this.isEmpty() & let this == s
post: this.push(s.topOf()) == s
*/
public void pop();
}
You may find it surprising to learn that, despite the simplicity of the concept, stacks are quite useful in practice.
A stack is used for managing the flow of execution every time you run a program. You are aware of the fact that, when a method is called, the caller's execution is suspended while the method executes. When the called method terminates, execution resumes within the caller at the command immediately following the one making the call. (The address of this command is called the return address.) This sounds fairly simple, but what happens when a method, having been called, calls another one (or even itself), which calls another one, which calls yet another, etc., etc.? Some systematic method for keeping track of return addresses is needed in order to ensure that, as each method terminates, execution resumes at the appropriate place. It turns out that the information necessary for keeping track of all this can be stored (and accessed) in a quite orderly manner: by using a stack that holds the return addresses.
As an example, suppose that we have a program
Method A: Method B: Method C: Method D:
-------- -------- -------- --------
... Call C; ... ...
... B1:... ... ...
Call B; ... Call D; End;
A1:... Call C; C1:...
... B2:End; End;
Call C;
A2:...
End;
The labels A1, A2, B1, and C1
indicate the addresses of the commands at which execution should resume
after the method called on the preceding line finishes. Each time a
call occurs, the corresponding return address is pushed onto the run-time
stack. Each time a method terminates execution, the return address at the
top of the run-time stack is popped and execution resumes at the location
that it indicates. During execution of the example program above, the
run-time stack will take on the following configurations (with elements
written from bottom to top):
contents last change ------------------------------------------------- 1. A1 A calls B; return address is pushed 2. A1 B1 B calls C; return address is pushed 3. A1 B1 C1 C calls D; return address is pushed 4. A1 B1 D terminates; C1 is popped; execution resumes there 5. A1 C terminates; B1 is popped; execution resumes there 6. A1 B2 B calls C (again); return address is pushed 7. A1 B2 C1 C calls D; return address is pushed 8. A1 B2 D terminates; C1 is popped; execution resumes there 9. A1 C terminates; B2 is popped; execution resumes there 10. [empty] B terminates; A1 is popped; execution resumes there 11. A2 A calls C; return address is pushed 12. A2 C1 C calls D; return address is pushed 13. A2 D terminates; C1 is popped; execution resumes there 14. [empty] C terminates; A2 is popped; execution resumes there
Another application of stacks is in the evaluation of arithmetic expressions. In order to keep things simple, here we will focus upon so-called fully-parenthesized arithmetic expressions, which we will abbreviate as "FPAE". By "fully-parenthesized", we mean that the expression contains a mated pair of parentheses for every occurrence of an operator symbol. That is, the left operand of each operator is immediately preceded by ( and the right operand is immediately followed by ) . An example of such an expression, annotated to show the connections between parentheses and operators, is
(((15 - 1) + 2) + (3 * ((6 + 0) / (1 + 2))))
|||___|__| | | | | | ||__|__| | |__|__||||
||_________|__| | | | |________|________|||
| | |__|____________________||
|_______________|__________________________|
We can define FPAE's (recursively) as follows:
An FPAE is either
Using a context-free grammar, we can state the definition like this:
<FPAE> ---> <numeric literal> <FPAE> ---> ( <FPAE> <operator> <FPAE> ) <operator> ---> + | - | * | /
Notes: (1) The boldfaced vertical bars are used for separating
alternatives. Hence, the third line of the grammar says that an
operator is either a plus sign, or a minus sign, or etc., etc.
(2) This definition allows only binary operators, and thus
excludes unary + and - (as in -4).
We could repair this, but, to keep matters as simple as possible, we won't.
End of notes.
How does one evaluate such an expression? Most likely, you would find an immediately evaluable non-atomic subexpression (i.e., one of the form (A op B), where A and B are numeric literals), you would evaluate it, and then you would replace it by the corresponding numeric literal. You would repeat this until the original expression had been reduced to a single numeric literal constituting the final result.
For the sake of making this precise, observe that, in an FPAE, the leftmost immediately evaluable subexpression is always the one ending with the leftmost right parenthesis. Suppose that we always choose that subexpression as the one to evaluate next. Applying this strategy to the FPAE given above, we get, on successive iterations:
(((15 - 1) + 2) + (3 * ((6 + 0) / (1 + 2))))
^^^^^^^^
= (( 14 + 2) + (3 * ((6 + 0) / (1 + 2))))
^^^^^^^^^^^^^^
= ( 16 + (3 * ((6 + 0) / (1 + 2))))
^^^^^^^
= ( 16 + (3 * ( 6 / (1 + 2))))
^^^^^^^
= ( 16 + (3 * ( 6 / 3 )))
^^^^^^^^^^^^^^^^^^^
= ( 16 + (3 * 2 ))
^^^^^^^^^^^^^^^^^^^^^^^^^
= ( 16 + 6 )
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
= 22
In each expression, the leftmost immediately evaluable subexpression is indicated.
Using our human common sense, this was easy. But how can we give precise instructions to a computer ---which has absolutely no sense--- that would make it correctly evaluate such an expression? Letting E denote the FPAE to be evaluated, our approach may be expressed in pseudocode as follows:
set a pointer to the beginning of E;
while (E is not a numeric literal) {
advance the pointer to right until encountering a right parenthesis;
evaluate the subexpression ending at that right parenthesis and
beginning with its mate;
replace that subexpression with the corresponding numeric literal;
}
Although this is still somewhat vague, it provides a basis for a more precise algorithm. Among the details to be worked out is how the program determines, upon encountering a right parenthesis, which subexpression is to be evaluated. One way would be to scan to the left (i.e., backwards) until encountering the matching left parenthesis. Necessarily, between the two parentheses there would be a numeric literal, an operator, and another numeric literal. (Either or both of those numeric literals could be the result of evaluating a complicated subexpression earlier.) How does the program scan to the left? In what kind of storage structure do the values of already-evaluated subexpressions reside?
One way to store them is by using two stacks, which we refer to as the operand stack and operator stack, respectively. As we scan the expression from left to right, on the latter we store the operator symbols and on the former we store the values of the operands. Upon encountering a right parenthesis (which, as noted before, indicates that we have reached the end of the leftmost immediately evaluable non-atomic subexpression), we pop an operator symbol off one stack and two values from the other stack, we apply that operator to those two values, and then we push the result back onto the operand stack. After the last token of the FPAE has been processed, the value of the FPAE will be the lone value remaining on the operand stack.
In order to prove that this works, we can show that each time a right parenthesis is encountered, the operator to which it corresponds is at the top of the operator stack and the (values of the) two operands to which it corresponds are the top two numbers on the operand stack. Such a proof is omitted, but the reader is encouraged to do several examples, after which he should be convinced of the claim's plausibility.
We formalize the above with the following Java-like method. It assumes the existence of classes Expr, Token, and Operator having the instance methods that are invoked and a class StackX that implements the interface Stack shown above. For simplicity, it also assumes that all numeric literals describe integers.
/* pre: e is a syntactically correct FPAE
post: value returned is that obtained by evaluating e
*/
public Integer evaluate( Expr e ) {
Stack<Integer> operandStk = new StackX<Integer>();
Stack<Operator> operatorStk = new StackX<Operator>();
Token t = e.firstToken();
while (e.hasMoreTokens()) {
t = e.nextToken();
if (t is a left parenthesis) { }
else if (t is an integer literal) { operandStk.push(t); }
else if (t is an operator) { operatorStk.push(t); }
else if (t is a right parenthesis) {
Integer y = operandStk.topOf();
operandStk.pop();
Integer x = operandStk.topOf();
operandStk.pop();
Operator op = operatorStk.topOf();
operatorStk.pop();
operandStk.push( op.apply(x,y) ); // apply op to x and y; push result
}
}
return operandStk.topOf();
}
As indicated by its precondition, the method above assumes that its
parameter is a syntactically correct FPAE. It is not hard to augment the
method in order to give it the ability to detect when its parameter is
not syntactically correct. To accomplish this, use the two stacks
to hold not only operand and operator values, respectively, but also
left parentheses.
(Note: Conceptually, this is simple, but, depending upon the
programming language, it may not be trivial to implement because it requires
that the stacks be heterogeneous (i.e., have the ability to hold items of
different data types).
(In Java, we could do this by instantiating the Stack class with the type
Object.)
When a left parenthesis is encountered, push it onto both stacks.
When a right parenthesis is encountered, do as specified above, but also
do an extra pop on both stacks. If the extra tokens popped are not both
left parentheses, the original FPAE was syntactically invalid.
The most obvious way to represent a stack using an array turns out to be
a good way of doing it. We simply store ---in elements 0, 1, etc., of the
array (call it contents) ---the items that are currently on the
stack, from bottom to top.
In order to perform a pop or push, there must be some
way to tell which location of the array corresponds to the top
of the stack. To fulfill this purpose, we use an instance variable of type
int, numItems, whose value indicates the number of items
currently occupying the stack. That is, the values occupying
contents[0..numItems-1] should correspond to the items on the stack.
In particular, the item stored in contents[numItems-1] (assuming
that numItems > 0) is the item at the top of the stack.
Under this representation scheme, each method in the class can be written
using code that is very simple (for a reader to follow) and very
efficient (for a computer to execute).
As an example, suppose that we have a stack of creatures from the class
Animal. For simplicity, each animal will be denoted by its name
(e.g., COW). The picture below corresponds to the representation of a
stack with five animals on it. The values in array elements
5, 6, ..., N-1 (where N == contents.length) are shown
as "---", which is intended to indicate that they are irrelevant.
The only serious question that arises in implementing this approach is
what to do in response to a push when the array
contents is "full" (i.e., the number of items on the stack
is equal to contents.length).
One reasonable answer is to create a larger array, copy the elements
of contents[] into that array, and then make contents[]
refer to the new array. In effect, this lengthens contents[]
at a cost (in running time) that is proportional to its current
length (i.e., its length before lengthening it!).
But by how much should we lengthen the array? By one element? By 10?
Actually, it turns out that, in order to ensure that the total cost of
all lengthenings is (at worst) proportional to the total number
of push operations performed upon the stack during its lifetime,
we should lengthen the array each time by some fraction of its current
length. (The reasoning behind this claim is beyond the scope of the course.)
A good fraction to use is 1, which would mean that the array is
doubled in length each time.
In order to avoid wasting space, the pop method should
detect when the number of items on the stack has become so few, relative
to contents.length, that contents[] should be
contracted (i.e., made shorter in length).
It turns out that one good strategy is to cut the array in half whenever
a pop reduces the number of items on the stack to less than a fourth of
the array's length.
The resulting class would look much like the following.
Hence, although the abstract stack structure is growing and shrinking
in small increments, the underlying structure used to represent it is
growing and shrinking in large increments.
A concrete representation that makes use of references, rather than an
array, can conveniently grow and shrink incrementally, just like the
abstract structure that it represents.
The idea is to make use of a (generic) class that provides one-directional
linking capabilities. We shall call this class Link1<T>.
An object of this class can be depicted as
Using Link1 as a basis, we can represent the stack containing
COW, CAT, DOG, BUG, and ANT objects (going from top to bottom) as follows,
where top is the lone instance variable comprising the state
of the stack and it points to the Link1<T> object corresponding
to the top item on the stack:
For simplicity, we have simply written each Link1 object's
animal name inside (the box representing) its first field.
In reality, each such field is a reference (i.e., pointer) to the
corresponding animal object.
Following this approach, here is the stack class that we derive:
An Array-based Implementation/Data Representation
+-----+
N-1| --- |
. | . |
. | . |
. | . |
6 | --- |
5 | --- |
4 | DOG |
3 | CAT |
2 | BUG |
1 | EEL | +-----+
0 | COW | | 5 |
+-----+ +-----+
contents numItems
public class StackViaArray<T> implements Stack<T> {
/* i n s t a n c e v a r i a b l e s */
private int numItems;
private T[] contents;
private static final int DEFAULT_INIT_LEN = 8;
/* c o n s t r u c t o r s */
public StackViaArray(int initLen) {
numItems = 0;
contents = (T[])(new Object[initLen]);
}
public StackViaArray() { this( DEFAULT_INIT_LEN ); }
/* o b s e r v e r s */
public boolean isEmpty() { return numItems == 0; }
public T topOf() { return contents[numItems-1]; }
/* m u t a t o r s */
public Stack push( T item ) {
if (numItems == contents.length) {
// contents[] is full, so double its length
T[] temp = (T[])(new Object[2 * contents.length]);
for (int i=0; i != numItems; i = i+1)
{ temp[i] = contents[i]; }
contents = temp;
}
contents[numItems] = item;
numItems = numItems + 1;
}
public void pop() {
contents[numItems-1] = null; // to help garbage collection
numItems = numItems - 1;
if (contents.length > DEFAULT_INIT_LEN) {
if (numItems < contents.length / 4) {
// contents is at most one-quarter full, so halve it
T[] temp = (T[])(new Object[contents.length / 2]);
for (int i=0; i != numItems; i = i+1)
{ temp[i] = contents[i]; }
contents = temp;
}
}
else { } // don't make contents any shorter than DEFAULT_INIT_LEN
}
}
A Reference-Based Implementation/Data Representation
One of the less attractive features of using an array as the basis
upon which to represent a stack is that, when the stack's size becomes
"incompatible" with the size of the array (i.e., when either the
stack has become too large to fit into the array or it has become
so small that a significant portion of the array is unused),
it becomes necessary/wise to create a new, differently-sized array
and to copy all the relevant data into it from the old array.
In order to ensure that this "size-change" operation does not
dominate the time required to process stack operations,
the new array's size should be significantly different from the old one.
(In our implementation, the new array is made to be either double
or half the size of the old one.)
+---+---+
| x | x-+----> points to a Link1<T> object
+-+-+---+
|
|
v
points to an object of type T
That is, a Link1<T> object contains a reference to an
object (of type T) and a reference to
another object of type Link1<T>. An implementation of
this class is as follows:
public class Link1<T> {
protected T item;
protected Link1<T> next;
public Link1(Link1 next, T item)
{ this.item = item; this.next = next; }
public Link1(T item) { this(null, item); }
public Link1() { this(null, null); }
public T getItem() { return this.item; }
public Link1 getNext() { return this.next; }
public void setItem(T item) { this.item = item; }
public void setNext(Link1 next) { this.next = next; }
}
+-----+---+ +-----+---+ +-----+---+ +-----+---+ +-----+---+
| COW | x-+---->| CAT | x-+---->| DOG | x-+---->| BUG | x-+---->| ANT | x-+--!
+-----+---+ +-----+---+ +-----+---+ +-----+---+ +-----+---+
^
|
|
+-+-+
| x |
+---+
top
class StackViaLink1<T> implements Stack<T> {
/* i n s t a n c e v a r i a b l e s */
protected Link1<T> top;
/* c o n s t r u c t o r s */
public StackViaLink1() { top = null; }
/* o b s e r v e r s */
public boolean isEmpty() { return top == null; }
public T topOf() { return top.getItem(); }
/* m u t a t o r s */
public void push( T item ) { top = new Link1<T>( top, item ); }
public void pop() { top = top.getNext(); }
}