|
| 1 | +# Design Circular Queue |
| 2 | + |
| 3 | +Design your implementation of the circular queue. The circular queue is a linear data structure in which the operations are performed based on FIFO (First In First Out) principle and the last position is connected back to the first position to make a circle. It is also called "Ring Buffer". |
| 4 | + |
| 5 | +One of the benefits of the circular queue is that we can make use of the spaces in front of the queue. In a normal queue, once the queue becomes full, we cannot insert the next element even if there is a space in front of the queue. But using the circular queue, we can use the space to store new values. |
| 6 | + |
| 7 | +Implementation the ```MyCircularQueue``` class: |
| 8 | + |
| 9 | +- ```MyCircularQueue(k)``` Initializes the object with the size of the queue to be ```k```. |
| 10 | +- ```int Front()``` Gets the front item from the queue. If the queue is empty, return ```-1```. |
| 11 | +- ```int Rear()``` Gets the last item from the queue. If the queue is empty, return ```-1```. |
| 12 | +- ```boolean enQueue(int value)``` Inserts an element into the circular queue. Return ```true``` if the operation is successful. |
| 13 | +- ```boolean deQueue()``` Deletes an element from the circular queue. Return ```true``` if the operation is successful. |
| 14 | +- ```boolean isEmpty()``` Checks whether the circular queue is empty or not. |
| 15 | +- ```boolean isFull()``` Checks whether the circular queue is full or not. |
| 16 | + |
| 17 | +You must solve the problem without using the built-in queue data structure in your programming language. |
| 18 | + |
| 19 | +**Example 1:** |
| 20 | +> **Input** |
| 21 | +> |
| 22 | +> ["MyCircularQueue", "enQueue", "enQueue", "enQueue", "enQueue", "Rear", "isFull", "deQueue", "enQueue", "Rear"] |
| 23 | +> |
| 24 | +> [[3], [1], [2], [3], [4], [], [], [], [4], []] |
| 25 | +> |
| 26 | +> **Output** |
| 27 | +> |
| 28 | +> [null, true, true, true, false, 3, true, true, true, 4] |
| 29 | +> |
| 30 | +> **Explanation** |
| 31 | +> |
| 32 | +> MyCircularQueue myCircularQueue = new MyCircularQueue(3); |
| 33 | +> |
| 34 | +> myCircularQueue.enQueue(1); // return True |
| 35 | +> |
| 36 | +> myCircularQueue.enQueue(2); // return True |
| 37 | +> |
| 38 | +> myCircularQueue.enQueue(3); // return True |
| 39 | +> |
| 40 | +> myCircularQueue.enQueue(4); // return False |
| 41 | +> |
| 42 | +> myCircularQueue.Rear(); // return 3 |
| 43 | +> |
| 44 | +> myCircularQueue.isFull(); // return True |
| 45 | +> |
| 46 | +> myCircularQueue.deQueue(); // return True |
| 47 | +> |
| 48 | +> myCircularQueue.enQueue(4); // return True |
| 49 | +> |
| 50 | +> myCircularQueue.Rear(); // return 4 |
| 51 | + |
| 52 | +**Constraints:** |
| 53 | +- ```1 <= k <= 1000``` |
| 54 | +- ```0 <= value <= 1000``` |
| 55 | +- At most ```3000``` calls will be made to ```enQueue```, ```deQueue```, ```Front```, ```Rear```, ```isEmpty```, and ```isFull```. |
| 56 | + |
| 57 | +## Solution |
| 58 | + |
| 59 | +### Approach 1: Array |
| 60 | + |
| 61 | +**Intuition** |
| 62 | + |
| 63 | +Based on the description of the problem, an intuitive data structure that meets all the requirements would be a **ring** where the head and the tail are adjacent to each other. |
| 64 | + |
| 65 | +However, there does not exist a ring data structure in any programming language. A similar data structure at our disposal is the one called **Array** which is a collection of elements that reside continuously in one dimensional space. |
| 66 | + |
| 67 | +The essence of many design problems, is how one can build more advanced data structures with the basic building blocks such as Array. |
| 68 | +> In this case, to build a circular queue, we could form a virtual ring structure with the Array, via the manipulation of index. |
| 69 | +
|
| 70 | +Given a fixed size array, any of the elements could be considered as a head in a queue. As long as we know the length of the queue, we then can instantly locate its tail, based on the following formula: |
| 71 | + |
| 72 | + |
| 73 | + |
| 74 | +where the variable ```capacity``` is the size of the array, the ```count``` is the length of the queue and the ```headIndex``` and ```tailIndex``` are the indice of head and tail elements respectively in the array. Here we showcase a few examples how a circular queue could reside in a fixed size array. |
| 75 | + |
| 76 | + |
| 77 | + |
| 78 | +**Algorithm** |
| 79 | + |
| 80 | +The procedure to design a data structure lies essentially on how we design the *attributes* within the data structure. |
| 81 | + |
| 82 | +One of the traits of a good design is to have as less attributes as possible, which arguably could bring several benefits. |
| 83 | + |
| 84 | +- Less attributes usually implies little or no redundancy among the attributes. |
| 85 | +- The less redundant the attributes are, the simpler the manipulation logic, which eventually could be less error-prone. |
| 86 | +- Less attributes also requires less space and therefore it could also bring efficiency to the runtime performance. |
| 87 | + |
| 88 | +*However, it is not advisable to seek for the minimum set of attributes.* Sometimes, a bit of redundancy could help with the time complexity. After all, like many other problems, we are trying to strike a balance between the space and the time. |
| 89 | + |
| 90 | +Following the above principles, here we give a list of attributes and the thoughts behind each attribute. |
| 91 | + |
| 92 | +- ```queue```: a fixed size array to hold the elements for the circular queue. |
| 93 | +- ```headIndex```: an integer which indicates the current head element in the circular queue. |
| 94 | +- ```count```: the current length of the circular queue, i.e. the number of elements in the circular queue. Together with the ```headIndex```, we could locate the current tail element in the circular queue, based on the formula we gave previously. Therefore, we choose not to add another attribute for the index of tail. |
| 95 | +- ```capacity```: the capacity of the circular queue, i.e. the maximum number of elements that can be hold in the queue. One might argument that it is not absolutely necessary to add this attribute, since we could obtain the capacity from the ```queue``` attribute. It is true. Yet, since we would frequently use this ```capacity``` in our algorithm, we choose to keep it as an attribute, instead of invoking function ```len(queue)``` in Python at every occasion. Though in other programming languages such as Java, it might be more efficient to omit this attribute, since it is part of the attributes (```queue.length```) in Java array. Note: for the sake of consistency, we keep this attribute for all implementations. |
| 96 | + |
| 97 | +``` |
| 98 | +class MyCircularQueue { |
| 99 | +
|
| 100 | + private int[] queue; |
| 101 | + private int headIndex; |
| 102 | + private int count; |
| 103 | + private int capacity; |
| 104 | +
|
| 105 | + /** Initialize your data structure here. Set the size of the queue to be k. */ |
| 106 | + public MyCircularQueue(int k) { |
| 107 | + this.capacity = k; |
| 108 | + this.queue = new int[k]; |
| 109 | + this.headIndex = 0; |
| 110 | + this.count = 0; |
| 111 | + } |
| 112 | +
|
| 113 | + /** Insert an element into the circular queue. Return true if the operation is successful. */ |
| 114 | + public boolean enQueue(int value) { |
| 115 | + if (this.count == this.capacity) |
| 116 | + return false; |
| 117 | + this.queue[(this.headIndex + this.count) % this.capacity] = value; |
| 118 | + this.count += 1; |
| 119 | + return true; |
| 120 | + } |
| 121 | +
|
| 122 | + /** Delete an element from the circular queue. Return true if the operation is successful. */ |
| 123 | + public boolean deQueue() { |
| 124 | + if (this.count == 0) |
| 125 | + return false; |
| 126 | + this.headIndex = (this.headIndex + 1) % this.capacity; |
| 127 | + this.count -= 1; |
| 128 | + return true; |
| 129 | + } |
| 130 | +
|
| 131 | + /** Get the front item from the queue. */ |
| 132 | + public int Front() { |
| 133 | + if (this.count == 0) |
| 134 | + return -1; |
| 135 | + return this.queue[this.headIndex]; |
| 136 | + } |
| 137 | +
|
| 138 | + /** Get the last item from the queue. */ |
| 139 | + public int Rear() { |
| 140 | + if (this.count == 0) |
| 141 | + return -1; |
| 142 | + int tailIndex = (this.headIndex + this.count - 1) % this.capacity; |
| 143 | + return this.queue[tailIndex]; |
| 144 | + } |
| 145 | +
|
| 146 | + /** Checks whether the circular queue is empty or not. */ |
| 147 | + public boolean isEmpty() { |
| 148 | + return (this.count == 0); |
| 149 | + } |
| 150 | +
|
| 151 | + /** Checks whether the circular queue is full or not. */ |
| 152 | + public boolean isFull() { |
| 153 | + return (this.count == this.capacity); |
| 154 | + } |
| 155 | +} |
| 156 | +``` |
| 157 | + |
| 158 | +**Complexity** |
| 159 | +- Time complexity: *O(1)*. All of the methods in our circular data structure is of constant time complexity. |
| 160 | +- Space Complexity: *O(N)*. The overall space complexity of the data structure is linear, where *N* is the pre-assigned capacity of the queue. However, it is worth mentioning that the memory consumption of the data structure remains as its pre-assigned capacity during its entire life cycle. |
| 161 | + |
| 162 | + |
| 163 | + |
| 164 | + |
| 165 | + |
| 166 | +There are several ways to mitigate the above concurrency problem. Take the function enQueue(int value) as an example, we show how we can make the function thread-safe in the following implementation: |
| 167 | + |
| 168 | +``` |
| 169 | +class MyCircularQueue { |
| 170 | +
|
| 171 | + private Node head, tail; |
| 172 | + private int count; |
| 173 | + private int capacity; |
| 174 | + // Additional variable to secure the access of our queue |
| 175 | + private ReentrantLock queueLock = new ReentrantLock(); |
| 176 | +
|
| 177 | + /** Initialize your data structure here. Set the size of the queue to be k. */ |
| 178 | + public MyCircularQueue(int k) { |
| 179 | + this.capacity = k; |
| 180 | + } |
| 181 | +
|
| 182 | + /** Insert an element into the circular queue. Return true if the operation is successful. */ |
| 183 | + public boolean enQueue(int value) { |
| 184 | + // ensure the exclusive access for the following block. |
| 185 | + queueLock.lock(); |
| 186 | + try { |
| 187 | + if (this.count == this.capacity) |
| 188 | + return false; |
| 189 | +
|
| 190 | + Node newNode = new Node(value); |
| 191 | + if (this.count == 0) { |
| 192 | + head = tail = newNode; |
| 193 | + } else { |
| 194 | + tail.nextNode = newNode; |
| 195 | + tail = newNode; |
| 196 | + } |
| 197 | + this.count += 1; |
| 198 | +
|
| 199 | + } finally { |
| 200 | + queueLock.unlock(); |
| 201 | + } |
| 202 | + return true; |
| 203 | + } |
| 204 | +} |
| 205 | +``` |
| 206 | + |
| 207 | +With the protection of locks, we now feel more confident to apply our circular queue in critical scenarios. |
| 208 | + |
| 209 | +The above improvement does not alter the time and space complexity of the original data structure, though overall the thread-safe measures do incur some extra costs. |
| 210 | + |
| 211 | +### Approach 2: Singly-Linked List |
| 212 | + |
| 213 | +**Intuition** |
| 214 | + |
| 215 | +Similar with Array, the Linked List is another common building block for more advanced data structures. |
| 216 | +> Different than a fixed size Array, a linked list could be more memory efficient, since it does not pre-allocate memory for unused capacity. |
| 217 | +
|
| 218 | +With a singly-linked list, we could design a circular queue with the same time and space complexity as the approach with Array. But we could gain some memory efficiency, since we don't need to pre-allocate the memory upfront. |
| 219 | + |
| 220 | +In the following graph, we show how the operations of ```enQueue()``` and ```deQueue()``` can be done via singly-linked list. |
| 221 | + |
| 222 | + |
| 223 | + |
| 224 | +**Algorithm** |
| 225 | + |
| 226 | +Here we give a list of attributes in our circular queue data structure and the thoughts behind each attribute. |
| 227 | + |
| 228 | +- ```capacity```: the maximum number of elements that the circular queue will hold. |
| 229 | +- ```head```: the reference to the head element in the queue. |
| 230 | +- ```count```: the current length of the queue. This is a critical attribute that helps us to do the boundary check in each method. |
| 231 | +- ```tail```: the reference to the tail element in the queue. Unlike the Array approach, we need to explicitly keep the reference to the tail element. Without this attribute, it would take us *O(N)* time complexity to locate the tail element from the head element. |
| 232 | + |
| 233 | +``` |
| 234 | +class Node { |
| 235 | + public int value; |
| 236 | + public Node nextNode; |
| 237 | +
|
| 238 | + public Node(int value) { |
| 239 | + this.value = value; |
| 240 | + this.nextNode = null; |
| 241 | + } |
| 242 | +} |
| 243 | +
|
| 244 | +class MyCircularQueue { |
| 245 | +
|
| 246 | + private Node head, tail; |
| 247 | + private int count; |
| 248 | + private int capacity; |
| 249 | +
|
| 250 | + /** Initialize your data structure here. Set the size of the queue to be k. */ |
| 251 | + public MyCircularQueue(int k) { |
| 252 | + this.capacity = k; |
| 253 | + } |
| 254 | +
|
| 255 | + /** Insert an element into the circular queue. Return true if the operation is successful. */ |
| 256 | + public boolean enQueue(int value) { |
| 257 | + if (this.count == this.capacity) |
| 258 | + return false; |
| 259 | +
|
| 260 | + Node newNode = new Node(value); |
| 261 | + if (this.count == 0) { |
| 262 | + head = tail = newNode; |
| 263 | + } else { |
| 264 | + tail.nextNode = newNode; |
| 265 | + tail = newNode; |
| 266 | + } |
| 267 | + this.count += 1; |
| 268 | + return true; |
| 269 | + } |
| 270 | +
|
| 271 | + /** Delete an element from the circular queue. Return true if the operation is successful. */ |
| 272 | + public boolean deQueue() { |
| 273 | + if (this.count == 0) |
| 274 | + return false; |
| 275 | + this.head = this.head.nextNode; |
| 276 | + this.count -= 1; |
| 277 | + return true; |
| 278 | + } |
| 279 | +
|
| 280 | + /** Get the front item from the queue. */ |
| 281 | + public int Front() { |
| 282 | + if (this.count == 0) |
| 283 | + return -1; |
| 284 | + else |
| 285 | + return this.head.value; |
| 286 | + } |
| 287 | +
|
| 288 | + /** Get the last item from the queue. */ |
| 289 | + public int Rear() { |
| 290 | + if (this.count == 0) |
| 291 | + return -1; |
| 292 | + else |
| 293 | + return this.tail.value; |
| 294 | + } |
| 295 | +
|
| 296 | + /** Checks whether the circular queue is empty or not. */ |
| 297 | + public boolean isEmpty() { |
| 298 | + return (this.count == 0); |
| 299 | + } |
| 300 | +
|
| 301 | + /** Checks whether the circular queue is full or not. */ |
| 302 | + public boolean isFull() { |
| 303 | + return (this.count == this.capacity); |
| 304 | + } |
| 305 | +} |
| 306 | +``` |
| 307 | + |
| 308 | +**Complexity** |
| 309 | +- Time complexity: *O(1)* for each method in our circular queue. |
| 310 | +- Space Complexity: The upper bound of the memory consumption for our circular queue would be *O(N)*, same as the Array approach. However, it should be more memory efficient as we discussed in the intuition section. |
0 commit comments