|
2 | 2 |
|
3 | 3 | A double-ended queue. For some reason this is pronounced as "deck".
|
4 | 4 |
|
5 |
| -A regular [queue](../Queue/) adds new elements to the back and dequeues from the front. The deque also allows enqueuing at the front and dequeuing from the back, and peeking at both ends. |
| 5 | +A regular [queue](../Queue/) adds new elements to the back and removes from the front. The deque also allows enqueuing at the front and dequeuing from the back, and peeking at both ends. |
6 | 6 |
|
7 | 7 | Here is a very basic implementation of a deque in Swift:
|
8 | 8 |
|
@@ -71,7 +71,232 @@ deque.dequeue() // 5
|
71 | 71 | ```
|
72 | 72 |
|
73 | 73 | This particular implementation of `Deque` is simple but not very efficient. Several operations are **O(n)**, notably `enqueueFront()` and `dequeue()`. I've included it only to show the principle of what a deque does.
|
| 74 | + |
| 75 | +## A more efficient version |
| 76 | + |
| 77 | +The reason that `dequeue()` and `enqueueFront()` are **O(n)** is that they work on the front of the array. If you remove an element at the front of an array, what happens is that all the remaining elements need to be shifted in memory. |
| 78 | + |
| 79 | +Let's say the deque's array contains the following items: |
| 80 | + |
| 81 | + [ 1, 2, 3, 4 ] |
| 82 | + |
| 83 | +Then `dequeue()` will remove `1` from the array and the elements `2`, `3`, and `4`, are shifted one position to the front: |
| 84 | + |
| 85 | + [ 2, 3, 4 ] |
| 86 | + |
| 87 | +This is an **O(n)** operation because all array elements need to be moved by one position in the computer's memory. |
| 88 | + |
| 89 | +Likewise, inserting an element at the front of the array is expensive because it requires that all other elements must be shifted one position to the back. So `enqueueFront(5)` will change the array to be: |
| 90 | + |
| 91 | + [ 5, 2, 3, 4 ] |
| 92 | + |
| 93 | +First, the elements `2`, `3`, and `4`, are move up by one position in the computer's memory, and then the new element `5` is inserted at the position where `2` used to be. |
| 94 | + |
| 95 | +Why is this not an issue at for `enqueue()` and `dequeueBack()`? Well, these operations are performed at the end of the array. The way resizable arrays are implemented in Swift is by reserving a certain amount of free space at the back. |
| 96 | + |
| 97 | +Our initial array `[ 1, 2, 3, 4]` actually looks like this in memory: |
| 98 | + |
| 99 | + [ 1, 2, 3, 4, x, x, x ] |
| 100 | + |
| 101 | +where the `x`s denote additional positions in the array that are not being used yet. Calling `enqueue(6)` simply copies the new item into the next unused spot: |
| 102 | + |
| 103 | + [ 1, 2, 3, 4, 6, x, x ] |
| 104 | + |
| 105 | +And `dequeueBack()` uses `array.removeLast()` to read that item and decrement `array.count` by one. There is no shifting of memory involved here. So operations at the back of the array are fast, **O(1)**. |
| 106 | + |
| 107 | +It is possible the array runs out of free spots at the back. In that case, Swift will allocate a new, larger array and copy over all the data. This is an **O(n)** operation but because it only happens once in a while, adding new elements at the end of an array is still **O(1)** on average. |
| 108 | + |
| 109 | +Of course, we can use this same trick at the *beginning* of the array. That will make our deque efficient too for operations at the front of the queue. Our array will look like this: |
| 110 | + |
| 111 | + [ x, x, x, 1, 2, 3, 4, x, x, x ] |
| 112 | + |
| 113 | +There is now a chunk of free space at the start of the array, which allows adding or removing elements at the front of the queue to be **O(1)** as well. |
| 114 | + |
| 115 | +Here is the new version of `Deque`: |
| 116 | + |
| 117 | +```swift |
| 118 | +public struct Deque<T> { |
| 119 | + private var array: [T?] |
| 120 | + private var head: Int |
| 121 | + private var capacity: Int |
| 122 | + |
| 123 | + public init(capacity: Int = 10) { |
| 124 | + self.capacity = max(capacity, 1) |
| 125 | + array = .init(count: capacity, repeatedValue: nil) |
| 126 | + head = capacity |
| 127 | + } |
| 128 | + |
| 129 | + public var isEmpty: Bool { |
| 130 | + return count == 0 |
| 131 | + } |
| 132 | + |
| 133 | + public var count: Int { |
| 134 | + return array.count - head |
| 135 | + } |
| 136 | + |
| 137 | + public mutating func enqueue(element: T) { |
| 138 | + array.append(element) |
| 139 | + } |
74 | 140 |
|
75 |
| -A more efficient implementation would use a [doubly linked list](../Linked List/), a [circular buffer](../Ring Buffer/), or two [stacks](../Stack/) facing opposite directions. |
| 141 | + public mutating func enqueueFront(element: T) { |
| 142 | + // this is explained below |
| 143 | + } |
| 144 | + |
| 145 | + public mutating func dequeue() -> T? { |
| 146 | + // this is explained below |
| 147 | + } |
| 148 | + |
| 149 | + public mutating func dequeueBack() -> T? { |
| 150 | + if isEmpty { |
| 151 | + return nil |
| 152 | + } else { |
| 153 | + return array.removeLast() |
| 154 | + } |
| 155 | + } |
| 156 | + |
| 157 | + public func peekFront() -> T? { |
| 158 | + if isEmpty { |
| 159 | + return nil |
| 160 | + } else { |
| 161 | + return array[head] |
| 162 | + } |
| 163 | + } |
| 164 | + |
| 165 | + public func peekBack() -> T? { |
| 166 | + if isEmpty { |
| 167 | + return nil |
| 168 | + } else { |
| 169 | + return array.last! |
| 170 | + } |
| 171 | + } |
| 172 | +} |
| 173 | +``` |
| 174 | + |
| 175 | +It still largely looks the same -- `enqueue()` and `dequeueBack()` haven't changed -- but there are also a few important differences. The array now stores objects of type `T?` instead of just `T` because we need some way to mark array elements as being empty. |
| 176 | + |
| 177 | +The `init` method allocates a new array that contains a certain number of `nil` values. This is the free room we have to work with at the beginning of the array. By default this creates 10 empty spots. |
| 178 | + |
| 179 | +The `head` variable is the index in the array of the front-most object. Since the queue is currently empty, `head` points at an index beyond the end of the array. |
| 180 | + |
| 181 | + [ x, x, x, x, x, x, x, x, x, x ] |
| 182 | + | |
| 183 | + head |
| 184 | + |
| 185 | +To enqueue an object at the front, we move `head` one position to the left and then copy the new object into the array at index `head`. For example, `enqueueFront(5)` gives: |
| 186 | + |
| 187 | + [ x, x, x, x, x, x, x, x, x, 5 ] |
| 188 | + | |
| 189 | + head |
| 190 | + |
| 191 | +Followed by `enqueueFront(7)`: |
| 192 | + |
| 193 | + [ x, x, x, x, x, x, x, x, 7, 5 ] |
| 194 | + | |
| 195 | + head |
| 196 | + |
| 197 | +And so on... the `head` keeps moving to the left and always points at the first item in the queue. `enqueueFront()` is now **O(1)** because it only involves copying a value into the array, a constant-time operation. |
| 198 | + |
| 199 | +Here is the code: |
| 200 | + |
| 201 | +```swift |
| 202 | + public mutating func enqueueFront(element: T) { |
| 203 | + head -= 1 |
| 204 | + array[head] = element |
| 205 | + } |
| 206 | +``` |
| 207 | + |
| 208 | +Appending to the back of the queue has not changed (it's the exact same code as before). For example, `enqueue(1)` gives: |
| 209 | + |
| 210 | + [ x, x, x, x, x, x, x, x, 7, 5, 1, x, x, x, x, x, x, x, x, x ] |
| 211 | + | |
| 212 | + head |
| 213 | + |
| 214 | +Notice how the array has resized itself. There was no room to add the `1`, so Swift decided to make the array larger and add a number of empty spots to the end. If you enqueue another object, it gets added to the next empty spot in the back. For example, `enqueue(2)`: |
| 215 | + |
| 216 | + [ x, x, x, x, x, x, x, x, 7, 5, 1, 2, x, x, x, x, x, x, x, x ] |
| 217 | + | |
| 218 | + head |
| 219 | + |
| 220 | +> **Note:** You won't see those empty spots at the back when you `print(deque.array)`. This is because Swift hides them from you. Only the ones at the front of the array show up. |
| 221 | +
|
| 222 | +The `dequeue()` method does the opposite of `enqueueFront()`, it reads the value at `head`, sets the array element back to `nil`, and then moves `head` one position to the right: |
| 223 | + |
| 224 | +```swift |
| 225 | + public mutating func dequeue() -> T? { |
| 226 | + guard head < array.count, let element = array[head] else { return nil } |
| 227 | + |
| 228 | + array[head] = nil |
| 229 | + head += 1 |
| 230 | + |
| 231 | + return element |
| 232 | + } |
| 233 | +``` |
| 234 | + |
| 235 | +There is one tiny problem... If you enqueue a lot of objects at the front, you're going to run out of empty spots at the front at some point. When this happens at the back of the array, Swift automatically resizes it. But at the front of the array we have to handle this ourselves with some extra logic in `enqueueFront()`: |
| 236 | + |
| 237 | +```swift |
| 238 | + public mutating func enqueueFront(element: T) { |
| 239 | + if head == 0 { |
| 240 | + capacity *= 2 |
| 241 | + let emptySpace = [T?](count: capacity, repeatedValue: nil) |
| 242 | + array.insertContentsOf(emptySpace, at: 0) |
| 243 | + head = capacity |
| 244 | + } |
| 245 | + |
| 246 | + head -= 1 |
| 247 | + array[head] = element |
| 248 | + } |
| 249 | +``` |
| 250 | + |
| 251 | +If `head` equals 0, there is no room left at the front. When that happens, we add a whole bunch of new `nil` elements to the array. This is an **O(n)** operation but since this cost gets divided over all the `enqueueFront()`s, each individual call to `enqueueFront()` is still **O(1)** on average. |
| 252 | + |
| 253 | +> **Note:** We also multiply the capacity by 2 each time this happens, so if your queue grows bigger and bigger, the resizing happens less often. This is also what Swift arrays automatically do at the back. |
| 254 | +
|
| 255 | +We have to do something similar for `dequeue()`. If you mostly enqueue a lot of elements at the back and mostly dequeue from the front, then you may end up with an array that looks like this: |
| 256 | + |
| 257 | + [ x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, 1, 2, 3 ] |
| 258 | + | |
| 259 | + head |
| 260 | + |
| 261 | +Those empty spots at the front only get used when you use `enqueueFront()`. But if enqueuing objects at the front happens only rarely, this leaves a lot of wasted space. So let's add some code to `dequeue()` to clean this up: |
| 262 | + |
| 263 | +```swift |
| 264 | + public mutating func dequeue() -> T? { |
| 265 | + guard head < array.count, let element = array[head] else { return nil } |
| 266 | + |
| 267 | + array[head] = nil |
| 268 | + head += 1 |
| 269 | + |
| 270 | + if capacity > 10 && head >= capacity*2 { |
| 271 | + array.removeFirst(capacity) |
| 272 | + head -= capacity |
| 273 | + } |
| 274 | + return element |
| 275 | + } |
| 276 | +``` |
| 277 | + |
| 278 | +Recall that `capacity` is the original number of empty places at the front of the queue. If the `head` has advanced more to the right than twice the capacity, then it's time to trim off half of these empty spots. |
| 279 | + |
| 280 | +For example, this: |
| 281 | + |
| 282 | + [ x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, x, 1, 2, 3 ] |
| 283 | + | | |
| 284 | + capacity head |
| 285 | + |
| 286 | +becomes this after trimming: |
| 287 | + |
| 288 | + [ x, x, x, x, x, x, x, x, x, 1, 2, 3 ] |
| 289 | + | |
| 290 | + head |
| 291 | + |
| 292 | +This way we can strike a balance between fast enqueuing and dequeuing at the front and keeping the memory requirements reasonable. |
| 293 | + |
| 294 | +> **Note:** We don't perform this trimming on very small arrays. It's not worth it for saving just a few bytes of memory. |
| 295 | +
|
| 296 | +## See also |
| 297 | + |
| 298 | +Other ways to implement deque are by using a [doubly linked list](../Linked List/), a [circular buffer](../Ring Buffer/), or two [stacks](../Stack/) facing opposite directions. |
| 299 | + |
| 300 | +[A fully-featured deque implementation in Swift](https://github.com/lorentey/Deque) |
76 | 301 |
|
77 | 302 | *Written for Swift Algorithm Club by Matthijs Hollemans*
|
0 commit comments