CS-151 Labs > Lab 5. Stack and Queue


Warmup

In the main part of the lab, you’re going to be using a Linked List idea to implement both Stack and Queue Data Types. The sole purpose of this list would be to model a queue - either LIFO queue (Stack) or FIFO queue (Queue). In the warmup we’re going to write several methods for this very minimalistic Linked List class. Unlike a fully-functional doubly-linked list implemented in the previous lab, this will be a singly-linked list, and it will be a conventional list - without sentinel nodes.

Node

In Eclipse, create a new Java Project called lab5. Create a new class MiniList and package it in the lab5 package. Yes, package it directly in the lab5 package because we will be using this class in the main part of the lab.

We want our MiniList class to be able to hold any given type of data, so change

public class MiniList

to

public class MiniList<E>

Recall that a singly-linked list is fully defined by its head node. Each node is a simple structure that contains a data element and a link to the next node in the sequence.

Inside your MiniList class, add a new, private inner class called Node.

public class MiniList<E> {
    private class Node {
    }
}

Add two public instance variables E data and Node next inside the Node class. The instance variables are public so the methods of the MiniList class can access them directly. That is, if we can create an instance variable node of type Node, then we can access node.data and node.next directly. But because Node is a private class, only MiniList itself will have access to its instance variables, and no outside class can even create an instance of the Node type.

Notice that MiniList is parameratized by E but that Node is not. Node doesn’t need to be parametrized because it uses the same type E as defined in the outer class.

Add a public constructor public Node(E data, Node next) that sets the this.data and this.next fields to data and next.

The next field of the Node class is a reference to the next node. As we already know, this reference can also be null. We could create three nodes and chain them together like this:

Node thirdNode = new Node("third", null);
Node secondNode = new Node("second", thirdNode);
Node firstNode = new Node("first", secondNode);

Notice that

That null in thirdNode.next is really important: that’s how we know it’s the last element in the list. We use the next fields to walk down the list, one element at a time using a loop.

Finally, at the end of the Node class, add the following method which will be used for printing the Node’s data:

public class MiniList<E> {
    private class Node {
        ...
        public String toString(){
            return this.data.toString();
        }
    }
}

Simple Linked List

Start by adding a private Node head instance variable to the MiniList class. For right now, our Linked List is just going to contain a reference variable (head) that is pointing to the first node in the list. The rest of the list can be reconstructed via the head’s next field.

Create a public MiniList() constructor that sets this.head to null. Just as we use null for a Node’s next to indicate that there are no more nodes in the list, we use null for head to indicate that our list contains no elements at all.

Speaking of a list having no elements, go ahead and write a method

public boolean isEmpty() {
    // Code here
}

that returns true if the list contains no elements.

Let’s check that when we create a new list, we get an empty list. Normally, we’d use a JUnit test for this and when you write your stack/queue implementations, you’ll definitely want to write such a test. For now, make a main() method that tests that we can construct an empty list. Something like this:

public static void main(String[] args) {
    MiniList<String> list = new MiniList<String>();
    System.out.println(list.isEmpty());
}

Run your code. It should print true.

Now it’s time to implement add and remove. For this lab we are only interested in adding/removing elements to the end/from the beginning of the list.

Let’s start by writing a method public void addFirst(E element) that inserts element at the beginning of the list. This is the simplest method. We want to create a new node whose data is element and whose next is this.head. Then, we want to set this.head to that node. Notice that one of two things will happen. If the list is empty, then this.head is null so this.head will be set to a Node whose next is null. Otherwise, the list is nonempty and this.head points to the first node in the list. By creating a new Node whose next is the current this.head and setting this.head to it, what was the first node in the list is now the second.

Modify your main() method to add a few strings to list using list.addFirst(). Now, isEmpty() had better return false.

Walking and printing

So far, we can add nodes contaning elements to the beginning of our list. Let’s add a private method printList() that will walk through all of the elements and print all the data elements in sequence. We will only use this for debugging purposes.

How can we do that? We know that we can use this.head.data to get the data of the first node in the list and this.head.next.data to get the data of the second node in the list and this.head.next.next.data to get the data of the third node in the list and so on. We can use a while loop to print the entire sequence.

Node node = this.head;
while (node != null) {
    // do something with the node
    System.out.println(node);
    node = node.next;
}

What’s happening here is that node is initially set to the first element of the list (or null if the list is empty). Then, at the end of each iteration of the loop, node is set to the next node in the list. We used null to recognize that we’ve reached the end of the list.

We can actually do the same thing slightly more concisely using a for loop!


for (Node node = this.head; node != null; node = node.next) {
    // do someting with the node
    System.out.println(node);
}

Which one you use is often a matter of personal preference.

Using any loop, implement the private printList() method, and have it print out an index to go with each element. E.g., something like

0: red
1: blue
2: yellow

where red, blue, and yellow were the the String elements added to the list.

In main add some elements to the list.

list.addFirst("red");
list.addFirst("blue");
list.addFirst("yellow");

Add a call list.printList() and see what it prints out. Did it match your expectations?

Removing from the front of the list

Now that we can add to the front of the list, it’s time to implement removal from it as well. Write a method public E removeFirst() that removes the first node of the list and returns its data.

There are two cases we need to handle. First, if the list is empty (which we can check with this.isEmpty()), then there’s nothing we can return. Instead, throw a new NoSuchElementException. You’ll need to import it first using

import java.util.NoSuchElementException;

below the package lab5; line.

Otherwise, the list is nonempty. The data we want to return is in this.head.data and we’ll need to set this.head to this.head.next. You’ll need to put this.head.data in a variable before you move this.head.

In main(), add a loop to remove elements from the list and print them out one at a time until the list is empty. You’ll probably want something like while (!list.isEmpty()).

Adding to the end of the list

Adding elements to and removing elements from the beginning of the list was pretty easy. We only needed to deal with a single first Node.

In contrast, adding to and removing from the end of the list as we have currently set it up is more tricky.

Implement public void addLast(E element). There are two cases. If the list is empty, then add the element to the beginning of the list and return. (In fact, you can just call this.addFirst(element)!) Otherwise, you’ll need to walk down the list, starting from this.head until you reach the node where the next is null. This indicates you’ve found the last real node of the list and you can set node.next = new Node(element, null);.

To walk down the list, it’s very similar to what you did to print out the list, except this time you want to stop when node.next is null rather than when node itself is null. The loop might look something like this:

// first check if the list is empty - and call addFirst
Node node = this.head;      //current node - not null

while (node.next != null) {     
    node = node.next;
}
node.next = new Node(element, null);

In main, try adding some elements to the beginning and end of the list and print out the results. E.g.,

list.addLast("are");
list.addFirst("lists");
list.addLast("neat!");
list.addFirst("Linked");
list.printList();

should print

0: Linked
1: lists
2: are
3: neat!

Now comes the hardest part: removing the last element. To remove the last element, we’ll need to handle several cases.

  1. If the list is empty, throw a new NoSuchElementException.
  2. If the list has only a single node (i.e., if this.head.next is null), return this.removeFirst(). (You already wrote that code after all, no need to duplicate it.)
  3. If the list has more than one node, then you’ll need to walk down the list, starting from this.head until you get to the second to last node in the list, remove the last node by setting the second to last node’s next to null, and then returning the data from the node you removed.

It’s that last step that can be tricky. Since we checked if the list was empty or only had a single element and handled those cases, we can assume that this.head.next is not null. (Why is that true? What would it mean if it were null?) So now we can find the second to last node via

Node node = this.head;
while (node.next.next != null) {
    node = node.next;
}

After this, the data we want to return is in node.next.data and we need to set node.next to null.

Implement public E removeLast() using the algorithm described above.

Make changes to your main() method to test out your new method. Make sure that after removing the very last element, list.isEmpty() returns true.

Fast vs. slow

Take a look at your addFirst(), removeFirst(), addLast(), and removeLast() methods. Notice that addLast() and removeLast() require looping over the whole list whereas addFirst() and removeFirst() do not. Think about what that means if our list contains 1 node, 100 nodes, or 10000 nodes. Adding to or removing from the front of the list takes the same time regardless of how much data is in the list. In contrast, adding/removing the last node takes roughly 100 times more work for 100 nodes than 1 node and another 100 times more work for 10000 nodes than for 100 nodes.

That’s not great, but we can do better! We can maintain a pointer (a link) to the last, or tail, node in the list. Let’s go through the “easy” changes first.

In each of those changes, we updated the method to maintain the invariant (a property that is always true) that this.tail is null if and only if this.head is null. I.e., either both are null which happens when we have an empty list or neither are null.

Now, we can simplify addLast(). Currently, the method should check if the list is empty and if so, just call addFirst() and return. Otherwise, it loops through until it can find the last node in the list. Well now, we don’t need to do any looping! We already know what the last node in the list is. It’s this.tail.

Remove the loop and in its place, set this.tail.next to new Node(element, null) and then this.tail to this.tail.next.

We’ve made all of our methods (except for isEmpty()) a little more complicated, but in return, we were able to change addLast() so that it doesn’t have to loop through the whole list. That’s a pretty good tradeoff.

Finally, removeLast(). Can we do something similar here? Unfortunately, no. For addLast(), we needed to update the current last node in the list’s next. In order to remove the last node, we’d need to update the second to last node’s next. So could we maintain last node and second-to-last node pointers in our list? Again, no. We’d need to know the third to last node in the list. You can see how this simply cannot work. We would need a way to get from a node to the previous node in the list. We could do that, but we’d need to maintain two links. And we want to have a very simple linked structure for this lab!

Stacks and Queues

At this point, we have fast methods for inserting elements at the beginning and end of a simple linked list and we have a fast method for removing elements from the beginning of our linked list.

If we only ever add elements to the beginning of our list and remove them from the beginning of the list, which abstract data type have we just implemented? Give it a shot:

MiniList<String> list = new MiniList<String>();

list.addFirst("one");
list.addFirst("two");
list.addFirst("three");
while (!list.isEmpty()) {
    System.out.println(list.removeFirst());
}

If we only ever add elements to the end of our list and remove them from the beginning of the list, which abstract data type have we just implemented? Give this a shot.

MiniList<String> list = new MiniList<String>();

list.addLast("one");
list.addLast("two");
list.addLast("three");
while (!list.isEmpty()) {
    System.out.println(list.removeFirst());
}

This implementation of MiniList is going to be used for implementing Stack and Queue ADTs in the main part of the lab.