15. ArrayQueue
Things needed for the implementation
A container
A way to keep track of the front/head
A way to keep track of the rear/tail
A way to keep track of the size
A linked structure was used as a container for a
Queue
Now an array will be used to implement the
Queue
15.1. Idea #1
Use an array for the container
Keep track of the front by simply having it always be index
0
Keep track of the
rear
indexSize will conveniently be equal to
rear
All adding (
enqueue
) happens at therear
endAdd the element to index
rear
Increment
rear
All removing (
dequeue
) happens at index0
Requires that elements are shuffled down
1
indexDecrement
rear
15.1.1. Discussion
Will this implementation work?
Is it correct?
Knowing that this is an array implementation, will something like an
expandCapacity
be required?What is the computational complexity of an
enqueue
with this idea?Amortized \(O(1)\)
What is the computational complexity of a
dequeue
with this idea?\(O(n)\) as it requires all \(n\) elements be shuffle down one index in the array
The drawback of idea #1 is a \(O(n)\)
dequeue
The
LinkedQueue
implementation has an \(O(1)\)dequeue
15.2. Idea #2
Use an array for the container
Keep track of the
front
indexKeep track of the
rear
indexSize will conveniently be
rear - front
All adding (
enqueue
) happens at therear
endAdd the element to index
rear
Increment
rear
All removing (
dequeue
) happens at indexfront
Remove the element at index
front
Increment
front
15.2.1. Discussion
Will this implementation work?
Is it correct?
Knowing that this is an array implementation, will something like an
expandCapacity
be required?What is the computational complexity of an
enqueue
with this idea?Amortized \(O(1)\)
What is the computational complexity of a
dequeue
with this idea?\(O(1)\)
The drawback of idea #2 is the wasted space caused by
dequeue
All indices before
front
are wastedexpandCapacity
would need to be called after \(n + 1\) enqueues despite the number of elements actually in the queue
15.3. Idea #3
Use an array for the container
Keep track of the
front
indexKeep track of the
rear
indexKeep track of the
size
If there are empty indices before the
front
, wrap therear
back to0
when the end of the array is hit
15.3.1. “Circular” Array
Pretend the array is a circle
The array is still, in reality, a linear array
For example, given an array with a capacity of
n
The indices’ order would be
..., 0, 1, 2, 3, ..., n-2, n-1, 0, 1, 2, 3, ..., n-2, n-1, 0, 1, 2, 3, ...
The index that comes after
n-1
is0
The index that comes before
0
isn-1
15.3.2. Modulo — %
The modulo (
%
) operator provides a way to get the remainder of a division4 % 2
4/2 == 2
remainder0
Therefore,
4 % 2
is0
5 % 4
5/4 == 1
remainder1
Therefore,
5 % 4
is1
7 % 8
7/8 == 0
remainder7
Therefore,
7 % 8
is7
Knowing the remainder provides a way to wrap back to the beginning of an array
15.3.2.1. rear = (rear + 1) % queue.length
Assume an array with a capacity
10
Also assume
rear
is currently9
rear
should always be the index of the next available spot in the arrayIf
enqueue
is called, the new element is added to index9
andrear
is updatedHowever,
rear
cannot simply be incremented to10
since there is no index10
in an array of capacity10
Instead, in this case,
rear
should be updated to0
This could be achieved with an
if
statement —if (rear == queue.length) rear = 0
But notice that when
rear == queue.length
,rear % queue.length
is0
But also notice that when
rear
is another number, like4
,rear % queue.length
would be4
With this information, the following expression for incrementing the
rear
should make senserear = (rear + 1) % queue.length
If
rear
is9
and this idea is used,rear
will end up being(9 + 1) % 10 == 10 % 10 == 0
If
rear
is any other number< 10
, the number is not divisible by10
and the%
will make no difference
15.3.3. Discussion
Does the strategy for updating
front
need to also be changed?Will this “circular” array ever run out of room?
Warning
Sometimes, good enough is good enough.
With this ArrayQueue
implementation scenario, idea #3 is quite clearly the superior option and is not overly
difficult to implement. However, as one continues in computer science and works on more complex problems, sometimes
ease of implementation and maintainability becomes very important.
Better algorithms always exist, but a subpar implementation may do the trick, especially when the problem space is small enough that performance doesn’t matter.
Computational complexity is very important, but sometimes in practice one may lose the forrest through the trees. If an algorithm can be changed from \(O(n^{2})\) to \(O(n)\), then do it. But then again, if the updated algorithm will take a day to implement and it only needs to be run once on a small problem, perhaps \(O(n^{2})\) is good enough.
Even worse, trying to save a few FLOPS here and there is great and all, but if that’s distracting someone from other more important issues, perhaps they should move on.
Donald Knuth, a very famous computer scientist, says:
Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%.
15.3.4. Expand Capacity
With an
ArrayQueue
at capacity, someexpandCapacity
method would need to be calledUnlike before, however, the size of the array cannot simply just be doubled with the contents copied
Instead, copy the contents into contiguous indices starting at index
front
Another alternative is to copy the contents into contiguous indices starting at the beginning (index
0
) of the new array
15.3.5. Discussion Again
Will this implementation work?
Is it correct?
What is the computational complexity of this
enqueue
?What is the computational complexity of this
dequeue
?How often will this call
expandCapacity
relative to idea #1 and #2?
15.4. Implementing a Queue — Array Container
All the code is available for download at the bottom of the page
Here, only a subset of methods are shown
15.4.1. enqueue
39 @Override
40 public boolean enqueue(T element) {
41 if (size == queue.length) {
42 queue = expandCapacity(queue);
43 }
44 queue[rear] = element;
45 rear = nextIndex(rear);
46 size++;
47 return true;
48 }
Note the call to
expandCapacity
andnextIndex
103 /**
104 * Calculates the next valid index for the front or rear fields. This method is used to have the index increment
105 * with an automatic wrapping back to index zero if there is no more room left at the end of the array.
106 *
107 * @param currentIndex Index to find next index of
108 * @return Wrapping next index
109 */
110 private int nextIndex(int currentIndex) {
111 return (currentIndex + 1) % queue.length;
112 }
nextIndex
is a simple private helper method to return the next index for a “circular” array
52 /**
53 * Returns a new array with double the size of the queue array container and copy the contents from the old array to
54 * the new array. Note that the elements are copied to the beginning (index 0) of the new array and the front and
55 * rear fields are updated.
56 */
57 @SuppressWarnings("unchecked")
58 private T[] expandCapacity(T[] oldQueue) {
59 T[] newQueue = (T[]) new Object[oldQueue.length * 2];
60 for (int i = 0; i < oldQueue.length; i++) {
61 newQueue[i] = oldQueue[front];
62 front = nextIndex(front);
63 }
64 front = 0;
65 rear = size();
66 return newQueue;
67 }
The
expandCapacity
used here is different from earlier versionsFirst, notice that the copying is from index
front
toi
Previously, for the
ArrayStack
,newStack[i] = oldStack[i]
Each time the loop updates both
i
andfront
front
is updated withnextIndex
After all the copying is complete, the
front
for thenewQueue
is set to0
rear
is set to the sizeWhen
front
is0
,rear
must be equal tosize
15.4.2. dequeue
72 @Override
73 public T dequeue() {
74 if (isEmpty()) {
75 throw new NoSuchElementException("Empty queue");
76 }
77 T returnElement = queue[front];
78 front = nextIndex(front);
79 size--;
80 return returnElement;
81 }
Since
front
may wrap around to index0
, the private methodnextIndex
is used
15.4.3. equals
129 @Override
130 public final boolean equals(Object o) {
131 if (this == o) {
132 return true;
133 }
134 if (o == null || getClass() != o.getClass()) {
135 return false;
136 }
137 ArrayQueue<?> that = (ArrayQueue<?>) o;
138 if (this.size() != that.size()) {
139 return false;
140 }
141 int thisCurrentIndex = this.front;
142 int thatCurrentIndex = that.front;
143 // Since this and that are the same size, this.size() can be used safely in the for loop
144 for (int i = 0; i < this.size(); i++) {
145 if (!Objects.equals(this.queue[thisCurrentIndex], that.queue[thatCurrentIndex])) {
146 return false;
147 }
148 thisCurrentIndex = this.nextIndex(thisCurrentIndex);
149 thatCurrentIndex = that.nextIndex(thatCurrentIndex);
150 }
151 return true;
152 }
It does not matter where in the array the contents are
It also does not matter what the capacities of the arrays are
All that matters is that the elements in the two arrays are equivalent
Starting at their respective
front
indices and wrapping when necessary
15.5. For Next Time
Read Chapter 5 Section 7
7 pages
15.5.1. Playing Code
Download and play with
ArrayQueue
codeArrayQueueTest
tests
One could use the same code from
PlayingLinkedQueue
to play with theArrayQueue
Only need to make one change
LinkedQueue
->ArrayQueue
If everything was done correctly, the following code from
PlayingArrayQueue
should work
1import java.util.NoSuchElementException;
2
3public class PlayingArrayQueue {
4 public static void main(String[] args) {
5 // Create a ArrayQueue
6 Queue<Integer> myQueue = new ArrayQueue<>(5);
7
8 // Check queue is empty
9 System.out.println(myQueue.size());
10 System.out.println(myQueue.isEmpty());
11 System.out.println(myQueue);
12
13 // Test enqueue
14 myQueue.enqueue(0);
15 myQueue.enqueue(1);
16 myQueue.enqueue(2);
17 myQueue.enqueue(3);
18 myQueue.enqueue(4);
19 System.out.println(myQueue.size());
20 System.out.println(myQueue.isEmpty());
21 System.out.println(myQueue);
22
23 // Test enqueue more to check expandCapacity
24 myQueue.enqueue(10);
25 myQueue.enqueue(11);
26 myQueue.enqueue(12);
27 myQueue.enqueue(13);
28 myQueue.enqueue(14);
29 System.out.println(myQueue.size());
30 System.out.println(myQueue.isEmpty());
31 System.out.println(myQueue);
32
33 // Test first
34 System.out.println(myQueue.first());
35 System.out.println(myQueue.size());
36 System.out.println(myQueue.isEmpty());
37 System.out.println(myQueue);
38
39 // Test dequeue
40 System.out.println(myQueue.dequeue());
41 System.out.println(myQueue.dequeue());
42 System.out.println(myQueue.dequeue());
43 System.out.println(myQueue.dequeue());
44 System.out.println(myQueue.dequeue());
45 System.out.println(myQueue.dequeue());
46 System.out.println(myQueue.dequeue());
47 System.out.println(myQueue.dequeue());
48 System.out.println(myQueue.dequeue());
49 System.out.println(myQueue.dequeue());
50 System.out.println(myQueue.size());
51 System.out.println(myQueue.isEmpty());
52 System.out.println(myQueue);
53
54 // Test first and dequeue throwing exception
55 try {
56 myQueue.first();
57 } catch (NoSuchElementException e) {
58 e.printStackTrace();
59 }
60 try {
61 myQueue.dequeue();
62 } catch (NoSuchElementException e) {
63 e.printStackTrace();
64 }
65 }
66}