A queue is a linear data structure that follows the FIFO (First In, First Out) principle: the first element added is the first one to be removed. A real-world analogy is a line of customers at a ticket counter: the person who arrives first is served first.
In Python, you can implement a queue using a regular list (array-based approach) or a circular list to optimize performance.
This guide walks you through both implementations, explains their trade-offs, and shows you why the circular approach is almost always the better choice when working with fixed-size arrays.
Queue Implementation Using a Regular List
The most straightforward way to implement a queue is by using a fixed-size Python list. Elements are added at the rear and removed from the front by shifting all remaining elements one position to the left.
How It Works
- Enqueue: Adds a new element at the current
sizeindex and increments the size counter. - Dequeue: Removes the front element by shifting every remaining element one position to the left, then decrements the size counter.
- getFront: Returns the element at index
0(the front) without removing it. - Display: Prints all elements currently in the queue.
Full Implementation
class Queue:
def __init__(self, q_size):
self.arr = [0] * q_size
self.size = 0
self.capacity = q_size
self.front = 0
def enqueue(self, x):
"""Add an element to the rear of the queue."""
if self.size == self.capacity:
print("Queue is full")
return
self.arr[self.size] = x
self.size += 1
def dequeue(self):
"""Remove the front element by shifting all others left."""
if self.size == 0:
print("Queue is empty")
return
for i in range(1, self.size):
self.arr[i - 1] = self.arr[i]
self.size -= 1
def getFront(self):
"""Return the front element without removing it."""
if self.size == 0:
return -1
return self.arr[self.front]
def display(self):
"""Print all elements in the queue."""
for i in range(self.front, self.size):
print(self.arr[i], end=" ")
print()
q = Queue(4)
q.enqueue(1)
q.enqueue(2)
q.enqueue(3)
print(q.getFront())
q.dequeue()
q.enqueue(4)
q.display()Output:
1
2 3 4Why This Approach Is Inefficient
The critical problem with this implementation lies in the dequeue operation. Every time an element is removed, all remaining elements must be shifted one position to the left:
for i in range(1, self.size):
self.arr[i - 1] = self.arr[i]| Operation | Time Complexity |
|---|---|
| Enqueue | O(1) |
| Dequeue | O(n), due to element shifting |
| getFront | O(1) |
| Display | O(n) |
Space Complexity: O(n), where n is the capacity of the queue.
Queue Implementation Using a Circular List
A circular queue is an optimized version that eliminates the element-shifting overhead. Instead of moving elements after each dequeue, the front pointer simply advances forward. When either the front or rear pointer reaches the end of the underlying array, it wraps around to the beginning using the modulo operator (%).
This means the array positions are reused efficiently, and no space is wasted after dequeuing.
How It Works
The circular queue maintains three tracking variables:
front: The index of the first element in the queue.size: The current number of elements (used to calculate the rear position).cap: The maximum capacity of the queue.
The rear index is calculated dynamically as (front + size) % capacity rather than being stored separately. This avoids synchronization issues between front, rear, and size.
Enqueue logic:
- If the queue is full (
size == capacity), reject the insertion. - Calculate the rear index:
rear = (front + size) % capacity. - Place the new element at the rear index and increment
size.
Dequeue logic:
- If the queue is empty (
size == 0), returnNone. - Store the value at
front. - Advance
frontcircularly:front = (front + 1) % capacity. - Decrement
sizeand return the stored value.
Full Implementation
class CircularQueue:
def __init__(self, capacity):
self.data = [None] * capacity
self.cap = capacity
self.size = 0
self.front = 0
def enqueue(self, x):
"""Add an element to the rear of the circular queue."""
if self.size == self.cap:
print("Queue is full")
return
rear = (self.front + self.size) % self.cap
self.data[rear] = x
self.size += 1
def dequeue(self):
"""Remove and return the front element."""
if self.size == 0:
return None
result = self.data[self.front]
self.front = (self.front + 1) % self.cap
self.size -= 1
return result
def getFront(self):
"""Return the front element without removing it."""
if self.size == 0:
return None
return self.data[self.front]
def getRear(self):
"""Return the rear element without removing it."""
if self.size == 0:
return None
rear = (self.front + self.size - 1) % self.cap
return self.data[rear]
def isEmpty(self):
"""Check if the queue is empty."""
return self.size == 0
def isFull(self):
"""Check if the queue is full."""
return self.size == self.cap
def getSize(self):
"""Return the current number of elements."""
return self.sizeExample Usage and Output
q = CircularQueue(4)
q.enqueue(10)
print(q.getFront(), q.getRear())
q.enqueue(20)
print(q.getFront(), q.getRear())
q.enqueue(30)
print(q.getFront(), q.getRear())
q.enqueue(40)
print(q.getFront(), q.getRear())
q.dequeue()
print(q.getFront(), q.getRear())
q.dequeue()
print(q.getFront(), q.getRear())
q.enqueue(50)
print(q.getFront(), q.getRear())Output:
10 10
10 20
10 30
10 40
20 40
30 40
30 50Step-by-step explanation:
10is enqueued: front and rear both point to10.20is enqueued: front is10, rear is20.30and40are enqueued, filling the queue to capacity.dequeue()removes10and advancesfrontto index 1 (value20).dequeue()removes20and advancesfrontto index 2 (value30).enqueue(50)wraps around to index 0 (the slot previously occupied by10), demonstrating the circular behavior.
Understanding the Wrap-Around with Modulo
The key to the circular queue is the modulo operator (%). Here's what happens when enqueue(50) is called after two dequeues:
rear = (2 + 2) % 4 # rear = 0: wraps back to the beginningWithout the modulo, the rear index would be 4, which is out of bounds. The modulo ensures it wraps around to 0, reusing the freed slot.
Common Mistake: Not Using Modulo for Wrap-Around
A frequent error when implementing circular queues is forgetting to apply the modulo operation, which causes index-out-of-bounds errors or incorrect behavior:
def enqueue(self, x):
if self.size == self.cap:
return
rear = self.front + self.size # Can exceed array bounds!
self.data[rear] = x # IndexError when rear >= capacity
self.size += 1
def dequeue(self):
if self.size == 0:
return None
result = self.data[self.front]
self.front = self.front + 1 # Front keeps growing, never reuses slots
self.size -= 1
return resultrear = (self.front + self.size) % self.cap
self.front = (self.front + 1) % self.capComparison: Regular List vs. Circular List
| Aspect | Regular List Queue | Circular List Queue |
|---|---|---|
| Enqueue | O(1) | O(1) |
| Dequeue | O(n) (element shifting) | O(1) (pointer advance) |
| getFront | O(1) | O(1) |
| getRear | O(1) | O(1) |
| Space reuse | ❌ No wasted-slot recovery | ✅ Freed slots are reused |
| Implementation complexity | Simple | Slightly more complex |
When to Use Python's Built-In Alternatives
Both implementations above are excellent for learning, interviews, and understanding how queues work internally. However, for production code, Python offers built-in options that are already optimized:
from collections import deque
queue = deque()
queue.append(10) # Enqueue
queue.append(20)
print(queue.popleft()) # Dequeue: 10, O(1)Conclusion
Implementing a queue using a list in Python can be done in two ways: a regular list approach that is simple but suffers from O(n) dequeue performance, and a circular list approach that achieves O(1) time complexity for all core operations by reusing array slots through modulo arithmetic.
The circular queue is the clear winner when you need a fixed-size, array-based queue.
For general-purpose work in Python, collections.deque remains the recommended tool, but understanding these implementations is essential for mastering data structures and acing technical interviews.