A bstract D ata T ypeThe List and Set abstractions from the collections framework are both ADTs.
In computer science, two of the most fundamental ADTs are called stacks and queues. They are so simple that they almost seem not worth studying. They are like the programming equivalent of drawers and shelves. Drawers and shelves are very simple and, therefore, sort of boring, and yet we find uses for them everywhere we turn.
It is useful to study stacks and queues as a way to understand a minimal kind of data structure. We'll find, for example, that they are less powerful than the list structures we have been looking at. But we often find ourselves wanting to think in terms of the simplest possible solution to a problem, as in, "You could solve that with a stack."
Like lists, stacks and queues store an ordered sequence of values. A minimal set of operations for such a structure would require at least:
Stacks and queues are similar in that they each store a sequence of values in a particular order. But stacks are what we call LIFO structures while queues are FIFO structures:
stacks queues L-ast F-irst I-n I-n F-irst F-irst O-ut O-utThe analogy for stacks is to think of a cafeteria and how trays are stacked up. When you go to get a tray, you take the one on the top of the stack. You don't bother to try to get the one on the bottom, because you'd have to move a lot of trays to get to it. Similarly if someone brings clean trays to add to the stack, they are added on the top rather than on the bottom. The result is that stacks tend to reverse things. Each new value goes to the top of the stack, and when we take them back out, we draw from the top, so they come back out in reverse order.
The analogy for queues is to think about standing in line at the grocery store. As new people arrive, they are told to go to the back of the line. When the store is ready to help another customer, the person at the front of the line is helped. In fact, the British use the word "queue" the way we use the word "line" telling people to "queue up" or to "go to the back of the queue".
In the case of a stack, the adding operation is called "push" and the removing operation is called "pop". All operations occur at one end of the stack, at the top. We push values onto the top and we pop them off the top. There is also a method for testing whether the stack is empty and an operation for requesting the current size of the stack. So for a Stack<E>, the basic operations are:
public void push(E value); public E pop(); public boolean isEmpty(); public int size();Notice that we are using Java generics to define the Stack in terms of an unspecified element type E. That way we'll be able to have a Stack<String> or Stack<Integer> or a Stack of any other kind of element type we are interested in.
For queues, we have a corresponding set of operations but they have different names. The operations for a Queue<E> are:
public void add(E value); public E remove(); public boolean isEmpty(); public int size();The collections framework does the right thing in terms of Queue<E> by making it an interface. The implementation we will be using for actual objects is LinkedList<E>. The stack version is much older and was not done as well. In particular, Stack<E> is a class, not an interface.
Even though we are using the standard Java stack and queue classes, I am limiting what operations you can use with them to those listed above.
We looked at this program that does some simple manipulations on a stack and queue:
import java.util.*; public class SimpleStackQueue { public static void main(String[] args) { String[] data = {"four", "score", "and", "seven", "years", "ago"}; Queue<String> q = new LinkedList<>(); Stack<String> s = new Stack<>(); for (String str : data) { q.add(str); s.push(str); } System.out.println("initial queue = " + q); while (!q.isEmpty()) { String str = q.remove(); System.out.println("removing " + str + ", now queue = " + q); } System.out.println(); System.out.println("initial stack = " + s); while (!s.isEmpty()) { String str = s.pop(); System.out.println("removing " + str + ", now stack = " + s); } } }It produced the following output:
initial queue = [four, score, and, seven, years, ago] removing four, now queue = [score, and, seven, years, ago] removing score, now queue = [and, seven, years, ago] removing and, now queue = [seven, years, ago] removing seven, now queue = [years, ago] removing years, now queue = [ago] removing ago, now queue = [] initial stack = [four, score, and, seven, years, ago] removing ago, now stack = [four, score, and, seven, years] removing years, now stack = [four, score, and, seven] removing seven, now stack = [four, score, and] removing and, now stack = [four, score] removing score, now stack = [four] removing four, now stack = []As we expected, the queue values came out in the same order as the array but the stack values came out in reverse order.
Then we spent time writing some client code to manipulate a queue. I said that we would use a queue of int values. So we wrote this line of code to construct one:
Queue<Integer> q = new LinkedList<>();Notice that we use the interface for defining the variable. We only use the name of the implementation when we are calling new. All variables, parameters and return types should be defined using the interface, as we discussed on Monday.
Because the implementation includes a toString method, we can print the queue:
System.out.println("q = " + q);I asked people to help me write some code that would add 6 multiples of 3 to the queue.
Queue<Integer> q = new LinkedList<>(); for (int i = 1; i <= 6; i++) { q.add(i * 3); } System.out.println("q = " + q);which produced the following output:
q = [3, 6, 9, 12, 15, 18]Then we explored how to put this code into a method. I suggested that we have a parameters indicating how many multiples to add to the queue and what number to find multiples of. We found that the return type for the method should be Queue<Integer>:
public static Queue<Integer> makeQueueOfMultiples(int count, int n) { Queue<Integer> q = new LinkedList<>(); for (int i = 1; i <= count; i++) { q.add(i * n); } return q; }We then changed main to call this method and print the result:
Queue<Integer> q = makeQueueOfMultiples(6, 3); System.out.println("q = " + q);I then said that I wanted to work with a stack as well, so I included this line of code:
Stack<Integer> s = new Stack<>();I said that I wanted to write a method that would transfer values from the queue to the stack. We need a loop that will remove values from the queue as long as there are more values left to remove. We can accomplish this with a while loop and the isEmpty and remove methods:
public static void queueToStack(Queue<Integer> q, Stack<Integer> s) { while (!q.isEmpty()) { int n = q.remove(); ... } }Notice that the parameters are of type Queue<Integer> and Stack<Integer>, using the Queue interface rather than the LinkedList implementation (we have no choice for Stack because it is not an interface).
This code removed things from the queue, but to add them to the stack, we have to include a call on push inside the loop. At first I showed this incorrect version:
public static void queueToStack(Queue<Integer> q, Stack<Integer> s) { while (!q.isEmpty()) { int n = q.remove(); s.push(q.remove()); } }The problem with this version is that it calls remove twice each time through the loop. It should be calling push with the value of n. Someone pointed out that we could eliminate the line that involves n altogether and have just the second line of code, which is true. But I changed it to work with n:
public static void queueToStack(Queue<Integer> q, Stack<Integer> s) { while (!q.isEmpty()) { int n = q.remove(); s.push(n); } }I added some code to main to report what was in the stack and queue after calling queueToStack:
Queue<Integer> q = makeQueueOfMultiples(6, 3); System.out.println("q= " + q); Stack<Integer> s = new Stack<>(); queueToStack(q, s); System.out.println("after queueToStack:"); System.out.println(" queue = " + q); System.out.println(" stack = " + s);It produced the following output:
q = [3, 6, 9, 12, 15, 18] after queueToStack: queue = [] stack = [3, 6, 9, 12, 15, 18]Then I asked people how we could write a method that would find the sum of the values in this queue. It is a cumulative sum task, which involves initializing a sum variable to 0 outside the loop and then adding each value to the sum as we progress through the loop. Our first attempt looked like this:
public static int sum(Queue<Integer> q) { int sum = 0; while (!q.isEmpty()) { int n = q.remove(); sum += n; } return sum; }When we called the method from main and printed the queue afterwards, we found that the queue is empty. As a side effect of calculating the sum, we destroyed the contents of the queue. This is generally not acceptable behavior.
Unfortunately, queues don't give us any peeking operations. We have no choice but to take things out of the queue. But we can restore the queue to its original form. How? Someone suggested using a second queue. That would work, but there is an easier way. Why not use the queue itself? As we remove values to be processed, we re-add them at the end of the list. Of course, then the queue never becomes empty. So instead of a while loop looking for an empty queue, we wrote a for loop using the size of the queue:
public static int sum(Queue<Integer> q) { int sum = 0; for (int i = 0; i < q.size(); i++) { int n = q.remove(); sum += n; q.add(n); } return sum; }By printing the queue before and after a call on sum, we were able to verify that our new version preserved the queue.
Then I created a variation of makeQueueOfMultiples that I called makeStackOfMultiples for making a stack of multiples of some number n. This was a fairly straightforward modification where we simply switched queue operations with stack operations:
public static Stack<Integer> makeStackOfMultiples(int count, int n) { Stack<Integer> s = new Stack<>(); for (int i = 1; i <= count; i++) s.push(i * n); return s; }In this case I had to use a different name because otherwise the signatures would match (both with a single parameter of type int). In Java, the return type is not part of the signature.
In place of queueToStack, we wrote a new method stackToQueue:
public static void stackToQueue(Stack<Integer> s, Queue<Integer> q) { while (!s.isEmpty()) { int n = s.pop(); q.add(n); } }I then said I wanted to consider a variation of the sum method for stacks:
public static int sum(Stack<Integer> s) { ... }This method can also be called sum because the two methods have different signatures. Remember that a signature of a method is its name plus its parameters. These are both called sum and they both have just a single parameter, but the parameter types are different, so this is okay. This is called overloading a method and it is a common technique in Java.
So how do we write the sum method for stacks? At first I did a similar modification where I simply substituted stack operations for queue operations:
int sum = 0; for (int i = 0; i < s.size(); i++) { int n = s.pop(); sum += n; s.push(n); } return sum;Unfortunately, this code didn't work. We had constructed a stack with 6 multiples of 3 and when we tested it, we saw this output:
stack = [3, 6, 9, 12, 15, 18] sum = 108The sum of these numbers is not 108. We're getting that sum because the loop pops the value 18 off the stack 6 different times and then pushes it back onto the top of the stack. With a queue, values go in at one end and come out the other end. But with a stack, all the action is at one end of the structure (the top). So this approach isn't going to work.
In fact, you can't solve this in a simple way with just a stack. You'd need something extra like an auxiliary structure. I said to consider how we could solve it if we had a queue available as auxiliary storage. Then we can put things into the queue as we take them out of the stack and after we have computed the sum, we can transfer things from the queue back to the stack using our queueToStack method:
int sum = 0; Queue<Integer> q = new LinkedList<>(); for (int i = 0; i < s.size(); i++) { int n = s.pop(); sum += n; q.add(n); } queueToStack(q, s); return sum;This also didn't work:
stack = [3, 6, 9, 12, 15, 18] sum = 45 after sum stack = [3, 6, 9, 18, 15, 12]There are two problems here. Only half of the values were removed from the stack and those values now appear in reverse order. Why only half? We are using a for loop that compares a variable i against the size of the stack. The variable i is going up by one while the size is going down by one every time. The result is that halfway through the process, i is large enough relative to size to stop the loop. This is a case where we want a while loop instead of a for loop:
int sum = 0; Queue<Integer> q = new LinkedList<>(); while (!s.isEmpty()) { int n = s.pop(); sum += n; q.add(n); } queueToStack(q, s); return sum;Even this is not correct. It finds the right sum, but it ends up reversing the values in the stack. Some people said, "Then why don't you use a stack for auxiliary storage?" That would solve the problem, but one of the things we are testing is whether you can figure out how to solve a problem like this given a certain set of tools. In this case, you are given auxiliary storage in the form of a queue.
The problem is that by transferring the data from the stack into the queue and then back into the stack, we have reversed the order. The fix is to do it again so that it goes back to the original. So we add two extra calls at the end of the method that move values from the stack back into the queue and then from the queue back into the stack:
public static int sum(Stack<Integer> s) { int sum = 0; Queue<Integer> q = new LinkedList<>(); while (!s.isEmpty()) { int n = s.pop(); sum += n; q.add(n); } queueToStack(q, s); stackToQueue(s, q); queueToStack(q, s); return sum; }This is the correct version of the method.