Design Circular Queue
Problem Description
Design a circular queue (Circular Queue), also known as a ring buffer. A circular queue is a linear data structure whose operations are based on the FIFO (First-In-First-Out) principle, and the tail of the queue is connected to the head to form a circle. It is also called a "ring buffer."
One advantage of a circular queue is that we can reuse the space that has been used before in the queue. In a regular queue, once the queue is full, we cannot insert the next element, even if there is space available at the front of the queue. However, with a circular queue, we can use that space to store new values.
You need to implement the following operations:
MyCircularQueue(k): Constructor, sets the queue length to k.Front: Gets the element at the front of the queue. If the queue is empty, returns -1.Rear: Gets the element at the rear of the queue. If the queue is empty, returns -1.enQueue(value): Inserts an element into the circular queue. Returns true if the operation is successful.deQueue(): Deletes an element from the circular queue. Returns true if the operation is successful.isEmpty(): Checks if the circular queue is empty.isFull(): Checks if the circular queue is full.
Solution Process
Step 1: Understanding the Core Concept and Challenges of a Circular Queue
Imagine a regular queue implemented with a fixed-length array. When we remove elements from the front (head) and add elements to the rear (tail), both the front and rear pointers move backward. Even if empty spaces appear at the front of the array, when the rear pointer reaches the end of the array, we consider the queue "full" and cannot add new elements. This leads to wasted space.
A circular queue solves this problem by treating the array as a "ring." When the rear pointer reaches the end of the array, if there is empty space at the beginning of the array, the next element is placed at the start of the array. This way, as long as the queue is not truly filled with elements, we can continue adding elements.
The main challenge lies in determining whether the queue is "empty" or "full."
- Condition for an empty queue: When there are no elements in the queue, we define the
frontandrearpointers to be equal. - Condition for a full queue: If we also use the equality of
frontandrearpointers to indicate a full queue, it conflicts with the condition for an empty queue.
Step 2: Choosing a Solution—Waste One Space
To resolve the conflict between empty and full conditions, the most common and clear method is to "waste" one element space in the array.
- We allocate an array of size
k+1, but stipulate that it can store at mostkvalid elements. front: Points to the position of the first element in the queue.rear: Points to the next position after the last element in the queue (i.e., the position where the next new element should be inserted).
Now, we can define clear state conditions:
- Queue is empty:
front == rear - Queue is full:
(rear + 1) % capacity == front- Explanation: If the next position of
rear(using modulo operation to achieve circularity) isfront, the queue is considered full. Since we always reserve one empty space, whenrearpoints to this empty space, we consider the queue full and cannot insert further; otherwise, we cannot distinguish between empty and full.
- Explanation: If the next position of
Step 3: Detailed Design of Each Operation
Assume the declared array is queue, the capacity (maximum number of elements that can be stored) is k, but the actual allocated array size is k+1. We define capacity = k+1.
-
Constructor
MyCircularQueue(k)- Initialize an array of size
k+1(e.g.,self.queue = [0] * (k+1)). - Initialize
self.front = 0. - Initialize
self.rear = 0. - Record the maximum capacity of the queue:
self.capacity = k+1.
- Initialize an array of size
-
isEmpty()- Directly check
self.front == self.rear. If equal, returnTrue; otherwise, returnFalse.
- Directly check
-
isFull()- Check
(self.rear + 1) % self.capacity == self.front. If equal, returnTrue; otherwise, returnFalse.
- Check
-
enQueue(value)- First, check if the queue is full (call
isFull()). If full, returnFalse. - If not full, perform the following:
a. Placevalueat the position currently pointed to by therearpointer:self.queue[self.rear] = value.
b. Move therearpointer one step backward (with circular wrap-around):self.rear = (self.rear + 1) % self.capacity. - Return
True.
- First, check if the queue is full (call
-
deQueue()- First, check if the queue is empty (call
isEmpty()). If empty, returnFalse. - If not empty, perform the following:
a. Move thefrontpointer one step backward (with circular wrap-around):self.front = (self.front + 1) % self.capacity.- (Note: In a queue, deleting an element from the front typically does not require explicitly deleting the data in the array; moving the pointer suffices.)
- Return
True.
- First, check if the queue is empty (call
-
Front()- If the queue is empty, return
-1. - Otherwise, return the element currently pointed to by the
frontpointer:return self.queue[self.front].
- If the queue is empty, return
-
Rear()- If the queue is empty, return
-1. - Otherwise, return the element at the position before
rear, becauserearpoints to the next insertion position. - Calculate the position of the rear element:
(self.rear - 1 + self.capacity) % self.capacity.- Explanation:
self.rear - 1might result in a negative number (e.g., whenrearis 0). Addingcapacityand then taking the modulo ensures a valid, non-negative index.
- Explanation:
- Return the element at that position:
return self.queue[(self.rear - 1) % self.capacity].
- If the queue is empty, return
Step 4: Example
Let's design a circular queue with a capacity of 3 (k=3). The allocated array size is 4 (capacity=4).
Initial state: front = 0, rear = 0, queue is empty.
[ , , , ] (underscore indicates empty space)
Indices: 0, 1, 2, 3
-
enQueue(1):rear=0is not full, place 1 at index 0,rearmoves to(0+1)%4=1.
Queue:[1, _, _, _],front=0,rear=1 -
enQueue(2):rear=1is not full, place 2 at index 1,rearmoves to(1+1)%4=2.
Queue:[1, 2, _, _],front=0,rear=2 -
enQueue(3):rear=2is not full, place 3 at index 2,rearmoves to(2+1)%4=3.
Queue:[1, 2, 3, _],front=0,rear=3 -
enQueue(4): Check if full?(3+1)%4=0,0 == front(0)? Yes, queue is full, insertion fails. -
deQueue(): Queue is not empty,frontmoves to(0+1)%4=1.
Queue:[_, 2, 3, _],front=1,rear=3(Space at index 0 is freed, but data 1 remains; we just no longer access it) -
enQueue(4): Now check if full?(3+1)%4=0,0 == front(1)? No, not full. Place 4 at index 3,rearmoves to(3+1)%4=0.
Queue:[_, 2, 3, 4],front=1,rear=0. This demonstrates the circular nature. -
Rear(): Returns the position beforerear.rear=0, previous position is(0-1+4)%4=3. Returns the value atqueue[3], which is 4. Correct. -
Front(): Returns the value atqueue[1], which is 2. Correct.
Through this example, you can clearly see how the strategy of "wasting one space" works and how the pointers circulate.