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
firstNode.next
issecondNode
;firstNode.next.next
andsecondNode.next
arethirdNode
(this is true becausefirstNode.next
issecondNode
, sofirstNode.next.next
is literally the same thing assecondNode.next
); andfirstNode.next.next.next
,secondNode.next.next
, andthirdNode.next
arenull
.
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.
- If the list is empty, throw a new
NoSuchElementException
. - If the list has only a single node (i.e., if
this.head.next
isnull
), returnthis.removeFirst()
. (You already wrote that code after all, no need to duplicate it.) - 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’snext
tonull
, and then returning thedata
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.
- Add a new
private Node tail
to theMiniList
class. - Update the constructor to set
tail
tonull
. - Add to the end of
addFirst()
an if statement to check ifthis.tail
isnull
. If it is (which will happen when this is the first node we are adding to the list), setthis.tail
tothis.head
. I.e.,this.head
andthis.tail
should point to the same node when there is only one node in the list. - Just before the
return
inremoveFirst()
, check ifthis.head
isnull
and if it is (which will happen when we remove the last node in the list), setthis.tail
tonull
.
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.