Files
codetutor/backend/data/questions/design-circular-queue.yaml
2025-05-25 11:08:40 +01:00

286 lines
12 KiB
YAML

title: Design Circular Queue
slug: design-circular-queue
difficulty: medium
leetcode_id: 622
leetcode_url: https://leetcode.com/problems/design-circular-queue/
categories:
- arrays
- queue
patterns:
- two-pointers
description: |
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".
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.
Implement the `MyCircularQueue` class:
- `MyCircularQueue(k)` Initializes the object with the size of the queue to be `k`.
- `int Front()` Gets the front item from the queue. If the queue is empty, return `-1`.
- `int Rear()` Gets the last item from the queue. If the queue is empty, return `-1`.
- `boolean enQueue(int value)` Inserts an element into the circular queue. Return `true` if the operation is successful.
- `boolean deQueue()` Deletes an element from the circular queue. Return `true` if the operation is successful.
- `boolean isEmpty()` Checks whether the circular queue is empty or not.
- `boolean isFull()` Checks whether the circular queue is full or not.
You must solve the problem without using the built-in queue data structure in your programming language.
constraints: |
- `1 <= k <= 1000`
- `0 <= value <= 1000`
- At most `3000` calls will be made to `enQueue`, `deQueue`, `Front`, `Rear`, `isEmpty`, and `isFull`.
examples:
- input: |
["MyCircularQueue", "enQueue", "enQueue", "enQueue", "enQueue", "Rear", "isFull", "deQueue", "enQueue", "Rear"]
[[3], [1], [2], [3], [4], [], [], [], [4], []]
output: "[null, true, true, true, false, 3, true, true, true, 4]"
explanation: |
MyCircularQueue myCircularQueue = new MyCircularQueue(3);
myCircularQueue.enQueue(1); // return True
myCircularQueue.enQueue(2); // return True
myCircularQueue.enQueue(3); // return True, queue is now [1, 2, 3]
myCircularQueue.enQueue(4); // return False, queue is full
myCircularQueue.Rear(); // return 3
myCircularQueue.isFull(); // return True
myCircularQueue.deQueue(); // return True, removes 1, queue is [2, 3]
myCircularQueue.enQueue(4); // return True, queue is now [2, 3, 4]
myCircularQueue.Rear(); // return 4
explanation:
intuition: |
Imagine a circular track where runners line up at different positions. When the track is full and someone at the front leaves, a new runner can take that vacated spot — the track "wraps around" from the end back to the beginning.
A circular queue works the same way. Unlike a standard array-based queue where you might shift all elements when dequeuing (expensive!) or waste space at the front after dequeuing, a circular queue uses **modular arithmetic** to wrap indices around. When the `rear` pointer reaches the end of the array, it wraps back to index `0` if there's space available.
The key insight is that we need to track two things:
1. **Where the front of the queue is** (for dequeue and peek operations)
2. **How many elements are currently in the queue** (to know if it's empty or full)
With these two pieces of information, we can compute where the rear is, and we can use the modulo operator (`%`) to wrap indices around the fixed-size array.
approach: |
We implement the circular queue using a **fixed-size array** with two pointers and a count:
**Step 1: Initialise the data structure**
- `data`: A fixed-size array of length `k` to store elements
- `head`: Points to the front element (where we dequeue from)
- `count`: Tracks the current number of elements in the queue
&nbsp;
**Step 2: Implement enQueue (insert at rear)**
- If the queue is full, return `false`
- Calculate the rear index: `(head + count) % k`
- Place the new element at this position
- Increment `count`
- Return `true`
&nbsp;
**Step 3: Implement deQueue (remove from front)**
- If the queue is empty, return `false`
- Move `head` forward: `head = (head + 1) % k`
- Decrement `count`
- Return `true`
&nbsp;
**Step 4: Implement Front and Rear**
- `Front()`: Return `data[head]` if not empty, else `-1`
- `Rear()`: Calculate rear index as `(head + count - 1) % k`, return that element if not empty
&nbsp;
**Step 5: Implement isEmpty and isFull**
- `isEmpty()`: Return `count == 0`
- `isFull()`: Return `count == k`
&nbsp;
The modulo operation is the magic that makes the "circular" behaviour work — it ensures indices wrap around when they exceed the array bounds.
common_pitfalls:
- title: Confusing Head and Tail Management
description: |
A common mistake is maintaining both `head` and `tail` pointers and struggling to differentiate between empty and full states. With two pointers, both `head == tail` could mean empty OR full!
Using a `count` variable instead of a `tail` pointer eliminates this ambiguity entirely. You always know the state: `count == 0` means empty, `count == k` means full.
wrong_approach: "Using head and tail pointers without a count"
correct_approach: "Use head pointer + count, compute tail when needed"
- title: Off-by-One Errors in Rear Calculation
description: |
When computing the rear index, remember that `rear` points to the *last element*, not the next empty slot. The formula is:
`rear = (head + count - 1) % k`
Not `(head + count) % k`, which would be the next insertion point.
For example, if `head = 0`, `count = 3`, and `k = 5`, the rear is at index `2`, not `3`.
wrong_approach: "rear = (head + count) % k"
correct_approach: "rear = (head + count - 1) % k"
- title: Forgetting to Handle Empty Queue Edge Cases
description: |
When the queue is empty, `Front()` and `Rear()` should return `-1`, not cause an index error or return garbage.
Always check `isEmpty()` before accessing elements:
```python
def Front(self):
if self.isEmpty():
return -1
return self.data[self.head]
```
wrong_approach: "Directly accessing data[head] without checking empty"
correct_approach: "Always check isEmpty() before accessing elements"
key_takeaways:
- "**Ring buffer pattern**: Circular queues are foundational for streaming data, bounded buffers, and producer-consumer scenarios"
- "**Modular arithmetic**: The `%` operator is the key to wrapping indices around a fixed-size array"
- "**Count vs. two pointers**: Tracking `count` separately simplifies empty/full detection and avoids ambiguous states"
- "**O(1) operations**: All operations are constant time, making this ideal for high-throughput scenarios"
time_complexity: "O(1). All operations (`enQueue`, `deQueue`, `Front`, `Rear`, `isEmpty`, `isFull`) execute in constant time."
space_complexity: "O(k). We use a fixed-size array of length `k` to store up to `k` elements."
solutions:
- approach_name: Array with Head and Count
is_optimal: true
code: |
class MyCircularQueue:
def __init__(self, k: int):
# Fixed-size array to store elements
self.data = [0] * k
# Index of the front element
self.head = 0
# Current number of elements
self.count = 0
# Maximum capacity
self.capacity = k
def enQueue(self, value: int) -> bool:
# Cannot insert if queue is full
if self.isFull():
return False
# Calculate rear position using modular arithmetic
# This wraps around when we reach the end of the array
rear = (self.head + self.count) % self.capacity
self.data[rear] = value
self.count += 1
return True
def deQueue(self) -> bool:
# Cannot remove if queue is empty
if self.isEmpty():
return False
# Move head forward (with wrap-around)
self.head = (self.head + 1) % self.capacity
self.count -= 1
return True
def Front(self) -> int:
# Return -1 for empty queue
if self.isEmpty():
return -1
return self.data[self.head]
def Rear(self) -> int:
# Return -1 for empty queue
if self.isEmpty():
return -1
# Rear is at (head + count - 1), wrapped around
rear = (self.head + self.count - 1) % self.capacity
return self.data[rear]
def isEmpty(self) -> bool:
return self.count == 0
def isFull(self) -> bool:
return self.count == self.capacity
explanation: |
**Time Complexity:** O(1) — All operations are constant time with no loops.
**Space Complexity:** O(k) — Fixed array size determined at initialization.
This implementation uses a single `head` pointer combined with a `count` variable. The rear position is computed on-demand using `(head + count) % capacity`. This approach is cleaner than maintaining separate head and tail pointers because the `count` variable unambiguously indicates whether the queue is empty or full.
- approach_name: Linked List Implementation
is_optimal: false
code: |
class ListNode:
def __init__(self, val: int):
self.val = val
self.next = None
class MyCircularQueue:
def __init__(self, k: int):
self.capacity = k
self.count = 0
# Head and tail pointers for the linked list
self.head = None
self.tail = None
def enQueue(self, value: int) -> bool:
if self.isFull():
return False
new_node = ListNode(value)
if self.isEmpty():
# First element: both head and tail point to it
self.head = new_node
self.tail = new_node
else:
# Append to tail
self.tail.next = new_node
self.tail = new_node
self.count += 1
return True
def deQueue(self) -> bool:
if self.isEmpty():
return False
# Move head to next node
self.head = self.head.next
self.count -= 1
# If queue becomes empty, reset tail too
if self.isEmpty():
self.tail = None
return True
def Front(self) -> int:
if self.isEmpty():
return -1
return self.head.val
def Rear(self) -> int:
if self.isEmpty():
return -1
return self.tail.val
def isEmpty(self) -> bool:
return self.count == 0
def isFull(self) -> bool:
return self.count == self.capacity
explanation: |
**Time Complexity:** O(1) — All operations are constant time.
**Space Complexity:** O(k) — At most `k` nodes in the linked list.
This alternative uses a singly linked list instead of an array. While it also achieves O(1) operations, it has higher memory overhead due to node objects and pointers. The array-based approach is generally preferred for its better cache locality and lower memory footprint. However, the linked list approach demonstrates that the "circular" concept is about the logical structure, not necessarily a physical ring in memory.