321 lines
14 KiB
YAML
321 lines
14 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:
|
|
- slug: two-pointers
|
|
is_optimal: true
|
|
|
|
function_signature: "class MyCircularQueue: ..."
|
|
|
|
test_cases:
|
|
visible:
|
|
- input:
|
|
operations: ["MyCircularQueue", "enQueue", "enQueue", "enQueue", "enQueue", "Rear", "isFull", "deQueue", "enQueue", "Rear"]
|
|
arguments: [[3], [1], [2], [3], [4], [], [], [], [4], []]
|
|
expected: [null, true, true, true, false, 3, true, true, true, 4]
|
|
hidden:
|
|
- input:
|
|
operations: ["MyCircularQueue", "isEmpty", "enQueue", "isEmpty", "Front", "deQueue", "isEmpty"]
|
|
arguments: [[1], [], [5], [], [], [], []]
|
|
expected: [null, true, true, false, 5, true, true]
|
|
- input:
|
|
operations: ["MyCircularQueue", "enQueue", "enQueue", "Front", "Rear", "deQueue", "Front"]
|
|
arguments: [[2], [1], [2], [], [], [], []]
|
|
expected: [null, true, true, 1, 2, true, 2]
|
|
- input:
|
|
operations: ["MyCircularQueue", "deQueue", "Front", "Rear"]
|
|
arguments: [[3], [], [], []]
|
|
expected: [null, false, -1, -1]
|
|
- input:
|
|
operations: ["MyCircularQueue", "enQueue", "enQueue", "enQueue", "deQueue", "enQueue", "Front", "Rear"]
|
|
arguments: [[3], [1], [2], [3], [], [4], [], []]
|
|
expected: [null, true, true, true, true, true, 2, 4]
|
|
- input:
|
|
operations: ["MyCircularQueue", "enQueue", "deQueue", "enQueue", "deQueue", "enQueue", "Front", "Rear"]
|
|
arguments: [[1], [10], [], [20], [], [30], [], []]
|
|
expected: [null, true, true, true, true, true, 30, 30]
|
|
- input:
|
|
operations: ["MyCircularQueue", "isFull", "enQueue", "enQueue", "isFull", "enQueue", "isFull"]
|
|
arguments: [[2], [], [1], [2], [], [3], []]
|
|
expected: [null, false, true, true, true, false, true]
|
|
|
|
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
|
|
|
|
|
|
|
|
**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`
|
|
|
|
|
|
|
|
**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`
|
|
|
|
|
|
|
|
**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
|
|
|
|
|
|
|
|
**Step 5: Implement isEmpty and isFull**
|
|
|
|
- `isEmpty()`: Return `count == 0`
|
|
- `isFull()`: Return `count == k`
|
|
|
|
|
|
|
|
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.
|