DSA Tutorial
🔍

Queue Implementation

What This Page Covers

The previous page covered operations using language built-ins. This page builds complete, production-quality queue classes from the ground up — three backend implementations and one advanced design pattern.

Implementation 1: Fixed-Size Circular Array Queue
  Underlying structure: fixed-capacity array with front/rear index pointers
  Teaches:  modulo arithmetic wrap-around, isFull vs isEmpty distinction
  Use when: capacity is known, maximum throughput required

Implementation 2: Dynamic Array Queue
  Underlying structure: resizable circular array
  Teaches:  automatic doubling, element copy during resize
  Use when: general-purpose, unknown capacity

Implementation 3: Linked List Queue
  Underlying structure: singly linked list with head and tail pointers
  Teaches:  true O(1) both ends, the tail-null bug, empty queue edge cases
  Use when: guaranteed O(1) per operation, no capacity limit

Advanced Pattern: Two-Stack Queue
  Implements a queue using two stacks — no linked list, no array tricks
  Teaches:  amortized O(1) dequeue via lazy transfer
  Use when: interview problem asks "implement queue using stacks"

Implementation 1: Fixed-Size Circular Array Queue

The key insight: instead of shifting elements when dequeuing, use two index pointers — front and rear. When an index reaches the end of the array, wrap it back to 0 using modulo. The array behaves as a ring — no element ever moves.

Circular array state, capacity=5:

Initial:  front=0, rear=0, size=0
  [_, _, _, _, _]
   0  1  2  3  4

Enqueue 10,20,30:
  [10, 20, 30, _, _]
   0   1   2  3  4
   ↑front         rear=3

Dequeue:
  front = (0+1)%5 = 1
  [10, 20, 30, _, _]   returns 10
        ↑front    rear=3

Enqueue 40,50,60 (wrap-around):
  rear = 3 → enqueue 40 at [3], rear=4
  rear = 4 → enqueue 50 at [4], rear=0  (wraps!)
  rear = 0 → enqueue 60 at [0], rear=1
  [60, 20, 30, 40, 50]
        ↑front=1   rear=1, size=5 (full)

isFull:  size == capacity
isEmpty: size == 0
(Do NOT use front == rear — it's true for both full and empty!)
Java
1public class CircularArrayQueue<T> { 2 3 private Object[] data; // Underlying fixed array 4 private int front; // Index of the front element 5 private int rear; // Index where next element will be inserted 6 private int size; // Current number of elements 7 private final int capacity; 8 9 @SuppressWarnings("unchecked") 10 public CircularArrayQueue(int capacity) { 11 this.capacity = capacity; 12 this.data = new Object[capacity]; 13 this.front = 0; 14 this.rear = 0; 15 this.size = 0; 16 } 17 18 // ENQUEUE — O(1) 19 public void enqueue(T value) { 20 if (isFull()) { 21 throw new IllegalStateException("Queue is full (capacity: " + capacity + ")"); 22 } 23 data[rear] = value; 24 rear = (rear + 1) % capacity; // Wrap rear with modulo 25 size++; 26 } 27 28 // DEQUEUE — O(1) 29 @SuppressWarnings("unchecked") 30 public T dequeue() { 31 if (isEmpty()) { 32 throw new java.util.NoSuchElementException("Queue is empty"); 33 } 34 T value = (T) data[front]; 35 data[front] = null; // Help GC — clear the slot 36 front = (front + 1) % capacity; // Wrap front with modulo 37 size--; 38 return value; 39 } 40 41 // PEEK — O(1) 42 @SuppressWarnings("unchecked") 43 public T peek() { 44 if (isEmpty()) { 45 throw new java.util.NoSuchElementException("Queue is empty"); 46 } 47 return (T) data[front]; 48 } 49 50 public boolean isEmpty() { return size == 0; } 51 public boolean isFull() { return size == capacity; } 52 public int size() { return size; } 53 public int capacity(){ return capacity; } 54 55 @Override 56 public String toString() { 57 if (isEmpty()) return "Queue: [] (empty)"; 58 StringBuilder sb = new StringBuilder("Queue (front→back): ["); 59 for (int i = 0; i < size; i++) { 60 sb.append(data[(front + i) % capacity]); 61 if (i < size - 1) sb.append(", "); 62 } 63 return sb.append("]").toString(); 64 } 65 66 public static void main(String[] args) { 67 CircularArrayQueue<Integer> q = new CircularArrayQueue<>(5); 68 69 q.enqueue(10); q.enqueue(20); q.enqueue(30); 70 System.out.println(q); // [10, 20, 30] 71 System.out.println("Peek: " + q.peek()); // 10 72 System.out.println("Size: " + q.size()); // 3 73 System.out.println("Dequeue: " + q.dequeue()); // 10 74 System.out.println("Dequeue: " + q.dequeue()); // 20 75 76 // Now front has wrapped — add more to test wrap-around 77 q.enqueue(40); q.enqueue(50); q.enqueue(60); 78 System.out.println(q); // [30, 40, 50, 60] 79 System.out.println("isFull: " + q.isFull()); // true 80 81 // Overflow attempt 82 try { q.enqueue(99); } 83 catch (IllegalStateException e) { 84 System.out.println("Overflow: " + e.getMessage()); 85 } 86 } 87}
Output:
Queue (front→back): [10, 20, 30]
Peek:     10
Dequeue:  10
Dequeue:  20
Queue (front→back): [30, 40, 50, 60]
isFull:   true
Overflow: Queue is full (capacity: 5)

Dry Run: Circular Array Wrap-Around

capacity=5, data=[_,_,_,_,_], front=0, rear=0, size=0

enqueue(10): data[0]=10, rear=(0+1)%5=1, size=1
  [10, _, _, _, _]  front=0, rear=1

enqueue(20): data[1]=20, rear=2, size=2
enqueue(30): data[2]=30, rear=3, size=3
  [10, 20, 30, _, _]  front=0, rear=3

dequeue(): value=data[0]=10, data[0]=null, front=(0+1)%5=1, size=2
  [null, 20, 30, _, _]  front=1, rear=3

dequeue(): value=data[1]=20, front=2, size=1
  [null, null, 30, _, _]  front=2, rear=3

enqueue(40): data[3]=40, rear=4, size=2
enqueue(50): data[4]=50, rear=(4+1)%5=0, size=3   ← WRAP! rear goes back to 0
enqueue(60): data[0]=60, rear=1, size=4
  [60, null, 30, 40, 50]  front=2, rear=1

toString: iterate i=0..3: data[(2+0)%5]=data[2]=30, data[3]=40, data[4]=50, data[0]=60
  Output: [30, 40, 50, 60] ✓

Why size and not front==rear for isEmpty/isFull:
  After all 4 elements: front=2, rear=1 (not equal)
  If queue were empty with same indices: front==rear would be true
  Both states can have front==rear — only size distinguishes them.

Implementation 2: Dynamic Array Queue

A dynamic array queue is a circular array that automatically doubles its capacity when full. The resize must re-linearize the elements — copy them starting at index 0 in the new array, resetting front to 0 and rear to size.

Java
1public class DynamicArrayQueue<T> { 2 3 private Object[] data; 4 private int front; 5 private int rear; 6 private int size; 7 private int capacity; 8 9 private static final int INITIAL_CAPACITY = 4; 10 11 @SuppressWarnings("unchecked") 12 public DynamicArrayQueue() { 13 capacity = INITIAL_CAPACITY; 14 data = new Object[capacity]; 15 front = 0; 16 rear = 0; 17 size = 0; 18 } 19 20 // ENQUEUE — O(1) amortized 21 public void enqueue(T value) { 22 if (size == capacity) resize(); 23 data[rear] = value; 24 rear = (rear + 1) % capacity; 25 size++; 26 } 27 28 // RESIZE — double capacity, re-linearize elements — O(n) 29 @SuppressWarnings("unchecked") 30 private void resize() { 31 int newCapacity = capacity * 2; 32 Object[] newData = new Object[newCapacity]; 33 34 // Copy elements in logical order starting at index 0 35 for (int i = 0; i < size; i++) { 36 newData[i] = data[(front + i) % capacity]; 37 } 38 39 data = newData; 40 front = 0; // Reset front to 0 41 rear = size; // Reset rear to size (next empty slot) 42 capacity = newCapacity; 43 44 System.out.println(" [Resize] capacity: " + (newCapacity / 2) 45 + " → " + newCapacity); 46 } 47 48 // DEQUEUE — O(1) 49 @SuppressWarnings("unchecked") 50 public T dequeue() { 51 if (isEmpty()) throw new java.util.NoSuchElementException("Empty"); 52 T value = (T) data[front]; 53 data[front] = null; 54 front = (front + 1) % capacity; 55 size--; 56 return value; 57 } 58 59 // PEEK — O(1) 60 @SuppressWarnings("unchecked") 61 public T peek() { 62 if (isEmpty()) throw new java.util.NoSuchElementException("Empty"); 63 return (T) data[front]; 64 } 65 66 public boolean isEmpty() { return size == 0; } 67 public int size() { return size; } 68 public int capacity() { return capacity; } 69 70 public static void main(String[] args) { 71 DynamicArrayQueue<Integer> q = new DynamicArrayQueue<>(); 72 System.out.println("Initial capacity: " + q.capacity()); // 4 73 74 for (int i = 1; i <= 9; i++) { 75 q.enqueue(i * 10); 76 System.out.printf(" Enqueue %2d | size=%d | capacity=%d%n", 77 i * 10, q.size(), q.capacity()); 78 } 79 } 80}
Output:
Initial capacity: 4
  Enqueue 10 | size=1 | capacity=4
  Enqueue 20 | size=2 | capacity=4
  Enqueue 30 | size=3 | capacity=4
  Enqueue 40 | size=4 | capacity=4
  [Resize] capacity: 4 → 8
  Enqueue 50 | size=5 | capacity=8
  Enqueue 60 | size=6 | capacity=8
  Enqueue 70 | size=7 | capacity=8
  Enqueue 80 | size=8 | capacity=8
  [Resize] capacity: 8 → 16
  Enqueue 90 | size=9 | capacity=16

Why Re-Linearize During Resize?

Before resize (capacity=4, front=2, rear=1, size=4):
  data = [30, 40, 10, 20]   (elements wrapped around)
           ↑rear      ↑front

If we just copy to a new array naively:
  newData = [30, 40, 10, 20, _, _, _, _]
  front=2 still points to 10, rear=1 still points to 30
  But 10 is at index 2, and we've doubled to 8 — modulo changes!
  (front + i) % 8 would give wrong indices.

Re-linearization (correct approach):
  Copy elements in LOGICAL order (front first):
    newData[0] = data[(2+0)%4] = data[2] = 10
    newData[1] = data[(2+1)%4] = data[3] = 20
    newData[2] = data[(2+2)%4] = data[0] = 30
    newData[3] = data[(2+3)%4] = data[1] = 40
  
  newData = [10, 20, 30, 40, _, _, _, _]
  Reset front=0, rear=4 (size)
  Now all modulo arithmetic works correctly with the new capacity.

Implementation 3: Linked List Queue

Java
1public class LinkedListQueue<T> { 2 3 private static class Node<T> { 4 T data; 5 Node<T> next; 6 Node(T d) { data = d; next = null; } 7 } 8 9 private Node<T> head; // front — dequeue here 10 private Node<T> tail; // back — enqueue here 11 private int size; 12 13 public LinkedListQueue() { head = null; tail = null; size = 0; } 14 15 // ENQUEUE — O(1) 16 public void enqueue(T value) { 17 Node<T> newNode = new Node<>(value); 18 if (tail == null) { 19 head = newNode; 20 tail = newNode; 21 } else { 22 tail.next = newNode; 23 tail = newNode; 24 } 25 size++; 26 } 27 28 // DEQUEUE — O(1) 29 public T dequeue() { 30 if (isEmpty()) throw new java.util.NoSuchElementException("Empty"); 31 T value = head.data; 32 Node<T> removed = head; 33 head = head.next; 34 if (head == null) tail = null; // CRITICAL: clear tail when empty 35 removed.next = null; // Detach removed node — help GC 36 size--; 37 return value; 38 } 39 40 // PEEK — O(1) 41 public T peek() { 42 if (isEmpty()) throw new java.util.NoSuchElementException("Empty"); 43 return head.data; 44 } 45 46 public boolean isEmpty() { return head == null; } 47 public int size() { return size; } 48 public void clear() { head = null; tail = null; size = 0; } 49 50 @Override 51 public String toString() { 52 if (isEmpty()) return "Queue: [] (empty)"; 53 StringBuilder sb = new StringBuilder("Queue (front→back): ["); 54 Node<T> curr = head; 55 while (curr != null) { 56 sb.append(curr.data); 57 if (curr.next != null) sb.append(", "); 58 curr = curr.next; 59 } 60 return sb.append("]").toString(); 61 } 62 63 public static void main(String[] args) { 64 LinkedListQueue<Integer> q = new LinkedListQueue<>(); 65 66 q.enqueue(10); q.enqueue(20); q.enqueue(30); 67 System.out.println(q); // [10, 20, 30] 68 System.out.println("Dequeue: " + q.dequeue()); // 10 69 System.out.println("Dequeue: " + q.dequeue()); // 20 70 System.out.println("Dequeue: " + q.dequeue()); // 30 — queue now empty 71 System.out.println("isEmpty: " + q.isEmpty()); // true 72 // Next enqueue must correctly set both head AND tail 73 q.enqueue(99); 74 System.out.println("After re-enqueue: " + q); // [99] 75 System.out.println("head==tail: " + (q.head == q.tail)); // true (both at 99) 76 } 77}
Output:
Queue (front→back): [10, 20, 30]
Dequeue: 10
Dequeue: 20
Dequeue: 30
isEmpty: true
After re-enqueue: Queue (front→back): [99]

Advanced Pattern: Two-Stack Queue

Problem: Implement a queue using only two stacks — no array, no linked list.

Insight: One stack (inbox) receives all enqueues. The other stack (outbox) serves all dequeues. When outbox is empty, transfer all elements from inbox to outbox — this reversal restores FIFO order (the bottom of inbox, which is the oldest element, becomes the top of outbox, which is dequeued first).

Enqueue A, B, C:
  inbox=[A, B, C] (C on top)   outbox=[]

Dequeue:
  outbox empty → transfer inbox to outbox:
    pop C from inbox → push C to outbox
    pop B from inbox → push B to outbox
    pop A from inbox → push A to outbox
  inbox=[]  outbox=[C, B, A] (A on top)
  pop A from outbox → return A ✓ (oldest element first)

Dequeue again:
  outbox=[C, B] (B on top) — still has elements, no transfer needed
  pop B from outbox → return B ✓

Enqueue D:
  inbox=[D]   outbox=[C]
  No transfer — outbox still has C

Dequeue:
  outbox not empty → pop C → return C ✓
  
Dequeue:
  outbox empty → transfer inbox: pop D → push D
  outbox=[D]  → pop D → return D ✓
Java
1import java.util.Deque; 2import java.util.ArrayDeque; 3 4public class TwoStackQueue<T> { 5 6 private Deque<T> inbox = new ArrayDeque<>(); // Receives enqueues 7 private Deque<T> outbox = new ArrayDeque<>(); // Serves dequeues 8 9 // ENQUEUE — always to inbox — O(1) 10 public void enqueue(T value) { 11 inbox.push(value); 12 } 13 14 // TRANSFER — move all from inbox to outbox (reversing order) — O(n) 15 private void transfer() { 16 while (!inbox.isEmpty()) { 17 outbox.push(inbox.pop()); 18 } 19 } 20 21 // DEQUEUE — from outbox; transfer if outbox empty — O(1) amortized 22 public T dequeue() { 23 if (outbox.isEmpty()) { 24 if (inbox.isEmpty()) throw new java.util.NoSuchElementException("Empty"); 25 transfer(); 26 } 27 return outbox.pop(); 28 } 29 30 // PEEK — peek outbox front; transfer if outbox empty — O(1) amortized 31 public T peek() { 32 if (outbox.isEmpty()) { 33 if (inbox.isEmpty()) throw new java.util.NoSuchElementException("Empty"); 34 transfer(); 35 } 36 return outbox.peek(); 37 } 38 39 public boolean isEmpty() { return inbox.isEmpty() && outbox.isEmpty(); } 40 public int size() { return inbox.size() + outbox.size(); } 41 42 public static void main(String[] args) { 43 TwoStackQueue<Integer> q = new TwoStackQueue<>(); 44 45 q.enqueue(1); q.enqueue(2); q.enqueue(3); 46 System.out.println("Peek: " + q.peek()); // 1 47 System.out.println("Dequeue: " + q.dequeue()); // 1 48 System.out.println("Dequeue: " + q.dequeue()); // 2 49 50 q.enqueue(4); q.enqueue(5); 51 52 System.out.println("Dequeue: " + q.dequeue()); // 3 53 System.out.println("Dequeue: " + q.dequeue()); // 4 54 System.out.println("Dequeue: " + q.dequeue()); // 5 55 System.out.println("isEmpty: " + q.isEmpty()); // true 56 } 57}
Output:
Peek:    1
Dequeue: 1
Dequeue: 2
Dequeue: 3
Dequeue: 4
Dequeue: 5
isEmpty: true

Two-Stack Queue — Amortized Analysis

Why dequeue is O(1) amortized even though transfer is O(n):

Each element passes through the system exactly twice:
  1. Pushed onto inbox  (1 operation per element)
  2. Transferred to outbox (1 operation per element)
  3. Popped from outbox (1 operation per element)
  Total: 3 operations per element = O(1) per element

For n total enqueue+dequeue operations:
  Total transfers across ALL dequeue calls ≤ n (each element transferred once)
  n enqueues + n transfers + n pops = 3n = O(n)
  Amortized per operation: O(n) / n = O(1)

Worst case single dequeue: O(n) (triggers transfer of all n inbox elements)
Amortized over sequence:   O(1) per operation

Implementation Comparison

PropertyFixed Circular ArrayDynamic ArrayLinked ListTwo-Stack Queue
EnqueueO(1)O(1) amortizedO(1)O(1)
DequeueO(1)O(1)O(1)O(1) amortized
PeekO(1)O(1)O(1)O(1) amortized
Max capacityFixed at creationUnlimitedUnlimitedUnlimited
Memory per elementValue onlyValue onlyValue + pointerValue only (in stacks)
Memory layoutContiguousContiguousScatteredContiguous (stack arrays)
Cache performanceExcellentExcellentPoorGood
Overflow possibleYesNoNoNo
Best forKnown capacity, hot-pathGeneral purposeGuaranteed O(1)Interview: queue from stacks

Common Mistakes in Implementation

Not re-linearizing elements during resize. When a circular array queue resizes, front may not be 0 — elements wrap around. Copying the raw array bytes into a new array preserves the wrapped layout, but the modulo arithmetic then uses the wrong capacity. Always copy elements in logical order (index 0 = front, index size-1 = rear) and reset front=0, rear=size after copying.

Using front == rear as the only isEmpty or isFull check. Both an empty and a full circular array queue can have front == rear. Use a size counter (or a boolean full flag) to distinguish the two states. The size == 0 and size == capacity checks are unambiguous.

Forgetting tail = null when the last element is dequeued from a linked-list queue. Setting head = null makes isEmpty() return true, but tail still holds a stale pointer. The next enqueue() takes the non-empty branch, sets tail.next = newNode on garbage memory, and silently corrupts the queue. After head = null, always set tail = null.

Two-stack queue: transferring eagerly instead of lazily. Transfer should happen only when outbox is empty and a dequeue is requested — not on every enqueue. Eager transfer (moving inbox to outbox on every enqueue) gives O(n) per enqueue instead of O(1) amortized.

Not handling the single-element edge case in linked-list queue. When a queue has exactly one element and it is dequeued, head becomes null but tail still points to the removed node. The check if (head == null) tail = null must cover this case. Forgetting it means re-enqueueing after full dequeue produces a broken state.

Interview Questions

Q: What is the time and space complexity of the Two-Stack Queue?

Enqueue is O(1) — always push to the inbox stack. Dequeue is O(1) amortized — each element is pushed to inbox once, transferred to outbox once, and popped from outbox once; total work per element is O(3) = O(1). In the worst case, a single dequeue triggers an O(n) transfer, but across n operations the total transfer work is O(n) — O(1) amortized. Space is O(n) for n elements distributed across the two stacks.

Q: Why must the circular array queue use a size counter instead of relying on front == rear to detect empty and full states?

When the queue is empty, front == rear is true (both at the same position). When the queue is completely full, front == rear is also true — rear has wrapped all the way around and landed back on front. Without a size counter, these two states are indistinguishable. The counter makes isEmpty() and isFull() unambiguous: size == 0 and size == capacity respectively.

Q: Why must elements be re-linearized (not just raw-copied) when resizing a circular array queue?

A circular array queue's elements may wrap around — for example, elements at indices [3, 4, 0, 1] with front=3 and capacity 5. Raw-copying to a new array of capacity 10 preserves the same indices but changes how modulo arithmetic works. The formula (front + i) % capacity now uses the old front and new capacity together, giving wrong indices. Re-linearizing copies elements in logical order to indices [0, 1, 2, 3] and resets front=0, rear=size, making modulo arithmetic correct for the new capacity.

Quick Quiz

Question 1: A circular array queue has capacity=5, front=3, rear=3, size=5. What does isFull() return and why can't you use front==rear alone?

  • A) false — because front equals rear, which usually means empty
  • B) truesize == capacity is the unambiguous check
  • C) Error — this state is impossible
  • D) true — because rear has caught up to front

Answer: B) truesize == capacity. When fully filled, rear wraps back to equal front — same position as the empty state. Only the size counter distinguishes them. front == rear is true for BOTH empty (size=0) and full (size=capacity) states.

Question 2: During resize of a circular array queue with front=2, capacity=4, size=4, what are front and rear after re-linearization into a new array of size 8?

  • A) front=2, rear=2 — unchanged
  • B) front=0, rear=4
  • C) front=0, rear=2
  • D) front=2, rear=6

Answer: B) front=0, rear=4. Re-linearization copies the 4 elements in logical order to indices 0–3 of the new array. front is reset to 0 (beginning of the re-linearized elements), and rear is reset to size = 4 (the next empty slot). This makes modulo arithmetic consistent with the new capacity.

Question 3: In the Two-Stack Queue, when does transfer from inbox to outbox happen?

  • A) On every enqueue — inbox is always flushed to outbox
  • B) Lazily — only when outbox is empty AND a dequeue or peek is requested
  • C) On every dequeue regardless of outbox state
  • D) At fixed intervals of n operations

Answer: B) Lazily, only when outbox is empty. This is the key to amortized O(1). Each element is transferred at most once. If outbox still has elements, dequeue pops directly without any transfer. Eager transfer (on every enqueue) would give O(n) per enqueue.

Question 4: In a linked-list queue, you dequeue the last element. head becomes null. What MUST also happen?

  • A) size is decremented — nothing else required
  • B) tail must also be set to null
  • C) The next node must be found before removing head
  • D) front must be reset to 0

Answer: B) tail = null. After dequeuing the last element, head = null signals empty. But tail still holds a dangling pointer to the removed node. The next enqueue takes the non-empty branch (because tail != null) and calls tail.next = newNode on garbage memory — silent corruption. Clearing tail = null whenever head becomes null is mandatory.

Summary

Four complete queue implementations each with distinct tradeoffs:

Fixed Circular Array Queue:

  • front, rear, size — three state variables
  • rear = (rear + 1) % capacity on enqueue; front = (front + 1) % capacity on dequeue
  • isEmpty: size == 0; isFull: size == capacity — never front == rear alone
  • Fastest — contiguous memory, no allocation per element, no resizing

Dynamic Array Queue:

  • Same as fixed circular array but resize doubles capacity
  • Re-linearize on resize: copy logical order (front first), reset front=0, rear=size
  • O(1) amortized enqueue — geometric series proof: total copy work ≈ n

Linked List Queue:

  • head (front) and tail (back) — both maintained at all times
  • Enqueue: tail.next = newNode; tail = newNode (special-case empty queue)
  • Dequeue: head = head.next; if head == null then tail = null — the CRITICAL tail-null step
  • True O(1) — no amortized cost, no resizing

Two-Stack Queue:

  • inbox receives all enqueues; outbox serves all dequeues
  • Transfer only when outbox is empty — lazy transfer = amortized O(1) dequeue
  • Amortized proof: each element pushed once, transferred once, popped once = O(1) per element
  • The canonical interview design pattern

In the next topic, you will explore Queue Collection Frameworks — the built-in queue implementations across Java, Python, C++, and JavaScript with their complete APIs, performance characteristics, and when to choose each.

Queue Implementation