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
QueueNow 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
0Keep track of the
rearindexSize will conveniently be equal to
rear
An example of idea #1’s ArrayQueue containing four elements. This implementation requires that index 0
always be the front. Note the value stored in rear refers to the next available spot in the array. Also notice
that the value in rear corresponds to the number of elements currently in the queue.
All adding (
enqueue) happens at therearendAdd the element to index
rearIncrement
rear
The state of idea #1’s ArrayQueue after an element was enqueued. Note that the element was added at index
rear and the value of rear was increased such that it refers to the next available spot in the array.
All removing (
dequeue) happens at index0Requires that elements are shuffled down
1indexDecrement
rear
The state of idea #1’s ArrayQueue after an element was dequeued. Note that the element at index 0 was
removed and all elements moved down one index in the array. Further, the value of rear was decreased by one.
15.1.1. Discussion
Will this implementation work?
Is it correct?
Knowing that this is an array implementation, will something like an
expandCapacitybe required?What is the computational complexity of an
enqueuewith this idea?Amortized \(O(1)\)
What is the computational complexity of a
dequeuewith 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)\)
dequeueThe
LinkedQueueimplementation has an \(O(1)\)dequeue
15.2. Idea #2
Use an array for the container
Keep track of the
frontindexKeep track of the
rearindexSize will conveniently be
rear - front
An example of idea #2’s ArrayQueue containing four elements. This implementation keeps track of the front
and rear indices. Note the value stored in rear refers to the next available spot in the array. Also notice
that the difference between rear and front corresponds to the number of elements currently in the queue.
All adding (
enqueue) happens at therearendAdd the element to index
rearIncrement
rear
The state of idea #1’s ArrayQueue after an element was enqueued. Note that the element was added at index
rear and the value of rear was increased such that it refers to the next available spot in the array.
All removing (
dequeue) happens at indexfrontRemove the element at index
frontIncrement
front
The state of idea #2’s ArrayQueue after an element was dequeued. Note that the element at index front was
removed and the value of front increased by one. Note that, with the exception of the removed element, no other
elements were required to be moved within the array.
15.2.1. Discussion
Will this implementation work?
Is it correct?
Knowing that this is an array implementation, will something like an
expandCapacitybe required?What is the computational complexity of an
enqueuewith this idea?Amortized \(O(1)\)
What is the computational complexity of a
dequeuewith this idea?\(O(1)\)
The drawback of idea #2 is the wasted space caused by
dequeueAll indices before
frontare wastedexpandCapacitywould 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
frontindexKeep track of the
rearindexKeep track of the
sizeIf there are empty indices before the
front, wrap therearback to0when the end of the array is hit
An example of idea #3’s ArrayQueue containing four elements. Note that the rear index has wrapped back to the
beginning of the “circular” array.
An example of idea #3’s ArrayQueue containing four elements. This figure contains the same ArrayQueue shown
in the previous figure but with the “circular” array shown as a typical linear array.
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
nThe 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-1is0The index that comes before
0isn-1
Example “circular” array with a capacity of 13. This figure shows the array containing seven elements stored in
indices 0 – 6. Within the context of the ArrayQueue, front would be index 0 and rear would
be 7.
Example “circular” array with a capacity of 13 containing two elements stored in indices 5 and 6. This would
be the state of the ArrayQueue shown in the proceeding figure after dequeue is called five times.
Example “circular” array with a capacity of 13 containing 10 elements stored in indices 5 – 12, 0 and
1. This would be the state of the ArrayQueue shown in the proceeding figure after enqueue is called
eight times. Notice that rear wrapped back to the beginning of the array.
15.3.2. Modulo — %
The modulo (
%) operator provides a way to get the remainder of a division4 % 24/2 == 2remainder0Therefore,
4 % 2is0
5 % 45/4 == 1remainder1Therefore,
5 % 4is1
7 % 87/8 == 0remainder7Therefore,
7 % 8is7
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
10Also assume
rearis currently9rearshould always be the index of the next available spot in the arrayIf
enqueueis called, the new element is added to index9andrearis updatedHowever,
rearcannot simply be incremented to10since there is no index10in an array of capacity10Instead, in this case,
rearshould be updated to0This could be achieved with an
ifstatement —if (rear == queue.length) rear = 0But notice that when
rear == queue.length,rear % queue.lengthis0But also notice that when
rearis another number, like4,rear % queue.lengthwould be4With this information, the following expression for incrementing the
rearshould make senserear = (rear + 1) % queue.lengthIf
rearis9and this idea is used,rearwill end up being(9 + 1) % 10 == 10 % 10 == 0If
rearis any other number< 10, the number is not divisible by10and the%will make no difference
15.3.3. Discussion
Does the strategy for updating
frontneed 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
Example ArrayQueue with an array of capacity four containing three elements. The value of rear is 1 as
it is the next available spot in the array.
Example ArrayQueue after a single element was added. The value of rear is 2 as it would be the next
available spot in the array, however, the array is at capacity — size == queue.length.
With an
ArrayQueueat capacity, someexpandCapacitymethod would need to be calledUnlike before, however, the size of the array cannot simply just be doubled with the contents copied
Example ArrayQueue after a naive expandCapacity. In this scenario, there is a “hole” in the middle of the
elements as the front is index 2 and the elements wrapped back to the beginning of the array.
Instead, copy the contents into contiguous indices starting at index
front
Example ArrayQueue with an improved expandCapacity. The elements are copied starting at index front.
Another alternative is to copy the contents into contiguous indices starting at the beginning (index
0) of the new array
Example ArrayQueue with another improved expandCapacity. These elements are copied by starting at index
front in the old array and copying them starting at index 0 in 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
expandCapacityrelative 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
expandCapacityandnextIndex
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 }
nextIndexis 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
expandCapacityused here is different from earlier versionsFirst, notice that the copying is from index
fronttoiPreviously, for the
ArrayStack,newStack[i] = oldStack[i]
Each time the loop updates both
iandfrontfrontis updated withnextIndex
After all the copying is complete, the
frontfor thenewQueueis set to0rearis set to the sizeWhen
frontis0,rearmust 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
frontmay wrap around to index0, the private methodnextIndexis 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
frontindices 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
ArrayQueuecodeArrayQueueTesttests
One could use the same code from
PlayingLinkedQueueto play with theArrayQueueOnly need to make one change
LinkedQueue->ArrayQueue
If everything was done correctly, the following code from
PlayingArrayQueueshould 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}