Collection Classes:
Stacks, Queues, PriorityQueues

Advanced Programming/Practicum
15-200


Introduction This lecture covers the details of how ordered collection classes are defined, implemented, and used. Little of this material is new: we have already introduced stacks and queues in a previous lecture (including their implementations by the SimpleStack and SimpleQueue classes), and priorities queues in Programming Assignment 4 (the SimplePriorityQueue class).

Unlike all the other collections that we will study, these are NOT in the standard Java library. So instead, I have written my own descriptions for these classes (in the edu.cmu.cs.pattis.orderedCollections package) in a style similar to the collections that we will study in the standard Java library. These descriptions unify all these data types under the OrderedCollections interface, bringing uniformity to the names of the methods: push and enqueue become add; pop and dequeue become remove.

We cover these collections first because they are familiar and a bit simpler to understand than lists, sets, and maps (which are covered in subsequent lectures). You can download, unzip, run, and examine all this code (along with a driver program) in the Ordered Collections Demonstration.


Design of OrderedCollection Classes In this section we will examine in detail the collection classes that implement the OrderedCollection interface. We will examine the overall relationships among this interface, the abstract classes that implement it, and the concrete classes that extend these abstract classes; these features naturally arrange themselves into three vertical levels in a hierarchy. The following legend explains the three levels and some of the notation used.

 

For ordered collections, we will explore the following hierarchy of interfaces, abstract classes, and concrete classes.

  This interface is implemented by abstract classes (two levels, which supply some but not all of the needed methods) which are extended by concrete classes that inherit some behvaior from the abstract classes and concretely define all their abstract methods. Recall that concrete subclasses automatically implement the interfaces that their superclasses implement, and [abstract]classes can implement more than one interface (but can extend only one superclass); this is a fundamental difference between interfaces and classes.

In the next three sections, we will examine the Javadoc of this interface, the abstract classes that implement it, and the concrete classes that extend these abstract classes (the ones using arrays, not linked lists -yet).


Interfaces The methods specified in the OrderedCollection interface are summarized in the following Javadoc (you can read the full Javadoc online, using the Javadoc of Course API link). The semantics of most methods should be somewhat intuitive. Primarily, objects can be added and removed from an ordered collection: their order of removal is based on the type of ordered collection: LIFO (for stacks), FIFO (for queues), and "priority ordering" for priority queues.

  Fundamentally, the add method adds an object to the ordered collection (generalizing push and enqueue). Note that add returns true if its value is added to the collection (another way to say this is the size increases): for ordered collections this is always the case, but with a collection like set, which has no duplicate values, it will return false if the element is already stored in the set. The addAll method iterates through every object in its parameter ordered collection, and adds each (in order) to this collection.

The remove and peek methods return a reference to the next object in the ordered collection (according to its behavior) respectively removing or not removing it; these methods work only if isEmpty returns true (i.e., size returns 0). Note that remove returns true if it removes a value from the collection (another way to say this is the size decreases): for ordered collections this is always the case, but with a collection like set, specifying to remove a value might return false if that value is not in the set.

The size method returns the number of elements in the ordered collection, and the clear method empties the ordered collection (its size becomes 0). The iterator method returns an iterator whose next method iterates over the ordered collection in the order the elements would be removed. Finally, there are various methods that return this ordered collection as an array or Collection (this interface is to be covered in the next lecture).

Now we will examine an abstract class and abstract subclass that implement a large number of these methods, leaving the concrete subclass to implement/override just a few (mostly add, remove, peek, and especially iterator, which many other methods use). Remember that there are 13 methods specified in this interface.


Abstract Classes Now we will examine the Javadoc of an abstract class and its abstract subclass that mostly implement the interface specified above (although some of its methods are abstract). The OrderedCollection interface specified 13 methods. The AbstractOrderedCollection class specifies one protected constructor and 13 methods; it doesn't define equals which is inherited from the Object class that this one implicitly extends (and overridden is the abstract subclass in the next section); it adds the specification of a toString method. Of these 13 (=13-1+1) methods, all but the iterator and peek are defined here, although operations like add and remove are only partially implemented, and must be overridden. Here is the Javadoc of AbstractOrderedCollection.

  Basically, this class stores as instance variables the int values objectCount (incremented, decrements, and checked by add, remove, and size) and modCount (checked by fail-fast iterators). Read the .java file for this class and note how all it methods are implemented. For example addAll can be implemented by the code
  public boolean addAll (OrderedCollection o)
  {
    modCount++;
    boolean modified = false;
    Iterator e = o.iterator();
      while (e.hasNext())
        if(add(e.next()))
          modified = true;
    return modified;
  }
even though the iterator method is abstract and the add method will be overridden. Given that these methods are promised (in the case of add, exists but is not complete), this code make sense to Java here: the ordered collection o is iterated over, and each of its elements is added to the this ordered collection.

We could also write this loop as

  while (e.hasNext())
    modified = add(e.next()) || modified;
but NOT as
  while (e.hasNext())
    modified = modified || add(e.next();
because the short-circuit evaluation would not execute any more calls to add once modified was set to true.

Likewise, the clear method can be implemented by the code

  public void clear ()
  {
    if (!isEmpty())
      modCount++;
    while (!isEmpty())
      remove();
  }

which removes values (ignoring the returned references) until the ordered collection is empty. Finally, all the to... methods (toArray, toCollection, and toString) can be implemented, again using the iterator method, which is promised to be defined in the concrete subclass. For example

  public Object[] toArray()
  {
    Object[] result = new Object[size()];
    Iterator e = iterator();
    for (int i=0; e.hasNext(); i++)     //or... int i=0; i<result.length; i++
      result[i] = e.next();
    return result;
  }
where an array of the right size is allocated, and this ordered collection is iterated over, and each of its elements is put into its correct array position.

The AbstractStack, AbstractQueue, and AbstractPriorityQueue, classes all extend AbstractOrderedCollection. Each also specifies one protected constructor, and specifies just one method: it overrides the equals method inherited from Object. This method allows only stacks to be equal to other stacks, and only then if their elements, ordered LIFO, are all equal. Here is the Javadoc of AbstractStack, which is very short; AbstractQueue, and AbstractPriorityQueue are defined similarly.

  Look the Methods inherited from class java.lang.Object. Is it a bug in Javadoc that since this class overrides equals the first comma has nothing in front of it?

The equals method in this class is defined as follows:

  public boolean equals(Object o)
  {
    if (o == this)
      return true;
    if (!(o instanceof AbstractStack))
      return false;

    if ( size() != ((AbstractStack) o).size() )
      return false;

    Iterator e1 = iterator();
    Iterator e2 = ((AbstractStack) o).iterator();
	    
    while(e1.hasNext() && e2.hasNext()) {  //simplify to just e1.hasNext()?
      Object o1 = e1.next();               //...stacks have the same size!
      Object o2 = e2.next();
      if (!(o1==null ? o2==null : o1.equals(o2)))
       return false;
    }
  
    return true;
  }
All equals methods look similar to this one. First, if the two objects being compared are the same, it returns true. Second, if the parameter isn't an AbstractStack (it can be any concrete extension of this abstract class) it returns false. Third, if the sizes of the stacks are unequal, it returns false. Now comes the interesting part, both stacks (which are now known to have the same size, so the test can be simplified) are traversed in a loop, and their elements are checked for equality (null is a special case): if any are unequal, it returns false; if all are equal, it eventually terminates the loop and returns true.

My declaration of objectCount and use of this instance variable in add and remove can be criticized; critics would prefer this protected instance variable to be removed from this class and instead defined private in the subclasses; and, these methods to be declared abstract here and defined in the subclass, rather than be declared concrete here and overridden in the subclass.

Next we will examine the Javadoc and implementation a concrete class that extends AbstractStack and one that extends AbstractPriorityQueue.


Concrete Classes Now we will examine the Javadoc of a concrete class that extends AbstractStack and one that extends AbstractPriorityQueue. The ArrayStack class is implemented with an array backing the stack (we will examine the other implementation, LinkedStack, in detail after we discuss linked lists). The ArrayStack class defines 5 public constructors and 7 methods: it overrides the inherited methods add, remove, and toString; it concretely defines two inherited abstract methods, iterator and peek; and it defines two new methods, not mentioned previously: ensureCapacity and trimToSize, which relate only to array implementations of stack (not to stacks theselves). Here is the Javadoc of ArrayStack (because of size constraints, it appears in a smaller font).

  Note that we can construct an empty stack (with some small backing array), an empty stack with a backing array whose length starts at some initial capacity, and a non-empty stack with values added from a Collection (studied in the next lecture), object array, or any OrderedCollection (if they are not empty). We can call EnsureCapacity to increase the length of the backing array to any value, or trimToLength to decrease it to the mininmum length needed to store all its current values.

The other methods here just do what they are supposed to, for stacks, following LIFO behavior.

Now on to the ArrayPriorityQueue class. It is implemented with an array backing the priority queue. The ArrayPriorityQueue class defines 5 public constructors and 7 methods: it overrides the inherited methods add, remove, and toString; it concretely defines two inherited abstract methods, iterator and peek; and it defines two new methods, not mentioned previously: ensureCapacity and trimToSize, which relate only to array implementations of priority queues (not to priority queues theselves). Here is the Javadoc of ArrayPriorityQueue (because of size constraints, it appears in a smaller font).

  As with stack constructurs, we can construct an empty priority queue (with some small backing array), an empty priority queue with a backing array whose length starts at some initial capacity, and a non-empty priority queue with values added from a Collection (studied in the next lecture), object array, or any OrderedCollection. We can call EnsureCapacity to increase the length of the backing array to any value, or trimToLength, which decreases it to the mininmum length needed to store its current values.

In addition, note that each constructor for a priority queue requires a parameter that is a reference to an object that is constructed from a class implementing the Comparator interface (a mouthful, but the same mouthful every time). This is the object that determines the relative priorities of any two elements in the priority queue; with this object we can arrange to keep the backing array sorted from lowest to highest priority. The Javadoc details specify that its matching argument must be non-null, otherwise the constructor throws an exception.

So, the structure leading from the OrderedCollection interface to the ArrayStack, ArrayQueue, and ArrayPriorityQueue concrete classes involve all sorts of interesting inheritance of abstract and concrete methods. Again, we can USE all these classes without knowing this information, mostly by examining just the OrderedCollection interface and the constuctors for these classes, and knowing that they implement their methods efficiently.

For this last aspect, skip a section to see a discussion of the performance of methods in terms of big O notation, where n is typically the number of elements stored in the collection.


Iterators for OrderedCollection Unlike other collections, the main purpose of ordered collection is just to remove values is an interesting order: LIFO, FIFO, or priority order. Thus, the iterators for these classes all throw UnsupportedOperationException when remove is called. This simplifies (by just a bit) their implementation, and doesn't impact their use much (so, we will live with this simplification and restriction).

Note that iterators are used frequently in the implementation of many methods itself, even in the abstract classes.


Complexity Classes The following table summarizes the complexity classes of all the methods in the different implementations of OrderedCollection. When we learn about heaps (technically a tree, but a special kind that can be backed by an array), we will see another implementation of priority queues with interesting complexity classes for add and remove.

In the table below * means amortized complexity. That is, when we add a value in an array, most of the time we perform some constant number of operations, independent of the size of the array. But every so often, we must construct a new array object with double the length, and then copy all the array's elements into it. If we pretended that each add did more operations (but still a constant number), that number would dominate the actual number of operations needed for all the doubling.

In the table below ** means that the parameter to the method is some collection (or array) storing M elements.

Method ArrayStack LinkedStack ArrayQueue LinkedQueue ArrayP...Q... ArrayUnsortedP...Q...
add O(1)* O(1) O(1)* O(1) O(N) O(1)*
addAll O(M)* ** O(M)** O(M)* ** O(M)** O(MN)** O(M)* **
clear O(N) O(1) O(N) O(1) O(N) O(N)
remove O(1) O(1) O(1) O(1) O(1) O(N)
peek O(1) O(1) O(1) O(1) O(1) O(N)
size O(1) O(1) O(1) O(1) O(1) O(1)
isEmpty O(1) O(1) O(1) O(1) O(1) O(1)
equals O(N) O(N) O(N) O(N) O(N) O(N log2N)
iteration... O(N) O(N) O(N) O(N) O(N) O(N log2N)
toString O(N) O(N) O(N) O(N) O(N) O(N log2N)
toArray O(N) O(N) O(N) O(N) O(N) O(N log2N)
toCollection O(N) O(N) O(N) O(N) O(N) O(N log2N)
  Using backing arrays, and length doubling, the array length for storing N elements is never more than 2N words of memory; using linked lists, storing N elements always requires 2N words of memory.


Simple Examples of Using Ordered Collection Class In this section we will illustrate a few interesting uses of ordered collection classes. There are many many (many) more.

First, we will use a stack to reverse an array. Assume that we have declared Object[] o; and stored into it a reference to an object filled with values that we want to reverse.

  AbstractStack s = new ArrayStack();
  for (int i=0; i<o.length; i++)  //Push all values on stack
    s.add(o[i]);
  for (int i=0; i<o.length; i++)  //Pop them off in "reverse" order
    o[i] = s.remove();
Technically we can declare s with the OrderedCollection interface, but we use AbstractStack to make our intent clearer; certainly we want only subclasses of AbstractStack (not, for instance, ArrayQueue) to be allowed.

Second, we can use the same code, with a different class (priority queue), to get a much different result: sorting. Again, assume that we have declared Object[] o; and stored into it a reference to an object filled with values that we want to sort; also assume that c stores a reference to the Comparator that we want to use.

  AbstractPriorityQueue pq = new ArrayPriorityQueue(c);
  for (int i=0; i<o.length; i++)
    pq.add(o[i]);
  for (int i=0; i<o.length; i++)
    o[i] = pq.remove();
Performing N enqueues (an O(N) operation) is O(N2). Performing N dequeues (an O(1) operation) is O(N). Doing one, then the other, is O(N2) + O(N) = O(N2), so this is much worse that calling Arrays.sort(o,c), which is O(N Log2 N),

Third, in a "perfect" HTML document, each "opening" tag has a matching "closing" tag, nested the right way: <b><pre>some sentence</pre></b> is correct but <b><pre>some sentence</b></pre> is not: this is similar to matching different kinds of parentheses: [()] is legal but [(]) is not. Assume that docIterator is an iterator for tags in an HTML document. Calling next() returns a String with the next tag: e.g., b or /b; pre or /pre. We can use a stack to check whether an HTML document is perfect with the following code; it finishes or throws IllegalStateException whenever a mismatch is found. Note that each opening tag is pushed on the stack; each closing tag is attempted to be matched against the top of the stack.

  AbstractStack s = new ArrayStack();
  while ( docIterator.hasNext() ) {
    String tag = (String)doc.next();
    if ( tag.charAt(0) != '/' )    //Test tag: closing or opening
       s.add(tag);
    else
      if ( s.isEmpty() || !tag.substring(1).equals(s.peek()) )
        throw new IllegalStateException("Tag mismatch");
      else
        s.remove();
  }
  if (!is.isEmpty())                //Tags left unmatched
    throw new IllegalStateException("Closing tag(s) missing");
Fourth, we can use a stack to compute the area of a 2-d figure quickly, not by scanning all it internal cells, but by traveling around its boundary (analogous to Green's Theorem in analysis). Look at the following picture

  To compute the area of this figure, we could examine all 696 (24 x 29) cells, counting the ones that are purple. Another approach, which works for all convex pictures, is to start at the top leftmost purple cell (in the picture above, it is labeled by 1) and iterate over all the remaining boundary cells in a clockwise order (labeled by 2-53). Each time that we move farther right (X increases), we push the current Y coordinate of the cell on to a stack; each time that we move to the left, we pop the top of the stack and subtract it from the current Y coordinate (and add 1): this is a count of the cells (vertically) between the top and bottom coordinate. After we have iterated over all the cells (returned to the starting point), we have computed the area of the figure by circumnavigating its boundary.

Assume that figureIterator is an iterator for walking the boundary of a figure: in the example above it first returns the point (1,2), then (2,7), then (3,6), then (4,6), etc. (so far, pushing each of these on the stack). The first time it doesn't move to the right (label 27), it computes the vertical area as 3 (13-11+1), then adds 4 (14-11+1), then adds 6 (14-9+1), etc. (now subtracting from the current Y coordinate the Y coordinate of the point popped off the stack). Each pushed value (at the top of the figure) is popped off exactly when the the corresponding point at the bottom of the figure is being processed, allowing us to account for all the cells in the vertical strip between them (inclusive). We can use a stack to compute the area with the following code.

  AbstractStack s    = new ArrayStack();
  int           sum  = 0;
  Point         prev = new Point(-1,-1);    //To the left of any point!
  while ( figureIterator.hasNext() ) {
    Point curr = (Point)figureIterator.next();
    if (curr.x > prev.x)
      s.add(curr);
    else
      sum += curr.y  - ((Point)s.remove()).y + 1;
    prev = curr;
  }
This process can be run quickly, because it examines only cells on the boundary (which is O(N)), not all the internal cells, (which is O(N2).

Finally we can simulate a supermarket using queues (for the checkout lines) and priority queues (for the scheduling events; the event to happen most closely in the future has the highest priority). A simulated supermarket is specified by an array storing the maximum number of items that can be checked-out in each line. The events are "enter the store and start shopping" (at random intervals), "enter a checkout line" (based on the number of items bought, the sizes of the lines, and how many items are allowable), start checkout process (keep track of shopper's wait time: from entering a line to checking out), and "exit store". The unhappiness of a custom is computed (line wait time/shop time) and an average unhappiness over all shoppers is reported. The program also reports the running time and simulation speed (# of events processed/second). You can download, unzip, run, and examine all this code in the Supermarket Simulation.


Problem Set To ensure that you understand all the material in this lecture, please solve the the announced problems after you read the lecture.

If you get stumped on any problem, go back and read the relevant part of the lecture. If you still have questions, please get help from the Instructor, a CA, or any other student.

  1. In the ArrayPriorityQueue add is O(N) while peek and remove are O(1); in the ArrayUnsortedPriorityQueue add is O(1) while peek and remove is O(N). Think of a simple task where ArrayPriorityQueue would be a better class to use than ArrayUnsortedPriorityQueue. Now, think of a simple task where the reverse it true.

  2. Write an efficient algorithm to reverse an array in place, without using a Stack or any other object(s). Is the complexity class of this algorithms different from the algorithm shown in the lecture using a Stack.

  3. Why is no casting required for the argument to equals in the expression
      tag.substring(1).equals(s.peek())
    in the HTML code above?

  4. Can we use the .equals method in AbstractStack to determine whether an ArrayStack object is equal to a LinkedStack object? If not, why not? If so, under what condition are they equal?