Module 4.2

Queues & Priority Queues

Think of a queue like a line at a coffee shop - first come, first served! Master Queue data structures from simple FIFO queues to double-ended queues and priority queues (heaps). You'll learn BFS traversal, task scheduling, and solve classic problems like sliding window maximum.

45 min read
Intermediate
High Frequency
What You'll Learn
  • FIFO Queue operations
  • Double-ended Deque
  • Priority Queue (Heap)
  • BFS traversal
  • Sliding window maximum
Contents
01

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
02

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:
  1. Choose your Queue: Use ArrayDeque for better performance, or LinkedList if you need null support
  2. offer(element): Adds element to the back, returns true on success, false if full
  3. peek(): Look at the front element without removing it, returns null if empty
  4. poll(): Remove and return the front element, returns null if empty
  5. 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:
  1. Wrap-around logic: (rear + 1) % capacity makes the pointer "wrap" to index 0 after reaching the end
  2. enQueue: Move rear forward, then insert the element
  3. deQueue: Move front forward (logically removing the element)
  4. 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 -> ...
03

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:
  1. Two ends: Deque allows operations at both front and back (very flexible!)
  2. As a Stack: Use push() and pop() (both work on the front)
  3. As a Queue: Use add() (back) and poll() (front) for FIFO behavior
  4. 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
04

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:
  1. Min-Heap by default: Smallest element is always at the front (root of heap)
  2. Max-Heap: Use Collections.reverseOrder() or (a, b) -> b - a
  3. Custom objects: Provide a Comparator to define priority order
  4. 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
}
05

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:
  1. Initialize: Add starting node to queue, mark as visited
  2. Process level: Get current queue size to process all nodes at this level
  3. Explore neighbors: For each node, add unvisited neighbors to the queue
  4. Distance tracking: Increment distance after processing each level (for shortest path)
  5. 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
06

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:
  1. Min-heap of size K: Always keeps the K largest elements seen so far
  2. Add each element: offer() adds to heap in O(log k) time
  3. Maintain size K: If heap grows beyond K, remove the smallest (root)
  4. 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:
  1. Store indices: Deque holds indices (not values) to track window boundaries
  2. Remove expired: pollFirst() removes indices outside current window
  3. Maintain decreasing order: pollLast() removes smaller elements (they can never be max)
  4. 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:
  1. Count frequencies: Use HashMap to count occurrences of each element
  2. Min-heap by frequency: Comparator sorts by frequency (least frequent at root)
  3. Maintain size K: Remove least frequent when heap exceeds K elements
  4. 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
07

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: stackIn for push, stackOut for pop/peek
  • Push: Always push to stackIn
  • Pop/Peek: If stackOut is empty, transfer all elements from stackIn

Time: O(1) amortized for all operations

LeetCode #232

Problem: 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, and size (or use count)
  • Handle edge cases: empty queue, full queue

Time: O(1) for all operations

LeetCode #622

Problem: 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 #239

Problem: 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 #621

Problem: 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 #994

Key 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

Question 1 of 6

What is the time complexity of offer() in PriorityQueue?

Question 2 of 6

Which data structure is best for BFS traversal?

Question 3 of 6

To find K largest elements, what size min-heap should you maintain?

Question 4 of 6

What does FIFO stand for?

Question 5 of 6

What is the time complexity of peek() operation in PriorityQueue?

Question 6 of 6

Which Java class is best for implementing a simple Queue?