First Part
Learning objectives
- Understanding of queue-based algorithms
- Understanding array-based and linked list queue implementations
Introduction
We all know of many applications of queues in “real-world” situations. A prime example are the waiting lines. In order to improve customer service, techniques are sought to reduce the average waiting time, or the average length of the queue. For airlines companies, this could be to have waiting lines for regular customers, as well as waiting lines for the frequent flyers. For a supermarket, this would the use of express lines for customers with few items, as well as regular waiting lines.
However, in order to select the best strategy, e.g. adding 1, 2 or 3 express lines, managers need tools to estimate the value of certain parameters, such as the average waiting time, so as to implement cost effective solutions.
The are two main approaches to obtain these estimates. Queuing theory is a branch of mathematics that studies waiting lines. The alternative is to build computer simulations of these “real-world” systems, and measure the values of the parameters with help of the simulations.
For this laboratory, we are developing a computer simulation for a supermarket that has express and regular lines. These waiting lines will be implemented with the help of queues.
Implementing a queue
First of all, make sure that you understand the implementation of a queue. We will start by implementing the queue using an array.
The methods
public interface Queue<E> {
public abstract void enqueue( E obj );
public abstract E dequeue();
public abstract boolean isEmpty();
public abstract int size();
}
- void enqueue: Add an element to the back of the queue
- E dequeue: Remove and return the first element of the queue
- boolean isEmpty: Returns true if the queue is empty, false otherwise
- int size: Returns the number of elements in the queue
Implementing a queue with an array
A queue always has to follow the "first in first out" (FIFO) algorithm. You can see that the implementation ArrayQueue uses front to represent the index of the first element in the queue (the next one to be removed) as well as rear to represent the index of the last element of the queue (-1 if the queue is empty). The variable size allows us to track the actual size of the queue (don’t forget that elems.length is not the same as size unless the queue is full!). Finally, it is in the array E elems[] that the elements of the queue will be saved.
1 public class ArrayQueue<E> implements Queue<E> {
2
3 // Constant
4
5 private static final int MAX_QUEUE_SIZE = 10000;
6
7 // Instance variables
8
9 private E[] elems; // stores the elements of this queue
10 private int front, rear, size;
11
12 @SuppressWarnings( "unchecked" )
13
14 public ArrayQueue() {
15 elems = (E []) new Object[MAX_QUEUE_SIZE];
16 front = 0;
17 rear = -1;
18 size = 0;
19 }
20
21 // Instance methods
22
23 public int size() {
24 return size;
25 }
26
27 public boolean isEmpty() {
28 return size == 0;
29 }
30
31 public boolean isFull() {
32 return size == MAX_QUEUE_SIZE;
33 }
34
35 public void enqueue( E elem ) {
36
37 // pre-condition: ???
38
39 if ( rear == ( MAX_QUEUE_SIZE -1 ) ) {
40
41 int j=0;
42 for ( int i=front; i<=rear; i++ ) {
43 elems[ j++ ] = elems[ i ];
44 }
45
46 front = 0;
47 rear = size - 1;
48
49 }
50
51 elems[ ++rear ] = elem;
52 size++;
53 }
54
55 public E dequeue() {
56
57 // pre-condition: ???
58
59 E saved = elems[ front ];
60 elems[ front ] = null; // scrubbing the memory!
61
62 front++;
63 size--;
64
65 return saved;
66 }
67
68 }
Note: when we remove the element at the position front in the method dequeue, we assign the value null to this element and increment front so that the next element becomes the first element of the queue. This causes a shift in the array where the first elements are null up to the position front. When the array elems[] with which we implement our queue is full, it is probable that there is in fact some available space in the first elements of the array because of a call to the method dequeue. The lines 39 to 49 allow us to shift the array to the left in the case where the variable rear has reached the end of the array.
Take some time to determinate the precondition to each method.
Implementing a queue with linked elements.
public class LinkedQueue<E> implements Queue<E> {
private static class Elem<T> {
private T value;
private Elem<T> next;
private Elem(T value, Elem<T> next) {
this.value = value;
this.next = next;
}
}
private Elem<E> front;
private Elem<E> rear;
private int size;
public LinkedQueue() {
size = 0;
}
public int size() {
return size;
}
public void enqueue(E value) {
Elem<E> newElem;
newElem = new Elem<E>(value, null );
if (rear == null) {
front = rear = newElem;
} else {
rear.next = newElem;
rear = newElem;
}
size++;
}
public E dequeue() {
E result = front.value;
if (front != null && front.next == null) {
front = rear = null;
} else {
front = front.next;
}
size--;
return result;
}
public boolean isEmpty() {
return front == null;
}
}
This implementation is quite different from the previous one. This time we use 3 instance variables, Elem front, Elem rear and int size. What is an object of type Elem? It comes from the static private nested class Elem in which we save the actual value of the element in the queue (a generic type T) as well as a reference to the next element in the queue. This way, front is a reference to the first Elem of the queue while rear is the last Elem of the queue.
Some questions to consider:
- In what situation would front == rear?
- Can the queue be full?
- Without the variable size, how could we determinate the size of the queue?