20: Building queues
Announcements
Quiz in our next discussion (the Monday right after break). “I got back late” is not an acceptable excuse for missing the quiz.
There will be an assignment due the week after break, and the week after that. No work due during break, of course (I view it just like any other weekend; up to you if you want to to be working then or not).
SRTI (“Student Response to Instruction”) assessments for this course will be online this semester. You’ll get an email about this sometime soon, and I’ll remind you, but it’s really important that you fill them out, good or bad. Instructors and departments all use this data to help improve teaching and the curriculum. I read all of them carefully, so please give me useful feedback – either about me, about the course, or both. Don’t worry, you’ll be hearing about this again once they’re available.
Implementing queues using linked lists
Recall to implement a stack with linked lists, we used just a head pointer. This made sense, because we always manipulated the same end of the list.
To review, to add to the head of the list, we did the following (review on board):
- create a new node
- set its next reference to the current head
- set head to the new node
To remove the head node, we did the following:
- remember the head node’s data
- set head to it’s next reference (now there are no refs into the former head node, so it’s deleted)
Queues have to manipulate both ends of the list, so we’ll need a head and tail pointer. Should we enqueue at the head or the tail? And where should we dequeue?
We know that both adding and removing at the head are pretty straightforward, since that’s what we did with push/pop for stacks. What about adding or removing at the tail?
(On board). Suppose we keep a “tail” pointer which always points at the last node in the list. To add, we:
- create a new node
- set tail node’s next point to this new node
- update tail to point at the new node
How do remove? We need a pointer to the node before the tail…we’d have to either forward and backward pointers in the list (which then is a doublely-linked list, which makes all other methods more difficult), or traverse to the node-before:
- traverse to node-before, keep reference to it
- remember the tail node’s data
- set tail to the node-before
What have we learned? Adding at the tail is trivial, but removing the tail in a singly-linked list is not (linear to seek to the node before the tail). So let’s enqueue onto the tail, and dequeue from the head.
What does the code look like?
public class LinkedQueue<E> {
Node<E> head;
Node<E> tail;
public LinkedQueue() {
head = null;
tail = null; // d'oh
}
...
When we enqueue, we make a new node and add it to the rear. Special case: if the queue is empty, our new node will be both the head and the tail. (on board)
public void add(E e) {
Node<E> node = new Node<>();
node.data = e;
if (head == null) { // or size == 0: the queue is empty
head = node;
} else {
tail.next = node;
}
tail = node;
}
In-class exercise
public void add(E e) {
Node<E> node = new Node<>();
node.data = e;
if (head == null) { // or size == 0: the queue is empty
head = node;
} else {
tail.next = node;
}
tail = tail.next;
}
I changed the last line. What’s broken (if anything)?
Back to the Queue
Dequeuing is similar. We remove the head node from the list and return its value. The corresponding special case is if the node is the last node; if so, we need to remember to also set tail to null. (on board)
public E remove() {
E value = head.data;
head = head.next;
if (head == null) {// or size == 0, or whatev
tail = null;
}
return value;
}
Implementing queues using arrays
Suppose we want to use arrays to back our queue, like we did with stacks. With stacks, we fixed the element at index 0 as the “bottom” of the stack, and maintained a top pointer. What if we do this with queues?
(on board)
We can easily enqueue elements by keeping a pointer to the last element enqueued. But what about dequeuing? We’d have to remove the element at position zero, then move the element from position 1 into 0, 2 in to 1, and so on. That’s linear in the size of the queue (and thus terrible).
The right thing to do parallels the linked list implementation. Instead of a head and tail pointer, we’ll maintain two indices into the array, and allow both the front and rear of the queue to move through the array.
There is a trick, though: we need to now thing of the array as a circle, not as a linear array, that “wraps around”. This is kind of like a clock: (on board)
Now, instead of incrementing our front/rear indices, we use front = (front + 1) % capacity)
– this is modular arithmetic, and “wraps around” exactly as we want.
We’ll define rear
as the last occupied index (and initialize to what? virtual position “-1” == capacity - 1). Thus the next place to enqueue is rear + 1
. We’ll define front
as the next occupied index to dequeue
and initialize it to 0.
We have to keep track of the size
, since both an empty queue and a full queue have rear just before front.
public class CircularQueue<E> {
E[] queue;
int front;
int rear;
int size;
public CircularQueue(int initialCapacity) {
queue = (E[]) new Object[initialCapacity];
size = 0;
front = 0;
rear = initialCapacity - 1; // or queue.length - 1 <- probably better
}
public boolean isFull() {
return size == queue.length;
}
public boolean isEmpty() {
return size == 0;
}
public void add(E e) {
// if full, throw exception...
rear = (rear + 1) % queue.length;
queue[rear] = e;
size++;
}
public E remove() {
// if empty...
E value = queue[front];
queue[front] = null;
front = (front + 1) % queue.length;
size--;
return value;
}
In-class exercise
Can we compute the size
of the queue? If so, how?
Do we really have to track size
in an instance variable? No. It turns out we can compute it, so long as we flag empty spaces with null (and thus don’t allow null
to be stored in the queue.)
public int size() {
if (queue[head] == null) {
return 0;
}
else if ((queue.length + rear - front + 1) % queue.length == 0) {
return queue.length;
}
else {
return (queue.length + rear - front + 1) % queue.length;
}
}
Usually we prefer to constant-cost compute properties like size()
rather than track them when modified, but in this case it’s a question of taste/style as to which is more straightforward.
Is the above clearer than the use of an explicit size
counter? Debatable. Is it less error-prone to implement? Debatable. Use your judgment, and develop it by reading others’ code. Will you ever do this in real life? Only on whiteboard coding interviews; but these are a very specific niche that not everyone should stress out about.