The Stack ADT/Collection Class

The Concept
Applications
Array-based Implementation/Data Representation
Referenced-based Implementation/Data Representation

The Concept

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.) We shall not follow this approach, however. Rather, we will assume that any item on the stack can be observed, and that each of them is referred to by its position on the stack (with the item at the top having position zero, the one below that having position one, etc.).

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 type parameter indicates the data type of the items allowed to be inserted into the stack), we get the following:


/** An instance of a class implementing this interface represents a stack
**  capable of holding items of the specified type T (the generic type
**  parameter).
**
**  Author: R. McCloskey
**  Date: March 2012
*/
public interface Stack<T> {


   /*  <<<<<  o b s e r v e r s  >>>>>  */

   /** Returns the number of items on the stack.
   **  pre: none
   */
   public int sizeOf(); 

   /** Returns true if the stack is empty, false otherwise.
   **  pre: none
   */
   public boolean isEmpty(); 


   /** Returns (a reference to the) item at the top of the stack.
   **  pre: !this.isEmpty()
   */
   public T topOf();


   /** Returns (a reference to) the k-th item on the stack
   **  (counting starting at zero from the top).
   **  pre: 0 <= k < sizeOf()
   */
   public T item(int k);


   /*  <<<<<  m u t a t o r s  >>>>>  */


   /** Places the specified item onto the top of the stack.
   **  pre:  let s == this
   **  post: this.topOf() == item  &&  s == this.pop()
   */
   public void push(T item);


   /** Removes the item at the top of the stack.
   **  pre: !this.isEmpty()  &&  let this == s
   **  post: this.push(s.topOf()) == s
   */
   public void pop();

}

Applications of Stacks:

You may find it surprising to learn that, despite the simplicity of the concept, stacks are quite useful in practice.

Managing Program Execution

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

Evaluating Arithmetic Expressions

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
  1. a numeric literal, or
  2. an expression of the form  ( F op G ) 
where F and G are themselves FPAE's and op is an arithmetic operator (e.g., +, -, etc.). We refer to FPAE's of the first form as atomic and those of the second form as composite.

Using a context-free grammar, we can state the definition like this:

   <FPAE>     --->  <numeric literal>
   <FPAE>     --->  ( <FPAE> <operator> <FPAE> )
   <operator> ---> +  |  -  |  *  |  /

Notes: 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. For the sake of simplicity, this definition allows only binary operators, and thus excludes unary + and - (as in -4). End of notes.

How does one evaluate such an expression? Most likely, you would find an immediately evaluable composite 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 underlined.

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:

place a cursor at the beginning of E;
while (E is not a numeric literal) {
   advance the cursor to the right until encountering a ')';
   evaluate the subexpression ending there and starting with the nearest '(' to the left;
   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 composite 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 (t != null) {
      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) ); // push x op y onto operand stack
      }
      t = e.nextToken();  // returns null if there are no more tokens
   }
   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. In addition, whenever an operator is encountered, check the operand stack to verify that the item at the top is a number (rather than a left parenthesis).

An Array-based Implementation/Data Representation

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.

        +-----+
     N-1| --- |
      . |  .  |
      . |  .  |
      . |  .  |
      6 | --- |
      5 | --- |
      4 | DOG |
      3 | CAT |
      2 | BUG |       
      1 | EEL |     +-----+
      0 | COW |     |  5  |
        +-----+     +-----+
        contents    numItems 

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.

/** An instance of this class represents a stack capable of holding items
**  of the specified type T (the generic type parameter).
**  The implementation is based upon storing the stack items in an array.
**
**  Author: R. McCloskey
**  Date: March 2012
*/

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;  // # items occupying the stack
   private T[] items;     // holds (references to) the items on the stack


   /*  s y m b o l i c   c o n s t a n t  */

   private static final int DEFAULT_INIT_CAPACITY = 8;


   /*  c o n s t r u c t o r s  */

   public StackViaArray(int initCapacity)
   {
      numItems = 0;
      items = (T[])(new Object[initCapacity]);
   }

   public StackViaArray() { this( DEFAULT_INIT_CAPACITY); }


   /*  o b s e r v e r s  */

   public boolean isEmpty() { return sizeOf() == 0; }

   public int sizeOf() { return numItems; }

   public T topOf() { return item(0); }

   public T item(int k) { return items[numItems-1-k]; }

   public String toString()
   {
      StringBuilder s = new StringBuilder();
      for (int i=0; i != sizeOf(); i++)
      {
         s.append(item(i).toString() + ",");
      }
      return s.substring(0,Math.max(0, s.length()-1));
   }


   /*  m u t a t o r s  */

   public void push( T item )
   {
      if (numItems == items.length)
      {
         // items[] is full, so double its length by creating a new array
         // (having double the length), copying the values from items[]
         // into the new array, and then making items[] refer to the new array
         T[] temp = (T[])(new Object[2 * items.length]);
         arrayCopy(items, temp, numItems);
         items = temp; 
      } 
      items[numItems] = item;
      numItems = numItems + 1;
   }


   public void pop()
   {
      items[numItems-1] = null;  // to aid garbage collection
      numItems = numItems - 1;

      if (items.length > DEFAULT_INIT_CAPACITY  && items.length > 4 * numItems)
      { 
         // The length of items[] is greater than the default initial capacity
         // and more than four times the stack's size, so cut the length of
         // items[] in half.
         T[] temp = (T[])(new Object[items.length / 2]);
         arrayCopy(items, temp, numItems);
         items = temp;
      }
   }

   /*  u t i l i t y  */

   /** Copies values in source[0..length-1] into dest[0..length-1]
   */
   private void arrayCopy(T[] source, T[] dest, int length)
   {
      System.arraycopy(source, 0, dest, 0, length);
      // alternative:
      // for (int i=0; i != length; i++)
      //    { dest[i] = source[i]; }
   }
} 

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

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

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

/** An instance of this class contains a reference to an object of the
**  specified type T (the generic type parameter) and a reference to
**  an object of the same kind (i.e., Link1).  The idea is that objects
**  of this class can be used as building blocks of one-directional linked
**  structures (i.e., one-way lists).
*/
public class Link1<T> { 

   /*  instance variables  */

   private T item;
   private Link1<T> next;

   /*  constructors  */

   public Link1(T item, Link1<T> next)
   { 
      this.item = item; this.next = next;
   }

   public Link1(T item) { this(item, null); }

   public Link1() { this(null, null); }


   /*  observers  */

   public T getItem() { return item; }
   public Link1<T> getNext() { return next; }


   /*  mutators  */

   public void setItem(T newItem) { item = newItem; }
   public void setNext(Link1<T> newNext) { next = newNext; }

}

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:

+-----+---+     +-----+---+     +-----+---+     +-----+---+     +-----+---+
| COW | x-+---->| CAT | x-+---->| DOG | x-+---->| BUG | x-+---->| ANT | x-+--!
+-----+---+     +-----+---+     +-----+---+     +-----+---+     +-----+---+
     ^
     |
     |
   +-+-+
   | x |
   +---+
    top 

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 instance of this class represents a stack capable of holding items
**  of the specified type T (the generic type parameter).
**  The underlying implementation makes use of a linked structure of
**  objects arising from the Link1 class.
**
**  Author: R. McCloskey
**  Date: March 2012
*/
public class StackViaLink1<T> implements Stack<T> {

   /*  i n s t a n c e    v a r i a b l e s  */

   private Link1<T> top;  // reference to Link1 object holding top item on stack
   private int numItems;  // # of items on the stack


   /*  c o n s t r u c t o r s  */

   public StackViaLink1() 
   { 
      top = null;  numItems = 0;
   }


   /*  o b s e r v e r s  */

   
   public int sizeOf() { return numItems; }

   public boolean isEmpty() { return sizeOf() == 0; }

   public T topOf() 
   { 
      return top.getItem();  // alternative: return item(0)
   }

   public T item(int k)
   {
      Link1<T> x = top;           // have x begin at the top
      for (int i=0; i!=k; i++) {  // follow next references k times
         x = x.getNext();
      }
      return x.getItem();         // return the item there
   }


   public String toString()
   {
      StringBuilder s = new StringBuilder();
      for (int i=0; i != sizeOf(); i++)
      {
         s.append(item(i).toString() + ",");
      }
      return s.substring(0,Math.max(0, s.length()-1));
   }


   /*  m u t a t o r s  */

   public void push( T item )  
   { 
      top = new Link1<T>( item, top );
      numItems = numItems + 1;
   }

   public void pop()
   { 
      top = top.getNext(); 
      numItems = numItems - 1;
   }

}