Skip to content

Commit ab15e38

Browse files
committed
Add optimized version of Queue
1 parent 6943444 commit ab15e38

File tree

4 files changed

+245
-20
lines changed

4 files changed

+245
-20
lines changed

Queue/Queue-Optimized.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
First-in first-out queue (FIFO)
3+
4+
New elements are added to the end of the queue. Dequeuing pulls elements from
5+
the front of the queue.
6+
7+
Enqueuing and dequeuing are O(1) operations.
8+
*/
9+
public struct Queue<T> {
10+
private var array = [T?]()
11+
private var head = 0
12+
13+
public var isEmpty: Bool {
14+
return count == 0
15+
}
16+
17+
public var count: Int {
18+
return array.count - head
19+
}
20+
21+
public mutating func enqueue(element: T) {
22+
array.append(element)
23+
}
24+
25+
public mutating func dequeue() -> T? {
26+
guard head < array.count, let element = array[head] else { return nil }
27+
28+
array[head] = nil
29+
head += 1
30+
31+
let percentage = Double(head)/Double(array.count)
32+
if array.count > 50 && percentage > 0.25 {
33+
array.removeFirst(head)
34+
head = 0
35+
}
36+
37+
return element
38+
}
39+
40+
public func peek() -> T? {
41+
if isEmpty {
42+
return nil
43+
} else {
44+
return array[head]
45+
}
46+
}
47+
}

Queue/Queue.swift renamed to Queue/Queue-Simple.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@
88
implemented with a linked list, then both would be O(1).
99
*/
1010
public struct Queue<T> {
11-
var array = [T]()
12-
13-
public var isEmpty: Bool {
14-
return array.isEmpty
15-
}
11+
private var array = [T]()
1612

1713
public var count: Int {
1814
return array.count
1915
}
2016

17+
public var isEmpty: Bool {
18+
return array.isEmpty
19+
}
20+
2121
public mutating func enqueue(element: T) {
2222
array.append(element)
2323
}

Queue/Queue.playground/Contents.swift

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,32 +13,35 @@
1313

1414
public struct Queue<T> {
1515
private var array = [T]()
16-
17-
public var count: Int {
18-
return array.count
19-
}
20-
16+
2117
public var isEmpty: Bool {
2218
return array.isEmpty
2319
}
24-
20+
21+
public var count: Int {
22+
return array.count
23+
}
24+
2525
public mutating func enqueue(element: T) {
2626
array.append(element)
2727
}
28-
28+
2929
public mutating func dequeue() -> T? {
30-
if count == 0 {
30+
if isEmpty {
3131
return nil
3232
} else {
3333
return array.removeFirst()
3434
}
3535
}
36-
36+
3737
public func peek() -> T? {
3838
return array.first
3939
}
4040
}
4141

42+
43+
44+
4245
// Create a queue and put some elements on it already.
4346
var queueOfNames = Queue(array: ["Carl", "Lisa", "Stephanie", "Jeff", "Wade"])
4447

Queue/README.markdown

Lines changed: 181 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,22 @@ This returns `3`, the next dequeue returns `57`, and so on. If the queue is empt
4040

4141
> **Note:** A queue is not always the best choice. If the order in which the items are added and removed from the list isn't important, you might as well use a [stack](../Stack/) instead of a queue. Stacks are simpler and faster.
4242
43+
## The code
44+
4345
Here is a very simplistic implementation of a queue in Swift. It's just a wrapper around an array that lets you enqueue, dequeue, and peek at the front-most item:
4446

4547
```swift
4648
public struct Queue<T> {
47-
var array = [T]()
48-
49+
private var array = [T]()
50+
4951
public var isEmpty: Bool {
5052
return array.isEmpty
5153
}
5254

5355
public var count: Int {
5456
return array.count
5557
}
56-
58+
5759
public mutating func enqueue(element: T) {
5860
array.append(element)
5961
}
@@ -76,10 +78,183 @@ This queue works just fine but it is not optimal.
7678

7779
Enqueuing is an **O(1)** operation because adding to the end of an array always takes the same amount of time, regardless of the size of the array. So that's great.
7880

79-
However, to dequeue we remove the element from the beginning of the array. This is an **O(n)** operation because it requires all remaining array elements to be shifted in memory.
81+
You might be wondering why appending items to an array is **O(1)**, or a constant-time operation. That is so because an array in Swift always has some empty space at the end. If we do the following:
82+
83+
```swift
84+
var queue = Queue<String>()
85+
queue.enqueue("Ada")
86+
queue.enqueue("Steve")
87+
queue.enqueue("Tim")
88+
```
89+
90+
then the array might actually look like this:
91+
92+
[ "Ada", "Steve", "Tim", xxx, xxx, xxx ]
93+
94+
where `xxx` is memory that is reserved but not filled in yet. Adding a new element to the array overwrites the next unused spot:
95+
96+
[ "Ada", "Steve", "Tim", "Grace", xxx, xxx ]
97+
98+
This is simply matter of copying memory from one place to another, a constant-time operation.
99+
100+
Of course, there are only a limited number of such unused spots at the end of the array. When the last `xxx` gets used and you want to add another item, the array needs to resize to make more room. Resizing includes allocating new memory and copying all the existing data over to the new array. This is an **O(n)** process, so relatively slow. But since it happens only every so often, the time for appending a new element to the end of the array is still **O(1)** on average, or **O(1)** "amortized".
101+
102+
The story for dequeueing is slightly different. To dequeue we remove the element from the *beginning* of the array, not the end. This is always an **O(n)** operation because it requires all remaining array elements to be shifted in memory.
103+
104+
In our example, dequeuing the first element `"Ada"` copies `"Steve"` in the place of `"Ada"`, `"Tim"` in the place of `"Steve"`, and `"Grace"` in the place of `"Tim"`:
105+
106+
before [ "Ada", "Steve", "Tim", "Grace", xxx, xxx ]
107+
/ / /
108+
/ / /
109+
/ / /
110+
/ / /
111+
after [ "Steve", "Tim", "Grace", xxx, xxx, xxx ]
112+
113+
Moving all these elements in memory is always an **O(n)** operation. So with our simple implementation of a queue, enqueuing is efficient but dequeueing leaves something to be desired...
114+
115+
## A more efficient queue
116+
117+
To make dequeuing more efficient, we can use the same trick of reserving some extra free space, but this time do it at the front of the array. We're going to have to write this code ourselves as the built-in Swift array doesn't support this out of the box.
118+
119+
The idea is this: whenever we dequeue an item, we don't shift the contents of the array to the front (slow) but mark the item's position in the array as empty (fast). After dequeuing `"Ada"`, the array is:
120+
121+
[ xxx, "Steve", "Tim", "Grace", xxx, xxx ]
122+
123+
After dequeuing `"Steve"`, the array is:
124+
125+
[ xxx, xxx, "Tim", "Grace", xxx, xxx ]
126+
127+
These empty spots at the front never get reused for anything, so they're wasting space. Every so often you can trim the array by moving the remaining elements to the front again:
128+
129+
[ "Tim", "Grace", xxx, xxx, xxx, xxx ]
130+
131+
This trimming procedure involves shifting memory so it's an **O(n)** operation. But because it only happens once in a while, dequeuing is now **O(1)** on average.
132+
133+
Here is how you could implement this version of `Queue`:
134+
135+
```swift
136+
public struct Queue<T> {
137+
private var array = [T?]()
138+
private var head = 0
139+
140+
public var isEmpty: Bool {
141+
return count == 0
142+
}
143+
144+
public var count: Int {
145+
return array.count - head
146+
}
147+
148+
public mutating func enqueue(element: T) {
149+
array.append(element)
150+
}
151+
152+
public mutating func dequeue() -> T? {
153+
guard head < array.count, let element = array[head] else { return nil }
154+
155+
array[head] = nil
156+
head += 1
157+
158+
let percentage = Double(head)/Double(array.count)
159+
if head > 20 && percentage > 0.25 {
160+
array.removeFirst(head)
161+
head = 0
162+
}
163+
164+
return element
165+
}
166+
167+
public func peek() -> T? {
168+
if isEmpty {
169+
return nil
170+
} else {
171+
return array[head]
172+
}
173+
}
174+
}
175+
```
176+
177+
The array now stores objects of type `T?` instead of just `T` because we need some way to mark array elements as being empty. The `head` variable is the index in the array of the front-most object.
178+
179+
Most of the new functionality sits in `dequeue()`. When we dequeue an item, we first set `array[head]` to `nil` to remove the object from the array. Then we increment `head` because now the next item has become the front one.
180+
181+
We go from this:
182+
183+
[ "Ada", "Steve", "Tim", "Grace", xxx, xxx ]
184+
head
185+
186+
to this:
187+
188+
[ xxx, "Steve", "Tim", "Grace", xxx, xxx ]
189+
head
190+
191+
It's like some weird supermarket where the people in the checkout lane don't shuffle forward towards the cash register, but the cash register moves up the queue.
192+
193+
Of course, if we never remove those empty spots at the front then the array will keep growing as we enqueue and dequeue elements. To periodically trim down the array, we do the following:
194+
195+
```swift
196+
let percentage = Double(head)/Double(array.count)
197+
if array.count > 50 && percentage > 0.25 {
198+
array.removeFirst(head)
199+
head = 0
200+
}
201+
```
202+
203+
This calculates the percentage of empty spots at the beginning as a ratio of the total array size. If more than 25% of the array is unused, we chop off that wasted space. However, if the array is small we don't want to resize it all the time, so there must be at least 50 elements in the array before we try to trim it.
204+
205+
> **Note:** I just pulled these numbers out of thin air -- you may need to tweak them based on the behavior of your app in a production environment.
206+
207+
To test this in a playground, do:
208+
209+
```swift
210+
var q = Queue<String>()
211+
q.array // [] empty array
212+
213+
q.enqueue("Ada")
214+
q.enqueue("Steve")
215+
q.enqueue("Tim")
216+
q.array // [{Some "Ada"}, {Some "Steve"}, {Some "Tim"}]
217+
q.count // 3
218+
219+
q.dequeue() // "Ada"
220+
q.array // [nil, {Some "Steve"}, {Some "Tim"}]
221+
q.count // 2
222+
223+
q.dequeue() // "Steve"
224+
q.array // [nil, nil, {Some "Tim"}]
225+
q.count // 1
226+
227+
q.enqueue("Grace")
228+
q.array // [nil, nil, {Some "Tim"}, {Some "Grace"}]
229+
q.count // 2
230+
```
231+
232+
To test the trimming behavior, replace the line
233+
234+
```swift
235+
if array.count > 50 && percentage > 0.25 {
236+
```
237+
238+
with:
239+
240+
```swift
241+
if head > 2 {
242+
```
243+
244+
Now if you dequeue another object, the array will look as follows:
245+
246+
```swift
247+
q.dequeue() // "Tim"
248+
q.array // [{Some "Grace"}]
249+
q.count // 1
250+
```
251+
252+
The `nil` objects at the front have been removed and the array is no longer wasting space. This new version of `Queue` isn't much more complicated than the first one but dequeuing is now also an **O(1)** operation, just because we were a bit smarter about how we used the array.
253+
254+
## See also
80255

81-
More efficient implementations would use a linked list, a [circular buffer](../Ring Buffer/), or a [heap](../Heap/). (I might add an example of this later.)
256+
There are many other ways to create a queue. Alternative implementations use a [linked list](../Linked List/), a [circular buffer](../Ring Buffer/), or a [heap](../Heap/).
82257

83258
Variations on this theme are [deque](../Deque/), a double-ended queue where you can enqueue and dequeue at both ends, and [priority queue](../Priority Queue/), a sorted queue where the "most important" item is always at the front.
84259

85-
*Written by Matthijs Hollemans*
260+
*Written for Swift Algorithm Club by Matthijs Hollemans*

0 commit comments

Comments
 (0)