Introduction
Abstract Data Type
Described abstractly, a queue is
- a finite collection of elements,
- in a particular order,
- that may contain the same element multiple times.
The fact that it may contain the same element multiple times makes it different from a set, the fact that it is ordered makes it different from a multiset.
Generally, it has operations to…
- … create an empty queue,
- … test for emptiness,
- … obtain the value of the element at “the end” of the queue (“peek”),
- … add an element at “the beginning” of the queue (“enqueue”),
- … remove an element at “the end” of the queue (“dequeue”).
The fact that only the “last element” can be removed and that elements can only be added “at the front” is the main difference with the list abstract data type. Exactly like a people waiting in line for a service, queues implement a ” first-in-first-out” (FIFO) principle.
Difference with array
Like lists, queues serve a similar purpose than arrays, but with a few notable differences:
- Queues do not need to have a number of elements fixed ahead of time,
- Queues automatically expand when elements are added,
- Queues automatically shrink when elements are removed.
Difference with lists
However, queues have differences with lists:
- Only the “last” element’s value can be read (accessed),
- Adding an element can only be done “on the left side”, that is, at the beginning of the queue.
Possible Implementation
Using Cells
One could implement queue by adapting our CList
class, making sure
that the read, remove and insert operations are limited to the “valid”
elements.
Using Arrays
One could implement queue by using arrays, adding elements “at the end” of the array and removing them “from the beginning”. This implementation has one major drawback, however: in this implementation, enqueue operation is , but dequeue is , since one need to “shift” all the elements by one after the first one has been removed.
An alternative is to use a circular array, where the beginning and the
end of the array are “glued together”. This requires to manipulate three
int
variables:
front
, pointing to the first element,end
, pointing to the last element,- and
size
, the number of elements in the queue.
An attribute mArray
then contain the elements in the queue, not
necessarily in the “right” order, since as the queue is dequeued, its
“front” moves.
using System; // This is required for the exception.
class CQueue<T>
{
private int front,
end,
size;
private T[] mArray;
public CQueue(int capacity = 10)
{
mArray = new T[capacity];
// Note that front, end and size
// are set to 0.
}
public void Clear()
{
front = 0;
end = 0;
size = 0;
}
public bool IsEmpty()
{
return size == 0;
}
public bool IsFull()
{
return size == mArray.Length;
}
public void Enqueue(T dataP)
{
if (!IsFull())
{
mArray[end] = dataP;
Increment(ref end);
size++;
}
else
Resize();
}
public T Dequeue()
{
size--;
T data = mArray[front];
Increment(ref front);
return data;
}
public void Resize()
{
throw new NotImplementedException(
"Queue Resize is not implemented."
);
}
// Increment must be done carefully:
// what if we reached the "end of mArray"
// but there is room available at the
// beginning of the queue? Then the value
// needs to become 0.
private void Increment(ref int value)
{
value++;
if (value == mArray.Length)
{
value = 0;
}
// This if statement could be replaced
// with
// value %= mArray.Length;
}
public int Capacity
{
get { return mArray.Length - size; }
}
public override string ToString()
{
string returned = "";
returned +=
$"Front : {front}, end : {end}, size : {size}, capacity: {Capacity}\n";
// Note how the for is constructed:
for (int i = front; i < size + front; i++)
{
returned += mArray[i % mArray.Length] + ":";
}
return returned;
}
}
An implementation using capacity
instead of end
is also
possible,
but it requires to use the modulo operation (%
) to compute the end
,
using
end = (front + size) % capacity;
Our implementation can recover the capacity of the queue by using:
capacity = mArray.Length - size;
However, trying to recover the size from the front
and end
values is
not possible. One could try, using
public int CSize
{
get
{
int csize = 0;
if (front < end) { csize = end - front; }
else if (front == end) { csize = mArray.Length; }
else
{
csize = mArray.Length - front + end;
}
if (csize != size)
{
throw new NotImplementedException("CSize is not correctly implemented!");
}
return csize;
}
}
but the exception would be thrown: can you figure out in which case(s) the “computed size” would be incorrect?
Solution
The implementation is almost correct, the problem is that
front == end
can be true for two reasons:
- The queue is empty, no element have been added yet (hence, size should be 0),
- The queue is full, no element can be added (hence, size should be
mArray.Length
).
Since IsFull
, IsEmpty
and Capacity
all uses size
, we have no way
of telling if we are in the first or second situation. The rest of the
implementation is correct, but size
must be an attribute to be able to
differenciate those two cases (note that we could also have added a
bool
to distinguish between “full” and “empty”, but it’s more
convenient to have a size
attribute available at all time).