Hello! Welcome to your next lesson.
In our recent lessons, we explored the stack data structure, a Last-In, First-Out (LIFO) container, and its powerful application in solving problems like the Next Greater Element using a monotonic stack. Today, we turn our attention to its close cousin, the queue.
A queue follows the First-In, First-Out (FIFO) principle, just like a line at a checkout counter—the first person to get in line is the first one to be served. This data structure is fundamental in computer science, used in everything from scheduling tasks in an operating system to handling requests on a web server and implementing graph algorithms like Breadth-First Search (BFS).
Your learning outcome for this lesson is to implement a queue using both a circular array and a linked list. We'll explore why a simple array isn't enough and how these two standard implementations provide the efficient O(1) performance required for core queue operations.
1. The Problem with a Simple Array
The most intuitive way to build a queue is with a standard array. We can use two pointers, front and rear, to keep track of the start and end of the queue.
enqueue(value): Add the element at therearindex and incrementrear. This is an efficient O(1) operation.dequeue(): Remove the element from thefrontindex.
Here's the catch: after removing the element at front, that space is now empty. To maintain a contiguous block of elements, we would have to shift all subsequent elements one position to the left. This shifting makes the dequeue operation O(N), which is highly inefficient and defeats the purpose of having a fast queue.
To see a walkthrough of this naive implementation and its performance bottleneck, let's watch a short segment of the following video.
Complete Queue Data Structure | in One Shot | Java Placement Course
This video from 'Apna College' starts by demonstrating a queue implementation using a simple array and clearly explains why the dequeue operation becomes a performance issue.
Watch from 05:15 to 11:20. Focus on how the remove (dequeue) operation is implemented by shifting elements and why this leads to O(N) time complexity.
This inefficiency is precisely why we need better implementations.
2. Implementation 1: The Circular Array
To avoid the O(N) dequeue operation, we can treat the array as circular. When the front or rear pointers reach the end of the array, they "wrap around" to the beginning. This way, we never need to shift elements. The key to this behavior is the modulo operator (%).
Core Concepts
We maintain an array and two pointers, front and rear.
- Pointers and Indices:
front: Index of the first element.rear: Index of the next available slot where a new element will be inserted.size: The current number of elements in the queue.
enqueue(value):- Check if the queue is full (
size == capacity). - Add the new element at
arr[rear]. - Update the rear pointer:
rear = (rear + 1) % capacity. - Increment
size.
- Check if the queue is full (
dequeue():- Check if the queue is empty (
size == 0). - Retrieve the element from
arr[front]. - Update the front pointer:
front = (front + 1) % capacity. - Decrement
size.
- Check if the queue is empty (
Both enqueue and dequeue are now O(1) operations.

To get a detailed visual explanation of how the pointers move and wrap around, the following video provides a very clear walkthrough.
Circular Queue Implementation - Array
The 'Blue Tree Code' channel offers a focused explanation of the circular queue, detailing the enqueue, dequeue, and resize logic with excellent animations.
Watch from 01:14 to 03:58 to understand the enqueue and dequeue logic with the modulo operator. Then, skim from 04:58 to 07:56 to see how this logic translates into Java methods like enqueue, dequeue, isEmpty, and isFull.
Java Implementation (Circular Array)
This implementation uses front, size, and capacity to manage the queue state, which is a common and robust approach. The rear index is calculated on the fly when needed.
Implementation of Circular Queue Using Array
The GeeksforGeeks article 'Implementation of Circular Queue Using Array' provides a clean and complete Java implementation that we will use as our reference.
Focus on the Java code snippets for the enqueue and dequeue methods. Notice how the rear index is calculated as (front + size) % capacity during enqueue, and the front pointer is simply moved during dequeue. Then, look at the complete main method to see it in action.
Here's the code, adapted from the article, for your reference.
class CircularQueue {
private int[] arr;
private int front;
private int size;
private int capacity;
public CircularQueue(int capacity) {
this.capacity = capacity;
this.arr = new int[capacity];
this.front = 0;
this.size = 0;
}
public boolean isFull() {
return size == capacity;
}
public boolean isEmpty() {
return size == 0;
}
public void enqueue(int value) {
if (isFull()) {
System.out.println("Queue is full. Cannot enqueue " + value);
return;
}
// Calculate rear index before insertion
int rear = (front + size) % capacity;
arr[rear] = value;
size++;
System.out.println("Enqueued: " + value);
}
public int dequeue() {
if (isEmpty()) {
System.out.println("Queue is empty. Cannot dequeue.");
return -1; // Or throw an exception
}
int removedValue = arr[front];
front = (front + 1) % capacity;
size--;
System.out.println("Dequeued: " + removedValue);
return removedValue;
}
public int peek() {
if (isEmpty()) {
System.out.println("Queue is empty.");
return -1;
}
return arr[front];
}
}
Test your understanding!
Imagine a circular queue with capacity = 4. Trace the state (front, size, and the array contents) after this sequence of operations:
enqueue(10)enqueue(20)enqueue(30)dequeue()enqueue(40)enqueue(50)dequeue()
Show answer
Initial state: front = 0, size = 0, arr = [_, _, _, _]
enqueue(10):rear=(0+0)%4=0.arr=[10,_,_,_],front=0,size=1enqueue(20):rear=(0+1)%4=1.arr=[10,20,_,_],front=0,size=2enqueue(30):rear=(0+2)%4=2.arr=[10,20,30,_],front=0,size=3dequeue(): Value 10 is removed.front=(0+1)%4=1,size=2.arrstate is logically[_,20,30,_].enqueue(40):rear=(1+2)%4=3.arr=[_,20,30,40],front=1,size=3enqueue(50):rear=(1+3)%4=0. The pointer wraps around!arr=[50,20,30,40],front=1,size=4. The queue is now full.dequeue(): Value 20 is removed.front=(1+1)%4=2,size=3.arrstate is logically[50,_,30,40].
Final state: front = 2, size = 3, and the queue contains 30, 40, 50 in that order.
3. Implementation 2: The Linked List
A circular array solves the efficiency problem, but it still has a fixed capacity. If you need a queue that can grow and shrink dynamically, a linked list is the perfect choice.
Core Concepts
We use a singly linked list and maintain two pointers: front (or head) and rear (or tail).
front: Points to the first node in the list.rear: Points to the last node in the list.enqueue(value):- Create a new node with the given value.
- If the queue is empty, set both
frontandrearto the new node. - Otherwise, set
rear.nextto the new node and updaterearto point to the new node.
dequeue():- Check if the queue is empty (
front == null). - Store the data from the
frontnode. - Move
fronttofront.next. - If
frontbecomesnull(meaning the last element was just removed), also setreartonull.
- Check if the queue is empty (
Again, both operations are a clean and efficient O(1).
You can see this implementation animated in the final section of the "Apna College" video.
Complete Queue Data Structure | in One Shot | Java Placement Course
The video now shifts to implementing a queue with a linked list, which naturally handles dynamic sizing.
Watch from 26:58 to 33:53. Pay attention to how the head and tail pointers are managed for add (enqueue) and remove (dequeue) operations.
Java Implementation (Linked List)
Here is a standard Java implementation for a queue using a linked list.
Queue Implementation Using Linked List in Java
For a clear code structure, let's refer to this GeeksforGeeks article on linked list-based queues.
Examine the 'Queue' class implementation. Note the simplicity of the enqueue and deque methods. The time and space complexity analysis at the end reinforces the performance characteristics.
This implementation clearly shows the role of the front and rear pointers.
// Node class for the linked list
class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
this.next = null;
}
}
// Queue class using a linked list
class LinkedListQueue {
private Node front, rear;
public LinkedListQueue() {
this.front = this.rear = null;
}
public boolean isEmpty() {
return this.front == null;
}
// Add an element to the rear of the queue
public void enqueue(int value) {
Node newNode = new Node(value);
System.out.println("Enqueued: " + value);
// If queue is empty, new node is both front and rear
if (this.isEmpty()) {
this.front = this.rear = newNode;
return;
}
// Add the new node at the end and update rear
this.rear.next = newNode;
this.rear = newNode;
}
// Remove an element from the front of the queue
public int dequeue() {
if (isEmpty()) {
System.out.println("Queue is empty.");
return -1; // Or throw an exception
}
int removedValue = this.front.data;
this.front = this.front.next;
// If front becomes null, then change rear to null as well
if (this.front == null) {
this.rear = null;
}
System.out.println("Dequeued: " + removedValue);
return removedValue;
}
}
4. Comparison: Circular Array vs. Linked List
This is a classic technical interview question. Knowing the trade-offs is crucial.
| Feature | Circular Array Implementation | Linked List Implementation |
|---|---|---|
| Size | Fixed capacity. Requires a resize operation if it becomes full. | Dynamic. Grows and shrinks as needed. |
| Memory Usage | Can lead to wasted space if the queue is often near-empty. | Uses exactly as much memory as needed for the elements. |
| Memory Overhead | Minimal. Just the array itself. | Each element has an overhead (the next pointer). |
| Performance | O(1) for enqueue and dequeue. Better cache locality. |
O(1) for enqueue and dequeue. Poorer cache locality. |
| Complexity | Logic is slightly more complex due to modulo arithmetic. | Logic is generally simpler and more intuitive. |
When to use which?
- Use a circular array when you have a good estimate of the maximum number of elements the queue will hold and performance is critical (cache locality can matter).
- Use a linked list when the number of elements is unpredictable or when simplicity of implementation is valued.
Conclusion
Great job! You've now learned how to implement one of the most fundamental data structures, the queue, in two standard and efficient ways.
Key Takeaways:
- A queue operates on a First-In, First-Out (FIFO) basis.
- A naive array implementation is inefficient for
dequeue(O(N)). - A circular array uses the modulo operator to achieve O(1)
enqueueanddequeuewith a fixed capacity. - A linked list provides a dynamically-sized queue with O(1)
enqueueanddequeueby managingfrontandrearpointers. - The choice between a circular array and a linked list involves a trade-off between memory management (static vs. dynamic) and performance characteristics (cache locality).
Preview of the next lesson:
Now that you understand both stacks and queues, get ready for an interesting interview puzzle. In the next lesson, we will explore how to implement a queue using two stacks. This will test your understanding of both data structures and their operational constraints.