Notes on Stacks

(related reading: Main & Savitch, Chapter Seven)

Pancakes

Suppose you have a stack of pancakes. What can you do? and that's about it. Limited number of possible actions. Still useful (and fun).

Stacks

A stack is an ordered collection of items for which we can only add or remove items from one end (the top of the stack). The stack is another container class, much like a list, but with a much more limited set of operations:

We are interested in designing a stack class template so that we may place all sorts of things on our stacks. Our interface will look something like:

template <class Item>
class Stack {
public:
  bool isEmpty() const;
  size_t size() const;
  Item pop();
  void push(const Item& it);
  ...
private:
  ...
};

Stacks are "last in, first out" (LIFO) structures. For example,
C++ command                      resulting stack
------------------------------------------------
Stack<int> S;
                                  _____ (empty stack of ints)



S.push(7);                            
                                  | 7 |  <-- top
                                  -----

S.push(2);                            
                                  | 2 |  <-- top 
                                  | 7 |
                                  -----

S.push(73);                           
                                  |73 |  <-- top 
                                  | 2 |
                                  | 7 |
                                  -----

S.pop();                           
                                  | 2 |  <-- top
                                  | 7 |                    -----
S.pop();      
                                  -----
S.pop();                           
                                  | 7 |  <-- top
                                  -----
S.pop();                           
                                  -----  (empty)

S.pop();                           
                    ERROR "stack underflow"

Stack Implementations

We consider two implementations of a Stack class template. The first uses arrays for the underlying representation. Because stack operations are very limited in nature, arrays can be used without compromising efficiency - we can't put something in the middle of the stack, only on the top. The array implementation can be found here.

As an alternative, linked-lists may be used to represent stacks. We can either build the linked-list data structure right into the stack class (using an auxiliary class for the "nodes" of the stack) as we did for StringList in class, or, as with the Set class template in Programming Assignment One, a Stack class template can be built using a List class template underneath (an example of code reuse). An example of this can be found here (note: the entire code for this won't be available until after Program One has been turned submitted.) (A variation on the linked list representation, using a nested, private, StackNode class, can be found in the supplemental notes on stacks.

Stack Errors

Two things can go wrong with stacks:

Stack Applications

Parentheses Matching

Many applications of stacks deal with determining whether an input string (or file, or sequence of symbols) is a member of a context-free language. (For a formal introduction to context-free languages (CFLs), take Computer Science 520.) Many programming languages are context free. Also many simpler languages.

As an example, consider the language of balanced parentheses. A sequence of symbols involving ()+* and integers, is said to have balanced-parentheses if each right parenthesis has a corresponding left parenthesis that occurs before it. For example: These expressions have balanced parentheses:

2*7          // no parens - still balanced
(1+3)   
((2*16)+1)*(44+(17+9))
These expressions do not:
(44+38    
)            // a right paren with no left
(55+(12*11)  // missing a )

How do we tell if a sequence of characters represents a balanced-parentheses expression? Use stacks. Idea:

(See C++ code to do this in Main & Savitch, p. 312.)

What items do we push onto the stack? If we are just interested in knowing if the expression is balanced, it does not matter. For clarity, we might choose a stack of characters, pushing "(" onto the stack for each left parenthesis.

We can do more. We can match multiple types of delimiters. For example, we might want to match (), [], and {}. In that case we can push the left delimiter onto the stack, and, when we pop, do a check, as in the pseudocode:

if (next character is a right delimiter) then {
  if (stack is empty) then
    return false
  else {
    pop the stack
    if (right delimiter is corresponding version
        of what was popped off the stack) then
	continue processing
    else
       return false
  }
}

Other variations on parentheses matching include:

Memory management issues for stacks

Warning: It is undefined in C++ to copy or assign static arrays. For example,

int a[20];
int b[20];
...
a = b;
is implementation dependent. Some compilers may refuse to compile this. Others may take no action. Still others (at least appear to) copy the elements of b to a. Therefore, whenever objects have static array members (as in our static array implementation of stacks), proper copy constructors and assignment operators should be defined to avoid ambiguity. Such a copy constructor and assignment operator for our static array implementation of stacks can be found here.

For stacks with static arrays, we don't need to worry about the destructor. (The array will be destroyed automatically when the object is). Nor need we worry about destoying items that are poppoed off the stack. Consider the code for push and pop:

template<class Item, size_t cap>
void Stack<Item,cap>::push(const Item& it)
{
  assert(!isFull());

  data[count] = it;
  count++;
}


template<class Item, size_t cap>
Item Stack<Item,cap>::pop()
{
  assert(!isEmpty());

  count--;
  return(data[count]);
}
The space occupied by data[count] that is no longer needed after pop returns that item, will be reused by the next assignment (i.e. the next push). Assuming that Item has a properly functioning copy constructor, assignent operator and destructor, then
data[count]=it 
invokes Item's assignment operator so memory management for the Item is taken care of there. Our documentation states (effectively a precondition for the whole class template) that the "template parameter, Item, is the data type of the items in the Stack. It may be any of the C++ built-in types (int, char, etc.), or a class with a copy constructor, assignment operator, and destructor." Unlike other forms of preconditions, we cannot check this at compile time or run-time. In order for a Stack class to be implemented properly we have to trust that the Item class has been implemented properly as well.

For a stack class template that uses linked lists, we need to write an explicit copy constructor, assignment operator, and destructor because memory is allocated dynamically. However, if we reuse the List template, these become trivial, because all the dynamic memory allocation takes place within that template's implementation (not the stack implementation). The copy constructor, assignment operator, and destructor for stack will automatically call the corresponding member function for the stack's sole member variable, a list.

Run-time complexity of stack operations

For all the standard stack operations (push, pop, isEmpty, size), the worst-case run-time complexity can be O(1). We say can and not is because it is always possible to implement stacks with an underlying representation that is inefficient. However, with the representations we have looked at (static array and a reasonable linked list) these operations take constant time. It's obvious that size and isEmpty constant-time operations. push and pop are also O(1) because they only work with one end of the data structure - the top of the stack. The upshot of all this is that stacks can and should be implemented easily and efficiently.

The copy constructor and assignment operator are O(n), where n is the number of items on the stack. This is clear because each item has to be copied (and copying one item takes constant time). The destructor takes linear time (O(n)) when linked lists are used - the underlying list has to be traversed and each item released (releasing the memory of each item is constant in terms of the number of items on the whole list). Destructors for arrays take constant time since contiguous chunks of memory can be freed in one fell swoop.

Other Stack Applications