Stacks

Heretofore (can you believe that's a word?), we've seen a direct relationship between ADTs and their implementing DSs, but we'll add a twist to that mix today...

Let's talk about another data structure that we've actually mentioned many times before, but can formalize now...

What is the call stack, and separately, if we had to think about as a data structure / collection, what would be its hallmarks / properties / operations?

Answering those questions separately:

  1. The Call Stack: manages local variable memory, scope, and where functions return to whenever called.

  2. Operations: new function calls place their stack frame on top of the one that called them in a "stacked" fashion!

It turns out that the Call Stack gets its name from another ADT!

A stack is an ADT used to store elements in a way that the most recently stored items are the first to be retrieved (FILO: First-in, Last-out).

FILO Intuition: The most recent thing we insert into a stack is the first thing we get back. Useful for certain scenarios wherein we care about the order in which items in a collection are stored.


Trust me, it's cool! By analogy, think of a pile of dishes:

  • You stack dishes one on top of the other

  • Once you've stacked dish Z on top of all the other dishes, you can only access dish Z (and cannot access any other dish beneath it) until you pop Z off the top again

Stacks are very useful; in fact, there's a reason why local variables are stored in the stack, because every function's variables get stacked on the top when they're called, and popped off again when you leave the function!

List some other examples of why you might use the FILO behavior of a Stack.

  • Retracing your steps in a maze so that you can backtrack and explore every path; moves most recently taken are near the top.

  • Some aspects of parsing code, such as determining which parentheses match with what other parentheses in some mathematical expression.

  • Some forms of state management in computer graphics, such as applying some style like a background color to a bunch of things before removing that style.


Stack Operations


Stacks support a few basic operations:

  • push(toPush); adds the given element to the top of the stack.

  • peek(); returns the element at the top of the stack.

  • pop(); removes the element at the top of the stack.


Stack Implementation


Stacks can actually be implemented using either a LinkedList or an ArrayList!

Since we're mostly familiar with ArrayLists, let's challenge ourselves by trying to use a LinkedList to represent a Stack:


OK, Andrew, why would we want to make a data structure that's a linked list... but more restricted than one?

This goes back to our public vs. private interface discussion: we want to give users of our classes certain guaranteed behaviors that allow for predictable code execution. If an algorithm requires that only the most recently pushed Node can be accessed at any time, we need to make sure our users don't tamper with that guaranteed behavior.

With this description in mind, let's try implementing our own IntStack using the IntLinkedList class we made last time!

Since we've done all of that work in the LinkedList class anyways, what class-design mechanism could we use to properly implement the Stack behavior?

How about inheritance! Let's just use the IntLinkedList class that we designed previously to implement our desired behavior.

  // Remark: Since this package is becoming more than just
  // lists, we'd probably rename it to something better, but
  // we'll keep it as-is for now just for illustrative purposes
  package intlist;

  // [?] This inheritance "works," but is it a good idea?
  public class IntStack extends IntLinkedList {
      
      IntStack () {
          super();
      }
      
      public void push (int toPush) {
          super.prepend(toPush);
      }
      
      public void pop () {
          super.removeAt(0);
      }
      
      public int peek () {
          return super.getAt(0);
      }
  
  }


Testing it, we see:

  package intlist;
  
  public class IntStackTests {
  
      public static void main (String[] args) {
          IntStack stacky = new IntStack();
          stacky.push(1);
          stacky.push(2);
          stacky.push(3);
          stacky.pop();
          // [?] What gets printed here?
          System.out.println(stacky.peek());
      }
      
  }

What's wrong with our inheritance-structured implementation of

2 main issues:

  • Since our IntStack inherits from IntLinkedList, we still have access to the methods that IntStack does not overload, possibly violating our FILO guarantee if we can access / change elements other than the top.

  • It doesn't seem particularly DRY if we're just borrowing the LinkedList's implementation in separate method definitions.

To illustrate problem 1: note that the following still compiles and runs:

  package intlist;

  public class IntStackTests {
  
      public static void main (String[] args) {
          IntStack stacky = new IntStack();
          // A stack with prepend?! Odd, but kinda OK...
          stacky.prepend(1);
          stacky.push(2);
          stacky.push(3);
          // A stack with removeAt?!?! Not OK!
          stacky.removeAt(1);
          System.out.println(stacky.peek());
      }
      
  }

Why is it, based on the operations we described for a Stack above, not OK for one to support the removeAt(index) method?

This would violate the FILO guarantee that Stacks are meant to provide, since a value that may have been inserted earlier may be removed before one that was inserted later. This could introduce errors when some user of a Stack needs that guarantee to be honored.

Sometimes we desire this flexibility, and is how the Java Stack is implemented (see JavaDocs and section below), but other times, we want to ensure that only those methods that we want exposed are available to the user.

What tool that we've learned about can help us restrict a Stack's operations to only those intended, even if we've implemented it using a LinkedList?

Create an Interface that removes the undesired methods!

To thus limit our IntStacks to only pop, push, and peek, we could define a new interface that IntStack implements:

  package intlist;
	
  public interface TrueIntStack {
  
      public void push (int toPush);
      public void pop ();
      public int peek ();
	
  }
	
  // [!] Not shown: IntStack implementing TrueIntStack

Of course, implementing an interface merely specifies the minimal methods that must be implemented, and does not inherently prevent others (including those inherited) from being made available as well.

To restrict IntStacks to these 3 methods, we would change our tests such that:

  package intlist;

  public class IntStackTests {
  
      public static void main (String[] args) {
          // [!] Note new declared "type" specifies the interface,
          // which is OK because the constructor on the RHS is an
          // object whose class implements it
          TrueIntStack stacky = new IntStack();
          // [X] Now: this is a compilation error (as we wanted)
          stacky.prepend(1);
          stacky.push(2);
          stacky.push(3);
          // [X] This too is a compilation error (as we wanted)
          stacky.removeAt(0);
          System.out.println(stacky.peek());
      }
      
  }

A couple of parting remarks from this example:

A *better* approach (solving problem 2) for implementing a Stack: implement its methods in the IntLinkedList class, have IntLinkedList implement TrueIntStack, and then simply create objects using the TrueIntStack interface, e.g., TrueIntStack s = new IntLinkedList();

Note: we could have just as easily maintained a stack as an ArrayList, whereby we pushed and popped items from the back of the list. The choice to implement one as an array vs. linked list will depend heavily on the intended application.

Try implementing an IntStack above using our IntArrayList from the previous lectures; it'll be pretty simple as well!



Queues

Queues aren't a whole lot different from stacks, except that instead of pushing to and popping from the top, we queue to the back of the queue and dequeue from the front.

A queue is an ADT that maintains the FIFO (first in, first out) behavior of insertion and retrieval of its elements.

FIFO Intuition: The most recent thing we insert into a stack is the *last* thing we get back. Useful for certain scenarios wherein we care about the order in which items in a collection are stored.

There's a reason that "queues" in supermarkets and theme parks are named so; people enter them at the back, and then are served at the front!

What are some examples of scenarios in which a Queue might be useful?

  • Maintaining an ordered sequence of actions that some agent should carry out, like moving "Up", then "Right", then "Up" in a grid maze in sequence.

  • Maintaining some time-specified order of admittance, like order in which users of a site can begin ordering tickets or an online game's server / lobby.


Here's what that might look like as a singly linked list:

Again, though, you should be aware that a queue is an abstract data type, and that some implementations WILL allow you to access the intermediary nodes behind the front.

Reflect: why would an ArrayList *not* be as good of an implementation choice for a Queue as a LinkedList is?


Common Queue Functions:

  • push(toQueue) (sometimes called enqueue) adds an element to the back of the queue.

  • poll() (sometimes generally referred to as pop() or dequeue()) returns and removes the element from the front of the queue.

  • peek() returns a reference to the element at the front of the queue (least recently added)


Implementing a Queue is left as an exercise, since they're easily maintained with a LinkedList (which, coincidentally, will be one of your upcoming homeworks heh).



Java Collections Framework

The JCF Stack provides an implementation for FILO behavior using a Vector (basically, an ArrayList) (though also recommended is the Deque data structure, which we won't discuss herein).

The Stack interface can be viewed here.

  Stack<String> stacky = new Stack<>();
  stacky.add("a");
  stacky.add("b");
  stacky.add("c");
  // pop removes and returns top of stack
  stacky.pop();
  
  // peek returns top of stack
  // [?] What gets printed here?
  System.out.println(stacky.peek());

Warning: because all Queue methods are already implemented using LinkedLists, the Java Queue is simply an interface, not an instantiable class!

As such, we can either initialize a Queue as a Linked List or some other structures we have yet to learn about.

The Queue interface can be viewed here.

Queue being an interface allows us to specify how we want that to be done, as long as certain implementations define the Queue interface's methods (in the link above) for example:

  // Queue (Interface) implemented with LinkedList
  Queue<String> q = new LinkedList<String>();
  // add pushes an item to the back of the Queue
  q.add("a");
  q.add("b");
  q.add("c");
  // poll dequeues and returns item at front
  q.poll();
  
  // peek returns item at front
  // [?] What gets printed here?
  System.out.println(q.peek()); 


  PDF / Print