Queue Basics (FIFO)
Queue (FIFO)
A First-In-First-Out data structure where elements are added at the back (enqueue) and removed from the front (dequeue). Like a line of people waiting - first come, first served.
Enqueue
Add element to back. O(1) time.
Dequeue
Remove from front. O(1) time.
Peek/Front
View front element. O(1) time.
isEmpty
Check if queue empty. O(1) time.
Visualizing Queue Operations (FIFO)
ENQUEUE (add to back) DEQUEUE (remove from front)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Initial Queue: [A] - [B] - [C]
^ ^
FRONT BACK
Enqueue('D'): [A] - [B] - [C] - [D] <- New element added at BACK
^ ^
FRONT BACK
Dequeue(): [B] - [C] - [D] -> 'A' removed from FRONT (returned)
^ ^
FRONT BACK
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FIFO: First In, First Out - Just like a real-world line!
The person who joins first gets served first.
| Operation | Queue Method | LinkedList Method | On Failure |
|---|---|---|---|
| Add to back | offer(e) |
add(e) |
offer: false, add: exception |
| Remove from front | poll() |
remove() |
poll: null, remove: exception |
| View front | peek() |
element() |
peek: null, element: exception |
Practice Questions: Queue Basics
Given operations:
offer(5), offer(3), poll(), offer(7), offer(2), poll(), peek()
Task: What value does peek() return? What elements remain in the queue (front to back)?
Show Solution
// Step by step (FIFO order):
// offer(5) -> Queue: [5]
// offer(3) -> Queue: [5, 3]
// poll() -> Returns 5, Queue: [3]
// offer(7) -> Queue: [3, 7]
// offer(2) -> Queue: [3, 7, 2]
// poll() -> Returns 3, Queue: [7, 2]
// peek() -> Returns 7, Queue: [7, 2]
// Answer: peek() returns 7
// Remaining queue (front to back): [7, 2]
Task: Explain the difference between poll() and remove() in Java Queue. When would you use each?
Show Solution
// Both remove and return the front element
// Difference is behavior on EMPTY queue:
Queue q = new LinkedList<>();
// poll() - returns null if empty (safe)
Integer val = q.poll(); // val = null
// remove() - throws NoSuchElementException if empty
Integer val = q.remove(); // THROWS EXCEPTION!
// Best practice:
// Use poll() in most cases (safer)
// Use remove() only when empty queue is a bug
// Safe pattern:
if (!queue.isEmpty()) {
int front = queue.poll();
}
Task: Explain FIFO with a real-world analogy. How does it differ from Stack (LIFO)?
Show Solution
FIFO = First In, First Out
Real-world examples:
1. Line at a store - first person in line served first
2. Printer queue - first document prints first
3. Customer service - first caller answered first
Queue vs Stack:
- Queue (FIFO): Add at back, remove from front
- Stack (LIFO): Add at top, remove from top
Use Queue when:
- Order of arrival matters
- Fair processing (first come, first served)
- BFS traversal, level-order processing
Queue in Java
Java provides multiple ways to implement a Queue. The Queue interface is part of the
Java Collections Framework and offers several implementations to choose from based on your needs.
Java Queue Interface
The Queue interface in Java extends the Collection interface and provides
methods for FIFO operations. Unlike arrays, you don't access elements by index. Instead, you
use offer() to add, poll() to remove, and peek() to view
the front element.
Two main implementations: LinkedList (doubly-linked, allows nulls)
and ArrayDeque (resizable array, faster, no nulls). For most cases, prefer
ArrayDeque for better performance.
import java.util.Queue;
import java.util.LinkedList;
import java.util.ArrayDeque;
// Using LinkedList (common)
Queue queue1 = new LinkedList<>();
// Using ArrayDeque (preferred - faster)
Queue queue2 = new ArrayDeque<>();
// Basic operations
queue1.offer(1); // Add to back
queue1.offer(2);
queue1.offer(3); // Queue: [1, 2, 3]
int front = queue1.peek(); // 1 (view front)
int removed = queue1.poll(); // 1 (remove front)
// Queue is now: [2, 3]
boolean empty = queue1.isEmpty(); // false
int size = queue1.size(); // 2
// Safe iteration
while (!queue1.isEmpty()) {
System.out.println(queue1.poll());
}
// Using for-each (doesn't remove elements)
for (int num : queue1) {
System.out.println(num);
}
Step-by-Step Breakdown:
- Choose your Queue: Use
ArrayDequefor better performance, orLinkedListif you need null support - offer(element): Adds element to the back, returns
trueon success,falseif full - peek(): Look at the front element without removing it, returns
nullif empty - poll(): Remove and return the front element, returns
nullif empty - Safe iteration: Use
while (!queue.isEmpty())when you need to process all elements
Circular Queue Implementation
class CircularQueue {
private int[] arr;
private int front, rear, size, capacity;
public CircularQueue(int k) {
capacity = k;
arr = new int[k];
front = 0;
rear = -1;
size = 0;
}
public boolean enQueue(int value) {
if (isFull()) return false;
rear = (rear + 1) % capacity; // Wrap around
arr[rear] = value;
size++;
return true;
}
public boolean deQueue() {
if (isEmpty()) return false;
front = (front + 1) % capacity; // Wrap around
size--;
return true;
}
public int Front() {
return isEmpty() ? -1 : arr[front];
}
public int Rear() {
return isEmpty() ? -1 : arr[rear];
}
public boolean isEmpty() {
return size == 0;
}
public boolean isFull() {
return size == capacity;
}
}
How Circular Queue Works:
- Wrap-around logic:
(rear + 1) % capacitymakes the pointer "wrap" to index 0 after reaching the end - enQueue: Move rear forward, then insert the element
- deQueue: Move front forward (logically removing the element)
- Why circular? Reuses empty slots at the beginning when elements are dequeued
Circular Queue Visualization
Regular Array Queue Problem: Circular Queue Solution:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
After several enqueue/dequeue: ┌───────┐
┌───┬───┬───┬───┬───┐ ┌─▶ │ 0 │ ──┐
│ X │ X │ C │ D │ E │ ← Can't add more! │ └───────┘ │
└───┴───┴───┴───┴───┘ │ ┌───────┐ │
↑ ↑ ↑ ↑ │ │ 1 │ │
wasted front rear front ← └───────┘ │
space │ ┌───────┐ │
│ │ 2 │ │
│ └───────┘ ▼
│ ┌───────┐ │
└── │ 3 │ ◀─┘ rear
└───────┘
Tip: rear = (rear + 1) % capacity -> Wraps around: 3 -> 0 -> 1 -> 2 -> 3...
Practice Questions: Queue Implementation
Task: Compare ArrayDeque and LinkedList as Queue implementations. When would you prefer one over the other?
Show Solution
ArrayDeque (Preferred):
+ Better cache locality (contiguous memory)
+ No node allocation overhead
+ Faster in practice for most operations
+ Resizable array (amortized O(1))
- Cannot store null values
LinkedList:
+ Can store null values
+ No resize needed (true O(1) always)
+ Good if you need List operations too
- Node allocation overhead
- Worse cache performance
Use ArrayDeque when:
- Performance matters (most cases)
- You don't need null values
Use LinkedList when:
- You need to store null
- You also need List operations (get by index)
Task: Implement a Queue using only Stack operations (push, pop, peek, isEmpty).
Hint: Use one stack for input, one for output.
Show Solution
class MyQueue {
Deque stackIn = new ArrayDeque<>();
Deque stackOut = new ArrayDeque<>();
public void push(int x) {
stackIn.push(x);
}
public int pop() {
moveIfNeeded();
return stackOut.pop();
}
public int peek() {
moveIfNeeded();
return stackOut.peek();
}
private void moveIfNeeded() {
if (stackOut.isEmpty()) {
while (!stackIn.isEmpty()) {
stackOut.push(stackIn.pop());
}
}
}
public boolean empty() {
return stackIn.isEmpty() && stackOut.isEmpty();
}
}
// Amortized O(1) for all operations!
Task: Explain why (rear + 1) % capacity is used in Circular Queue. What problem does it solve?
Show Solution
Problem with regular array queue:
- After dequeue, front moves forward
- Space at beginning becomes wasted
- Eventually "full" even with empty slots
Example (capacity 5):
[X, X, C, D, E] - Can't add F even though 2 slots free!
Modulo solution:
rear = (rear + 1) % capacity
When rear = 4 (last index):
(4 + 1) % 5 = 0 <- Wraps to beginning!
This allows:
- Reuse of "wasted" front slots
- Continuous operation without shifting
- True O(1) operations
Array becomes logically circular:
Index: 0 -> 1 -> 2 -> 3 -> 4 -> 0 -> 1 -> ...
Double-Ended Queue (Deque)
What if you need a data structure that works as both a Stack AND a Queue? That's exactly what a Deque (pronounced "deck") offers - the flexibility to add and remove from both ends!
Deque (Double-Ended Queue)
A Deque (Double-Ended Queue) is a linear data structure that allows insertion and deletion at both ends - front and back. Think of it like a line where people can join or leave from either end.
Versatility: Use it as a Stack (push/pop at front), as a
Queue (add at back, remove at front), or leverage both ends for problems like
sliding window maximum. In Java, ArrayDeque is the preferred implementation.
import java.util.Deque;
import java.util.ArrayDeque;
Deque deque = new ArrayDeque<>();
// Add to front
deque.addFirst(1); // or offerFirst(1)
deque.push(0); // Same as addFirst
// Add to back
deque.addLast(2); // or offerLast(2)
deque.add(3); // Same as addLast
// Deque: [0, 1, 2, 3]
// Remove from front
int first = deque.removeFirst(); // or pollFirst()
int front = deque.pop(); // Same as removeFirst
// Remove from back
int last = deque.removeLast(); // or pollLast()
// View without removing
int peekFirst = deque.peekFirst(); // or getFirst()
int peekLast = deque.peekLast(); // or getLast()
// As Stack: push/pop work on front
deque.push(5);
int top = deque.pop();
// As Queue: add works on back, poll on front
deque.add(5); // Add to back
int out = deque.poll(); // Remove from front
Understanding Deque Operations:
- Two ends: Deque allows operations at both front and back (very flexible!)
- As a Stack: Use
push()andpop()(both work on the front) - As a Queue: Use
add()(back) andpoll()(front) for FIFO behavior - Sliding window: Use
offerLast()to add,pollFirst()to remove expired items
| Operation | Front | Back |
|---|---|---|
| Insert | addFirst(), offerFirst() |
addLast(), offerLast() |
| Remove | removeFirst(), pollFirst() |
removeLast(), pollLast() |
| Examine | getFirst(), peekFirst() |
getLast(), peekLast() |
Practice Questions: Deque
Task: Show how to use a single Deque as a Stack (LIFO) and as a Queue (FIFO).
Show Solution
Deque deque = new ArrayDeque<>();
// === Use as STACK (LIFO) ===
deque.push(1); // Add to front
deque.push(2); // Add to front
deque.push(3); // Deque: [3, 2, 1]
deque.pop(); // Returns 3 (from front)
deque.peek(); // Returns 2 (front)
// === Use as QUEUE (FIFO) ===
deque.clear();
deque.offer(1); // Add to back
deque.offer(2); // Add to back
deque.offer(3); // Deque: [1, 2, 3]
deque.poll(); // Returns 1 (from front)
deque.peek(); // Returns 2 (front)
// Key insight:
// Stack: push/pop at FRONT
// Queue: add at BACK, remove at FRONT
Task: Give a real problem where you need operations at both ends of a Deque.
Show Solution
Sliding Window Maximum problem:
Given: [1, 3, -1, -3, 5, 3, 6, 7], k=3
Find maximum in each window of size k
Use Deque to store indices:
- Add new element: pollLast() smaller elements
(they can never be max), then offerLast()
- Remove expired: pollFirst() if outside window
- Get max: peekFirst() always has window max
Why both ends needed:
- Back: Remove smaller elements, add new
- Front: Remove expired elements, get max
This achieves O(n) instead of O(n*k)!
Task: Explain when addFirst vs offerFirst should be used. Same for remove vs poll.
Show Solution
// add/remove: Throw exception on failure
// offer/poll: Return special value on failure
Deque d = new ArrayDeque<>();
// On empty deque:
d.removeFirst(); // Throws NoSuchElementException!
d.pollFirst(); // Returns null (safe)
d.getFirst(); // Throws NoSuchElementException!
d.peekFirst(); // Returns null (safe)
// For bounded deques (rare):
d.addFirst(x); // Throws if full
d.offerFirst(x); // Returns false if full
// Best practice:
// Use offer/poll/peek for safe operations
// Use add/remove/get only when empty = bug
Priority Queue (Heap)
Priority Queue
Elements are dequeued based on priority, not insertion order. Implemented as a binary heap. Default is min-heap (smallest first).
import java.util.PriorityQueue;
import java.util.Collections;
// Min-Heap (default) - smallest element first
PriorityQueue minHeap = new PriorityQueue<>();
minHeap.offer(3);
minHeap.offer(1);
minHeap.offer(2);
minHeap.peek(); // 1 (smallest)
minHeap.poll(); // 1 (removes smallest)
// Heap: [2, 3]
// Max-Heap - largest element first
PriorityQueue maxHeap = new PriorityQueue<>(Collections.reverseOrder());
// Or: new PriorityQueue<>((a, b) -> b - a);
maxHeap.offer(3);
maxHeap.offer(1);
maxHeap.offer(2);
maxHeap.peek(); // 3 (largest)
maxHeap.poll(); // 3 (removes largest)
// Custom comparator for objects
PriorityQueue pq = new PriorityQueue<>(
(a, b) -> a[0] - b[0] // Sort by first element
);
// For custom objects
class Task {
int priority;
String name;
}
PriorityQueue taskQueue = new PriorityQueue<>(
(a, b) -> a.priority - b.priority
);
PriorityQueue Key Concepts:
- Min-Heap by default: Smallest element is always at the front (root of heap)
- Max-Heap: Use
Collections.reverseOrder()or(a, b) -> b - a - Custom objects: Provide a Comparator to define priority order
- Common pattern: Keep heap size at K to find K largest/smallest elements
| Operation | Time | Description |
|---|---|---|
offer(e) |
O(log n) | Add element |
poll() |
O(log n) | Remove highest priority |
peek() |
O(1) | View highest priority |
remove(e) |
O(n) | Remove specific element |
Practice Questions: Priority Queue
Task: Show two ways to create a max-heap in Java.
Show Solution
// Min-Heap (default)
PriorityQueue minHeap = new PriorityQueue<>();
// peek() returns SMALLEST
// Max-Heap - Method 1: Collections.reverseOrder()
PriorityQueue maxHeap1 =
new PriorityQueue<>(Collections.reverseOrder());
// Max-Heap - Method 2: Lambda comparator
PriorityQueue maxHeap2 =
new PriorityQueue<>((a, b) -> b - a);
// Max-Heap - Method 3: Comparator.reverseOrder()
PriorityQueue maxHeap3 =
new PriorityQueue<>(Comparator.reverseOrder());
// All three methods work the same way
// peek() returns LARGEST element
Task: To find the Kth largest element efficiently, should you use a min-heap or max-heap of size K? Explain why.
Show Solution
// Use MIN-HEAP of size K!
// Why? Keep track of K largest elements
// The root (minimum of K largest) = Kth largest
int findKthLargest(int[] nums, int k) {
PriorityQueue minHeap = new PriorityQueue<>();
for (int num : nums) {
minHeap.offer(num);
if (minHeap.size() > k) {
minHeap.poll(); // Remove smallest
}
}
return minHeap.peek(); // Kth largest!
}
// Example: [3,2,1,5,6,4], k=2
// After processing: heap = [5, 6]
// peek() = 5 (2nd largest)
// Why not max-heap?
// Max-heap would need to store all elements
// Then poll K-1 times - less efficient!
Task: Explain why remove(element) is O(n) while poll() is O(log n). How can you work around this?
Show Solution
poll() is O(log n):
- Always removes the ROOT (index 0)
- No searching needed
- Just bubble-down to restore heap property
remove(element) is O(n):
- Must FIND the element first
- Heap is NOT sorted - only parent-child order
- Must scan entire heap to find element
- Then bubble-up/down to restore heap
Workaround for frequent removals:
1. Lazy deletion: Mark as deleted, skip during poll
2. Use TreeMap/TreeSet for O(log n) removal
3. Use indexed priority queue (advanced)
// Lazy deletion pattern:
Set deleted = new HashSet<>();
while (!heap.isEmpty()) {
int top = heap.peek();
if (deleted.contains(top)) {
heap.poll(); // Skip deleted
continue;
}
// Process valid top
}
BFS Applications
Breadth-First Search (BFS) is one of the most important applications of queues. It explores nodes level by level, making it perfect for finding shortest paths in unweighted graphs.
Breadth-First Search (BFS)
BFS is a graph/tree traversal algorithm that explores all neighbors at the current depth before moving to nodes at the next depth level. It uses a Queue to track which nodes to visit next.
Key property: BFS guarantees the shortest path in unweighted graphs because it visits all nodes at distance d before any node at distance d+1. Common uses include: level-order tree traversal, shortest path finding, and flood fill algorithms.
Level Order Traversal
// Level Order Traversal of Binary Tree
List> levelOrder(TreeNode root) {
List> result = new ArrayList<>();
if (root == null) return result;
Queue queue = new LinkedList<>();
queue.offer(root);
while (!queue.isEmpty()) {
int levelSize = queue.size();
List currentLevel = new ArrayList<>();
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll();
currentLevel.add(node.val);
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
result.add(currentLevel);
}
return result;
}
// Shortest Path in Unweighted Graph
int shortestPath(int[][] graph, int start, int end) {
Queue queue = new LinkedList<>();
Set visited = new HashSet<>();
queue.offer(start);
visited.add(start);
int distance = 0;
while (!queue.isEmpty()) {
int size = queue.size();
for (int i = 0; i < size; i++) {
int node = queue.poll();
if (node == end) return distance;
for (int neighbor : graph[node]) {
if (!visited.contains(neighbor)) {
visited.add(neighbor);
queue.offer(neighbor);
}
}
}
distance++;
}
return -1; // No path found
}
// Number of Islands (BFS approach)
int numIslands(char[][] grid) {
int count = 0;
int rows = grid.length, cols = grid[0].length;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (grid[i][j] == '1') {
bfs(grid, i, j);
count++;
}
}
}
return count;
}
void bfs(char[][] grid, int r, int c) {
Queue queue = new LinkedList<>();
queue.offer(new int[]{r, c});
grid[r][c] = '0'; // Mark visited
int[][] dirs = {{0,1}, {1,0}, {0,-1}, {-1,0}};
while (!queue.isEmpty()) {
int[] cell = queue.poll();
for (int[] dir : dirs) {
int nr = cell[0] + dir[0];
int nc = cell[1] + dir[1];
if (nr >= 0 && nr < grid.length &&
nc >= 0 && nc < grid[0].length &&
grid[nr][nc] == '1') {
grid[nr][nc] = '0';
queue.offer(new int[]{nr, nc});
}
}
}
}
BFS Pattern Breakdown:
- Initialize: Add starting node to queue, mark as visited
- Process level: Get current queue size to process all nodes at this level
- Explore neighbors: For each node, add unvisited neighbors to the queue
- Distance tracking: Increment distance after processing each level (for shortest path)
- Grid BFS: Use 4-directional array
{{0,1}, {1,0}, {0,-1}, {-1,0}}for neighbors
Practice Questions: BFS Applications
Task: Explain why BFS uses Queue (FIFO) while DFS uses Stack (LIFO). What happens if you use Stack for BFS?
Show Solution
BFS needs FIFO (Queue):
- Process nodes in order they were discovered
- All nodes at distance d processed before d+1
- Guarantees shortest path in unweighted graphs
DFS uses LIFO (Stack):
- Process most recently discovered node first
- Goes deep before exploring siblings
- Used for backtracking, cycle detection
If you use Stack for "BFS":
- You get DFS instead!
- Nodes processed depth-first, not breadth-first
- Shortest path no longer guaranteed
Example traversal (same graph):
BFS with Queue: A -> B -> C -> D -> E (level by level)
DFS with Stack: A -> B -> D -> E -> C (deep first)
Task: In BFS level-order traversal, why do we do int size = queue.size() before the for loop?
Show Solution
// WRONG - queue.size() changes during loop!
while (!queue.isEmpty()) {
for (int i = 0; i < queue.size(); i++) { // BUG!
// queue.size() changes as we add children
}
}
// CORRECT - capture size before processing level
while (!queue.isEmpty()) {
int levelSize = queue.size(); // Snapshot!
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll();
// Add children (queue grows)
if (node.left != null) queue.offer(node.left);
if (node.right != null) queue.offer(node.right);
}
// Now all nodes at current level processed
// Queue contains only next level nodes
}
// This ensures we process EXACTLY one level per iteration
// Critical for: level grouping, distance tracking
Task: What is multi-source BFS? Give an example problem where it's useful.
Show Solution
// Multi-source BFS: Start from MULTIPLE nodes at once
// Example: Rotting Oranges
// All rotten oranges spread simultaneously
// Add ALL rotten oranges to queue initially!
int orangesRotting(int[][] grid) {
Queue queue = new LinkedList<>();
int fresh = 0;
// Add ALL rotten oranges (multiple sources)
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[0].length; j++) {
if (grid[i][j] == 2) {
queue.offer(new int[]{i, j});
} else if (grid[i][j] == 1) {
fresh++;
}
}
}
// BFS from all sources simultaneously
// Each level = 1 minute of rotting
}
// Other multi-source examples:
// - 01 Matrix: Distance from any 0
// - Walls and Gates: Distance from any gate
Common Problems
These classic interview problems demonstrate the power of queues and heaps. Master these patterns and you'll be ready for most queue-related coding challenges!
Queue Problem Patterns
Most queue problems fall into a few patterns: Top K elements (use heap of size K), Sliding window (use monotonic deque), and Task scheduling (use heap with cooldown tracking).
Key insight: For "K largest" use a min-heap; for "K smallest" use a max-heap. This counterintuitive approach keeps heap size at K, giving O(n log k) instead of O(n log n).
Kth Largest Element
// Find kth largest in array using min-heap of size k
int findKthLargest(int[] nums, int k) {
PriorityQueue minHeap = new PriorityQueue<>();
for (int num : nums) {
minHeap.offer(num);
if (minHeap.size() > k) {
minHeap.poll(); // Remove smallest
}
}
return minHeap.peek(); // Kth largest
}
How Kth Largest Works:
- Min-heap of size K: Always keeps the K largest elements seen so far
- Add each element:
offer()adds to heap in O(log k) time - Maintain size K: If heap grows beyond K, remove the smallest (root)
- Result: After processing all elements, the heap root is the Kth largest
Sliding Window Maximum
// Maximum in each sliding window of size k
// Using Monotonic Deque - O(n)
int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
int[] result = new int[n - k + 1];
Deque deque = new ArrayDeque<>(); // Store indices
for (int i = 0; i < n; i++) {
// Remove indices outside window
while (!deque.isEmpty() && deque.peekFirst() < i - k + 1) {
deque.pollFirst();
}
// Remove smaller elements (they can't be max)
while (!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) {
deque.pollLast();
}
deque.offerLast(i);
// Add to result when window is complete
if (i >= k - 1) {
result[i - k + 1] = nums[deque.peekFirst()];
}
}
return result;
}
Monotonic Deque Technique:
- Store indices: Deque holds indices (not values) to track window boundaries
- Remove expired:
pollFirst()removes indices outside current window - Maintain decreasing order:
pollLast()removes smaller elements (they can never be max) - Get maximum:
peekFirst()always gives the index of current window maximum
Top K Frequent Elements
int[] topKFrequent(int[] nums, int k) {
// Count frequencies
Map count = new HashMap<>();
for (int num : nums) {
count.put(num, count.getOrDefault(num, 0) + 1);
}
// Min-heap by frequency
PriorityQueue heap = new PriorityQueue<>(
(a, b) -> count.get(a) - count.get(b)
);
for (int num : count.keySet()) {
heap.offer(num);
if (heap.size() > k) {
heap.poll();
}
}
int[] result = new int[k];
for (int i = 0; i < k; i++) {
result[i] = heap.poll();
}
return result;
}
Top K Frequent Pattern:
- Count frequencies: Use HashMap to count occurrences of each element
- Min-heap by frequency: Comparator sorts by frequency (least frequent at root)
- Maintain size K: Remove least frequent when heap exceeds K elements
- Extract result: Remaining K elements are the most frequent
Practice Questions: Common Problems
Task: The naive approach to sliding window maximum is O(n*k). Explain why the monotonic deque solution is O(n).
Show Solution
Naive approach: O(n * k)
- For each window, scan all k elements to find max
- n windows * k elements = O(n * k)
Monotonic Deque: O(n)
- Each element is added to deque at most ONCE
- Each element is removed from deque at most ONCE
- Total operations: 2n = O(n)
Key insight: Amortized analysis
- We don't scan k elements per window
- We maintain a "running max" structure
- pollFirst(): Remove expired (outside window)
- pollLast(): Remove smaller elements (never max)
- peekFirst(): Always has current window max
Why remove smaller elements?
- If nums[i] > nums[j] and i > j
- nums[j] can NEVER be max while nums[i] exists
- Safe to remove nums[j] permanently
Task: In Top K Frequent Elements, we use a min-heap sorted by frequency. Why not max-heap?
Show Solution
// Min-heap of size K: O(n log k)
// We want K MOST frequent
// Why min-heap works:
// - Keep only K elements in heap
// - When heap size > k, remove SMALLEST frequency
// - Result: K largest frequencies remain!
// Why not max-heap?
// - Would need to store ALL elements first
// - Then poll K times
// - Space: O(n), Time: O(n log n)
// Min-heap approach:
PriorityQueue heap = new PriorityQueue<>(
(a, b) -> count.get(a) - count.get(b) // Min by frequency
);
for (int num : count.keySet()) {
heap.offer(num);
if (heap.size() > k) {
heap.poll(); // Remove least frequent
}
}
// Heap now contains K most frequent!
// Time: O(n log k) - much better than O(n log n)
// Space: O(k) - much better than O(n)
Task: Given K sorted linked lists, merge them into one sorted list. Use PriorityQueue for O(n log k) solution.
Show Solution
ListNode mergeKLists(ListNode[] lists) {
// Min-heap of list heads
PriorityQueue pq = new PriorityQueue<>(
(a, b) -> a.val - b.val
);
// Add all non-null heads
for (ListNode head : lists) {
if (head != null) pq.offer(head);
}
ListNode dummy = new ListNode(0);
ListNode curr = dummy;
while (!pq.isEmpty()) {
ListNode smallest = pq.poll();
curr.next = smallest;
curr = curr.next;
// Add next node from same list
if (smallest.next != null) {
pq.offer(smallest.next);
}
}
return dummy.next;
}
// Time: O(n log k) where n = total nodes
// Space: O(k) for the heap
Practice Problems
Test your queue skills with these classic interview problems. Start with the easier ones and work your way up!
Problem: Implement a FIFO queue using only two stacks. The queue should support push, pop, peek, and empty operations.
Approach:
- Use two stacks:
stackInfor push,stackOutfor pop/peek - Push: Always push to
stackIn - Pop/Peek: If
stackOutis empty, transfer all elements fromstackIn
Time: O(1) amortized for all operations
LeetCode #232Problem: Design your implementation of the circular queue with a fixed size array.
Approach:
- Use modulo operator for wrap-around:
(index + 1) % capacity - Track
front,rear, andsize(or use count) - Handle edge cases: empty queue, full queue
Time: O(1) for all operations
LeetCode #622Problem: Given an array and window size k, return the maximum in each sliding window.
Approach:
- Use a monotonic decreasing deque to store indices
- Remove indices outside the current window from front
- Remove smaller elements from back (they can never be max)
- Front of deque always has the maximum for current window
Time: O(n), each element is added and removed at most once
LeetCode #239Problem: Given tasks and a cooling interval n, find the minimum time to complete all tasks.
Approach:
- Use a max-heap to always pick the most frequent task
- Use a queue to track tasks in cooldown with their ready time
- At each time unit, process one task or idle if all are cooling down
Time: O(n × m) where n is tasks and m is the cooldown period
LeetCode #621Problem: Given a grid with fresh (1) and rotten (2) oranges, return minimum minutes for all oranges to rot.
Approach:
- Add all rotten oranges to queue initially (multi-source BFS)
- Process level by level (each level = 1 minute)
- For each rotten orange, rot adjacent fresh oranges
- Track remaining fresh oranges to check if all can be rotted
Time: O(rows × cols)
LeetCode #994Key Takeaways
FIFO Queue
First In, First Out. Use for BFS, level-order traversal, scheduling.
Deque = Both Ends
Add/remove from both ends. Perfect for sliding window problems.
PriorityQueue = Heap
O(log n) insert/remove. Use for K largest/smallest problems.
BFS with Queue
Level-by-level traversal, shortest path in unweighted graphs.
Circular Queue
Use modulo for wrap-around. Reuses space efficiently in fixed-size buffers.
Monotonic Deque
O(n) sliding window max/min. Remove smaller/larger elements from back.
Knowledge Check
What is the time complexity of offer() in PriorityQueue?
Which data structure is best for BFS traversal?
To find K largest elements, what size min-heap should you maintain?
What does FIFO stand for?
What is the time complexity of peek() operation in PriorityQueue?
Which Java class is best for implementing a simple Queue?