DSA Tutorial
🔍

Queue Operations

Operations Overview

A queue exposes a minimal, well-defined set of operations. In interviews, every queue-based algorithm calls only these — knowing them precisely removes all ambiguity from implementation.

Core operations (must know):
  enqueue(value) / offer(value)  → add to back              O(1)
  dequeue()      / poll()        → remove and return front   O(1)
  peek()         / front()       → read front without remove O(1)
  isEmpty()                      → true if no elements       O(1)

Supporting operations:
  size()    → number of elements           O(1)
  clear()   → remove all elements          O(n) or O(1)

All core operations are O(1). The queue never needs to search, compare, or shift elements during these operations — it only ever touches the front or the back.

Implementation 1: Array-Backed Queue

An array-backed queue uses a dynamic array as underlying storage. The front is always array[0] and the back is always array[size - 1]. The critical warning: never use plain array index 0 removal — it is O(n). Use a circular array or dynamic language's built-in.

For clarity in this section, the implementations use the dynamic array built-ins that handle this correctly.

Java
1import java.util.ArrayList; 2import java.util.NoSuchElementException; 3 4public class ArrayQueue<T> { 5 6 private ArrayList<T> data; 7 8 public ArrayQueue() { 9 data = new ArrayList<>(); 10 } 11 12 // ENQUEUE — add to back — O(1) amortized 13 public void enqueue(T value) { 14 data.add(value); 15 } 16 17 // DEQUEUE — remove and return front — O(n) for ArrayList (shifting) 18 // Note: In practice use ArrayDeque for O(1). This shows the naive approach. 19 public T dequeue() { 20 if (isEmpty()) { 21 throw new NoSuchElementException("Dequeue from empty queue"); 22 } 23 return data.remove(0); // O(n) — all elements shift left 24 } 25 26 // PEEK — read front without removing — O(1) 27 public T peek() { 28 if (isEmpty()) { 29 throw new NoSuchElementException("Peek at empty queue"); 30 } 31 return data.get(0); 32 } 33 34 // ISEMPTY — O(1) 35 public boolean isEmpty() { 36 return data.isEmpty(); 37 } 38 39 // SIZE — O(1) 40 public int size() { 41 return data.size(); 42 } 43 44 // CLEAR — O(1) reference drop (GC handles nodes) 45 public void clear() { 46 data.clear(); 47 } 48 49 @Override 50 public String toString() { 51 if (isEmpty()) return "Queue: [] (empty)"; 52 return "Queue (front→back): " + data.toString(); 53 } 54 55 public static void main(String[] args) { 56 ArrayQueue<Integer> q = new ArrayQueue<>(); 57 58 q.enqueue(10); q.enqueue(20); q.enqueue(30); 59 System.out.println(q); // [10, 20, 30] 60 System.out.println("Peek: " + q.peek()); // 10 61 System.out.println("Size: " + q.size()); // 3 62 System.out.println("Dequeue: " + q.dequeue()); // 10 63 System.out.println("Dequeue: " + q.dequeue()); // 20 64 System.out.println(q); // [30] 65 System.out.println("isEmpty: " + q.isEmpty()); // false 66 q.clear(); 67 System.out.println("After clear: " + q.isEmpty()); // true 68 } 69}
Output:
Queue (front→back): [10, 20, 30]
Peek:    10
Size:    3
Dequeue: 10
Dequeue: 20
Queue (front→back): [30]
isEmpty: false
After clear isEmpty: true

Implementation 2: Linked-List-Backed Queue

A linked-list-backed queue maintains both a head (front) and a tail (back) pointer. Enqueue attaches a new node to the tail — O(1). Dequeue removes the head node — O(1). Both operations are true O(1) with no amortized cost.

Linked-list queue state:

  head → [10] → [20] → [30] ← tail
  front                       back

  Enqueue 40: new node(40), tail.next = node(40), tail = node(40)
  head → [10] → [20] → [30] → [40] ← tail

  Dequeue: value = head.data = 10, head = head.next = node(20)
  head → [20] → [30] → [40] ← tail   returns 10
Java
1import java.util.NoSuchElementException; 2 3public class LinkedQueue<T> { 4 5 private static class Node<T> { 6 T data; 7 Node<T> next; 8 Node(T data) { this.data = data; this.next = null; } 9 } 10 11 private Node<T> head; // front of queue 12 private Node<T> tail; // back of queue 13 private int size; 14 15 public LinkedQueue() { 16 head = null; 17 tail = null; 18 size = 0; 19 } 20 21 // ENQUEUE — append new node to tail — O(1) 22 public void enqueue(T value) { 23 Node<T> newNode = new Node<>(value); 24 if (tail == null) { 25 // Empty queue — new node is both head and tail 26 head = newNode; 27 tail = newNode; 28 } else { 29 tail.next = newNode; // Old tail points to new node 30 tail = newNode; // New node becomes the new tail 31 } 32 size++; 33 } 34 35 // DEQUEUE — remove head node — O(1) 36 public T dequeue() { 37 if (isEmpty()) throw new NoSuchElementException("Dequeue from empty queue"); 38 T value = head.data; 39 head = head.next; // Move head forward 40 if (head == null) tail = null; // Queue is now empty — clear tail too 41 size--; 42 return value; 43 } 44 45 // PEEK — read head without removing — O(1) 46 public T peek() { 47 if (isEmpty()) throw new NoSuchElementException("Peek at empty queue"); 48 return head.data; 49 } 50 51 public boolean isEmpty() { return head == null; } 52 public int size() { return size; } 53 54 // CLEAR — O(1) — drop both pointers 55 public void clear() { head = null; tail = null; size = 0; } 56 57 @Override 58 public String toString() { 59 if (isEmpty()) return "Queue: [] (empty)"; 60 StringBuilder sb = new StringBuilder("Queue (front→back): ["); 61 Node<T> curr = head; 62 while (curr != null) { 63 sb.append(curr.data); 64 if (curr.next != null) sb.append(", "); 65 curr = curr.next; 66 } 67 return sb.append("]").toString(); 68 } 69 70 public static void main(String[] args) { 71 LinkedQueue<String> q = new LinkedQueue<>(); 72 73 q.enqueue("A"); q.enqueue("B"); q.enqueue("C"); 74 System.out.println(q); // [A, B, C] 75 System.out.println("Peek: " + q.peek()); // A 76 System.out.println("Size: " + q.size()); // 3 77 System.out.println("Dequeue: " + q.dequeue()); // A 78 System.out.println("Dequeue: " + q.dequeue()); // B 79 System.out.println(q); // [C] 80 81 q.clear(); 82 System.out.println("After clear: " + q); // [] (empty) 83 } 84}
Output:
Queue (front→back): [A, B, C]
Peek:    A
Size:    3
Dequeue: A
Dequeue: B
Queue (front→back): [C]
After clear: Queue: [] (empty)

Dry Run: Enqueue and Dequeue Sequence

Tracing every pointer change in the linked-list queue:

LinkedQueue, head=null, tail=null, size=0

enqueue("A"):
  newNode = Node("A")
  tail == null → head = node(A), tail = node(A)
  size = 1
  State: head → [A] ← tail

enqueue("B"):
  newNode = Node("B")
  tail.next = node(B)       → [A] → [B]
  tail      = node(B)
  size = 2
  State: head → [A] → [B] ← tail

enqueue("C"):
  newNode = Node("C")
  tail.next = node(C)       → [A] → [B] → [C]
  tail      = node(C)
  size = 3
  State: head → [A] → [B] → [C] ← tail

peek():
  return head.data = "A"   ← head unchanged

dequeue():
  value = head.data = "A"
  head  = head.next = node(B)
  head ≠ null → tail unchanged
  size = 2
  returns "A"
  State: head → [B] → [C] ← tail

dequeue():
  value = head.data = "B"
  head  = head.next = node(C)
  size = 1
  returns "B"
  State: head → [C] ← tail

dequeue():
  value = head.data = "C"
  head  = head.next = null
  head == null → tail = null   ← MUST clear tail when queue empties
  size = 0
  returns "C"
  State: head=null, tail=null

Operation 1: Enqueue — Deep Dive

Enqueue places a new element at the back. Both array-backed and linked-list-backed give O(1).

Linked-list enqueue — three cases:

Case 1: Empty queue (head == null, tail == null)
  New node becomes both head AND tail.
  head = newNode; tail = newNode;
  Critical: forgetting to set head causes a broken queue where
  the tail is set but peek/dequeue crash on null head.

Case 2: Non-empty queue (standard)
  tail.next = newNode;   // old tail points forward to new node
  tail      = newNode;   // new node becomes the new tail
  head is unchanged.

Array-backed enqueue:
  data.add(value);       // append to end — O(1) amortized
  No index tracking needed — the array manages its own size.
Java
1import java.util.Deque; 2import java.util.ArrayDeque; 3 4public class EnqueueDemo { 5 6 public static void main(String[] args) { 7 // Java preferred: Queue interface via ArrayDeque 8 Deque<Integer> queue = new ArrayDeque<>(); 9 10 // Enqueue methods 11 queue.offer(10); // Returns false if full (never for ArrayDeque) — preferred 12 queue.add(20); // Throws IllegalStateException if full — same for ArrayDeque 13 queue.offerLast(30); // Explicit "add to back" (same as offer for queue use) 14 15 System.out.println("After 3 enqueues: " + queue); // [10, 20, 30] 16 17 // Enqueue multiple — no built-in bulk enqueue; loop it 18 int[] batch = {40, 50, 60}; 19 for (int v : batch) queue.offer(v); 20 System.out.println("After batch: " + queue); // [10, 20, 30, 40, 50, 60] 21 22 System.out.println("Size: " + queue.size()); // 6 23 } 24}

Operation 2: Dequeue — Deep Dive

Dequeue removes and returns the front element. The most critical edge case is dequeuing from an empty queue.

Linked-list dequeue — two cases:

Case 1: Queue becomes empty after dequeue (head.next == null)
  value = head.data
  head  = head.next  → head = null
  MUST also set tail = null   ← forgetting this is the #1 linked queue bug
  If tail still points to the deleted node, the next enqueue will
  corrupt the list (tail.next = newNode on a deleted node).

Case 2: Queue still has elements (head.next != null)
  value = head.data
  head  = head.next
  tail is unchanged.

Safe vs throwing dequeue (Java):
  poll()    → returns null if empty — safe
  remove()  → throws NoSuchElementException if empty — throwing
Java
1import java.util.Deque; 2import java.util.ArrayDeque; 3 4public class DequeueDemo { 5 6 public static void main(String[] args) { 7 Deque<Integer> queue = new ArrayDeque<>(); 8 queue.offer(10); queue.offer(20); queue.offer(30); 9 10 // Throwing dequeue — use when empty is a bug 11 System.out.println("remove(): " + queue.remove()); // 10 12 System.out.println("removeFirst(): " + queue.removeFirst()); // 20 13 14 // Safe dequeue — use when empty is expected 15 System.out.println("poll(): " + queue.poll()); // 30 16 System.out.println("pollFirst(): " + queue.pollFirst()); // null (empty!) 17 18 // Never call remove() on empty — it throws 19 try { 20 queue.remove(); 21 } catch (java.util.NoSuchElementException e) { 22 System.out.println("remove() on empty throws: " 23 + e.getClass().getSimpleName()); 24 } 25 26 // Typical interview pattern: check before dequeue 27 Deque<Integer> q2 = new ArrayDeque<>(); 28 q2.offer(42); 29 if (!q2.isEmpty()) { 30 System.out.println("Safe dequeue: " + q2.poll()); // 42 31 } 32 } 33}
Output (Java):
remove():    10
removeFirst(): 20
poll():      30
pollFirst(): null
remove() on empty throws: NoSuchElementException
Safe dequeue: 42

Operation 3: Peek — Reading Without Consuming

Peek reads the front element without removing it. It is O(1) and does not change the queue's state.

Why peek matters in queue algorithms:

  BFS example:
    Before processing the front node, check if it is the target.
    If yes, return — no need to dequeue unnecessarily.
    But: in most BFS implementations, you dequeue then check —
    peek is less commonly needed than in stack algorithms.

  Where peek IS essential:
    Sliding window maximum (monotonic deque):
      Peek the front to check if it has expired (outside the window).
      Only dequeue it if it is outside. Peek avoids unconsuming.

    Two-queue median or priority simulations:
      Peek both queue fronts to decide which is smaller before dequeuing.
Java
1import java.util.Deque; 2import java.util.ArrayDeque; 3 4public class PeekDemo { 5 6 public static void main(String[] args) { 7 Deque<Integer> queue = new ArrayDeque<>(); 8 queue.offer(10); queue.offer(20); queue.offer(30); 9 10 // Peek methods — all read front without removing 11 System.out.println("peek(): " + queue.peek()); // 10 — null if empty 12 System.out.println("peekFirst():" + queue.peekFirst()); // 10 — null if empty 13 System.out.println("element(): " + queue.element()); // 10 — throws if empty 14 System.out.println("getFirst(): " + queue.getFirst()); // 10 — throws if empty 15 16 System.out.println("Size unchanged: " + queue.size()); // 3 17 18 // Safe peek pattern 19 Integer front = queue.peek(); // null if empty — safe 20 if (front != null) { 21 System.out.println("Front value: " + front); 22 } 23 24 // Peek empty queue 25 queue.clear(); 26 System.out.println("Peek empty (safe): " + queue.peek()); // null 27 try { 28 queue.element(); // throws 29 } catch (java.util.NoSuchElementException e) { 30 System.out.println("element() on empty throws"); 31 } 32 } 33}

Method Variant Comparison (Java)

Java's Queue/Deque interface provides two variants for each operation: one that throws on failure and one that returns a sentinel value. Always use the sentinel-returning variant (offer/poll/peek) in algorithm code.

OperationThrows on failureReturns sentinelWhen to use sentinel version
Enqueueadd(e) — throws IllegalStateExceptionoffer(e) — returns falseBounded queues; or always — cleaner
Dequeueremove() — throws NoSuchElementExceptionpoll() — returns nullBFS loops; anytime empty is possible
Peekelement() — throws NoSuchElementExceptionpeek() — returns nullChecking front before deciding to dequeue
Rule of thumb:
  Use offer/poll/peek in interview code — they never throw.
  State "I'll use poll() which returns null on empty" and move on.
  This shows you know the API and avoids unnecessary try-catch.

Operation Complexity Summary

OperationArray-Backed (built-in)Linked-List-BackedNotes
enqueueO(1) amortizedO(1) alwaysLinked list has no amortized cost
dequeueO(1) circular / O(n) plainO(1)Plain array dequeue shifts all elements
peekO(1)O(1)Read head — no traversal
isEmptyO(1)O(1)Stored as a field
sizeO(1)O(1)Stored as a field
clearO(n) elements, O(1) refO(n) C++ / O(1) GCC++ must free each node
Build from n elementsO(n)O(n)n enqueues

Common Mistakes Beginners Make

Not clearing tail when the queue becomes empty after dequeue. In linked-list implementation, after the last dequeue, head becomes null — but tail still points to the deleted node. The next enqueue sets tail.next = newNode on a dangling pointer, silently corrupting the list. Always check if (head == null) tail = null immediately after moving head.

Using list.pop(0) or array.shift() as dequeue. Both are O(n) — they shift every element one position. In a BFS over n nodes, this turns O(n) BFS into O(n²). Always use collections.deque.popleft() in Python and a circular array or linked-list implementation in JavaScript.

Calling queue.remove() or queue.element() instead of queue.poll() / queue.peek() in Java. The throwing variants crash if the queue is empty. In BFS and most algorithm code, the queue empties as a normal part of execution — use poll() which returns null, and check with isEmpty() before loops.

Forgetting to handle the single-element queue case in linked implementation. When a queue has one element and you dequeue it, both head and tail become null. But if only head is set to null without clearing tail, the queue appears empty to isEmpty() (which checks head == null) but tail still points to stale memory. The next enqueue then sets tail.next = newNode on garbage.

Calling front() or pop() on an empty std::queue in C++. Unlike Java or Python, C++ does not throw an exception — it is undefined behavior. The program may crash, return garbage, or silently corrupt memory. Always call empty() before front() or pop() in C++.

Interview Questions

Q: What is the time complexity of dequeue and why does implementation choice matter?

For a linked-list-backed queue, dequeue is O(1) — move the head pointer forward, return the old head's data. For a circular-array-backed queue, dequeue is also O(1) — increment the front index and return the old value. For a plain (non-circular) array, dequeue is O(n) — every element shifts left to fill the gap at index 0. The implementation choice is critical: using a plain array for a queue turns every O(n) BFS into O(n²) because each dequeue during the traversal costs O(n).

Q: In Java, what is the difference between poll() and remove() on a Queue?

Both dequeue from the front. remove() throws NoSuchElementException when the queue is empty. poll() returns null when the queue is empty — it never throws. For algorithm and interview code, poll() is always preferred because an empty queue is a normal terminal state in BFS and other queue-based algorithms, not an exceptional condition. Similarly, peek() returns null while element() throws, and offer() returns false while add() throws.

Q: Why must tail be set to null when the last element is dequeued from a linked-list queue?

After the last dequeue, head becomes null — correctly signalling an empty queue. But if tail still points to the just-deleted node, the pointer is dangling. The next call to enqueue() enters the non-empty branch (because tail != null) and sets tail.next = newNode on the deleted memory. This corrupts the queue — the new node is attached to a stale, possibly freed node. Setting tail = null whenever head becomes null prevents this.

FAQs

In C++, why does queue::pop() return void?

The same reason as stack::pop() — exception safety. If pop() returned a value by copy and the copy constructor threw, the element would be removed without being successfully returned — data lost. The two-step front() + pop() separates read from remove: if the copy in front() throws, the element is still in the queue. This is deliberate C++ API design. In C++17 and later, std::optional<T> could return the value safely, but the API remains void for backward compatibility.

Should I use ArrayDeque or LinkedList as a Queue in Java?

ArrayDeque for almost everything. It is array-backed with automatic resizing — better cache locality, no per-node allocation overhead (no pointer per element), and 2-3x faster in benchmarks than LinkedList. Use LinkedList only when you also need mid-list operations or need both List and Queue operations simultaneously on the same object. The Java docs explicitly recommend ArrayDeque over LinkedList for stack and queue use.

Can I use queue.size() to check emptiness instead of queue.isEmpty()?

Yes, but isEmpty() is clearer and is O(1) for all implementations. size() == 0 is also O(1) for standard implementations. Use isEmpty() — it directly expresses intent and avoids any ambiguity about zero-size vs null.

Quick Quiz

Question 1: For a linked-list queue, which pointer must be updated during enqueue?

  • A) Only head
  • B) Only tail
  • C) Both head and tail when empty; only tail when non-empty
  • D) Always both head and tail

Answer: C) Both when empty, only tail when non-empty. If the queue is empty, the new node becomes both head and tail — both must be set. If the queue is non-empty, the new node is appended by setting tail.next = newNode and then tail = newNode. The head is never changed during enqueue (it still points to the original front element).

Question 2: queue.poll() is called on an empty Java ArrayDeque. What is returned?

  • A) 0
  • B) Throws NoSuchElementException
  • C) null
  • D) Throws EmptyQueueException

Answer: C) null. poll() is the safe variant — it returns null when the queue is empty instead of throwing. Use remove() if you want the exception. In interview code, always use poll() to avoid crashes when the queue empties.

Question 3: What is wrong with using list.pop(0) as dequeue in Python inside a BFS loop over n nodes?

  • A) It removes the wrong element — should use list.pop(-1)
  • B) Each pop(0) is O(n) due to element shifting; n calls = O(n²) total
  • C) Python lists cannot be used with BFS
  • D) pop(0) throws IndexError when the list is empty

Answer: B) O(n²) total. list.pop(0) removes the first element by shifting all remaining elements left — O(n). Called n times in a BFS over n nodes, total cost is O(n²). Use collections.deque.popleft() which is O(1) — BFS stays O(V + E).

Question 4: In the linked-list queue, after dequeuing the LAST element, what must happen to tail?

  • A) tail should remain pointing to the last dequeued node for reference
  • B) tail should be set to head (which is now null)
  • C) tail must be set to null — otherwise the next enqueue corrupts the list
  • D) tail is automatically cleared when head becomes null

Answer: C) tail must be explicitly set to null. When head = head.next results in head == null, the queue is empty. But tail still holds a pointer to the just-removed node. The next enqueue() call enters the non-empty branch (because tail != null) and sets tail.next = newNode on the deleted memory — undefined behavior in C++, dangling reference in Java/Python, silent corruption. Setting tail = null whenever head becomes null is mandatory.

Summary

Queue operations are O(1) for enqueue, dequeue, and peek — but only if the underlying implementation is correct. The plain array is the hidden O(n) trap.

The complete operation reference:

  • enqueue(value) — O(1) amortized (array) or O(1) (linked list) — always at the back
  • dequeue() — O(1) circular array or linked list, O(n) plain array — removes and returns front
  • peek() — O(1) — reads front without consuming — use peek()/poll() in Java, not throwing variants
  • isEmpty() — O(1) — essential guard before every dequeue/peek
  • size() — O(1) — stored as a field
  • clear() — O(1) GC languages / O(n) C++ (must free each node)

Linked-list queue: maintain both head (front) and tail (back) — update tail on enqueue, update head on dequeue, clear tail when queue becomes empty.

Array-backed queue: use collections.deque (Python), ArrayDeque (Java), std::queue (C++), or implement circular array — never use plain array index-0 removal.

In Java, always use offer/poll/peek — the null-returning safe variants. Never call element() or remove() in algorithm code where empty is a normal terminal state.

In the next topic you will explore Queue Implementation — building a complete queue from scratch with both the linked-list and circular array approaches, with full edge case coverage and resize logic.

Queue Operations