Queues — The Complete Visual Reference

Queue internals, Kotlin types, core operations, 16 algorithm patterns — visual diagrams, complexity tags & key insights
What Is a Queue?
Queue = FIFO (First In, First Out) data structure Enqueue 10, 20, 30: enqueue → ┌────┬────┬────┐ → dequeue │ 102030 │ └────┴────┴────┘ front back Dequeue → returns 10 (first enqueued) ┌────┬────┐ │ 2030 │ └────┴────┘ front back
OperationActionTime
enqueueAdd to backO(1)
dequeueRemove from frontO(1)
peekView front without removingO(1)
Real-world analogies: Checkout line — first person in line served first Print jobs — first document sent prints first Customer service — callers handled in order received Conveyor belt — items processed in arrival order
VariantDescription
Simple QueueStandard FIFO — enqueue at back, dequeue from front
DequeDouble-ended — insert/remove at both front and back
Priority QueueDequeue by priority, not insertion order
Circular QueueFixed-size, wraps around — avoids shifting
Queue Internals & Memory Model
Circular array queue (wrap-around): Index: [0] [1] [2] [3] [4] [5] [6] [7] Value: _ _ 10 20 30 40 _ _ ↑ front ↑ back capacity=8, size=4 After enqueue 50, 60, 70, 80 (wraps around): Index: [0] [1] [2] [3] [4] [5] [6] [7] Value: 70 80 10 20 30 40 50 60 ↑ back ↑ front ← wraps: back = (front + size) % capacity
Linked-list queue: head(front) → [10] → [20] → [30] → [40] → null ← tail(back) dequeue from head = O(1) enqueue at tail = O(1)
ImplementationMemoryNotes
ArrayDequeCompact, contiguousCircular buffer, excellent cache locality
LinkedListExtra pointer per nodeNo resize, but scattered memory
Custom IntArrayMinimal overheadNo boxing, best for primitives
Blocking queuesVariesThread-safe, producer-consumer patterns
Kotlin/JVM Queue Types
TypeBackingNotes
ArrayDequeResizable circular arrayRecommended — Kotlin stdlib, fast
java.util.ArrayDequeResizable circular arrayJava version, also good
LinkedListDoubly linked listImplements both Queue & Deque, poor cache locality
PriorityQueueBinary heap (array)Dequeue by priority, not FIFO
Blocking queuesVariousThread-safe — ArrayBlockingQueue, LinkedBlockingQueue
Queue interface hierarchy: Queue ├── PriorityQueueDeque (extends Queue) ├── java.util.ArrayDeque └── LinkedList kotlin.collections.ArrayDeque (standalone, not java.util.Queue)
// ── Kotlin ArrayDeque as queue ── val queue = ArrayDeque<Int>() queue.addLast(10) // enqueue queue.addLast(20) // enqueue queue.removeFirst() // dequeue → 10 queue.first() // peek → 20 // ── java.util.ArrayDeque as queue ── val jQueue = java.util.ArrayDeque<Int>() jQueue.offer(10) // enqueue (null-safe) jQueue.offer(20) // enqueue jQueue.poll() // dequeue → 10 (null if empty) jQueue.peek() // peek → 20 (null if empty)
Creating & Initializing
// ── Empty queue ── val queue = ArrayDeque<Int>() // ── From a list ── val queue = ArrayDeque(listOf(1, 2, 3)) // 1 is front // ── PriorityQueue (min-heap — default) ── val minHeap = java.util.PriorityQueue<Int>() // ── PriorityQueue (max-heap) ── val maxHeap = java.util.PriorityQueue<Int>(compareByDescending { it }) val maxHeap2 = PriorityQueue<Int>(reverseOrder()) // ── PriorityQueue (custom comparator) ── val pq = java.util.PriorityQueue<IntArray>(compareBy { it[1] })
Enqueue, Dequeue, Peek
val queue = ArrayDeque<Int>() // ── Enqueue ── queue.addLast(10) // [10] queue.addLast(20) // [10, 20] queue.addLast(30) // [10, 20, 30] // ── Peek (view front) ── queue.first() // 10 — throws if empty queue.firstOrNull() // 10 — null if empty (safe) // ── Dequeue (remove front) ── queue.removeFirst() // 10 — throws if empty queue.removeFirstOrNull() // 20 — null if empty (safe) // ── Safe patterns ── if (queue.isNotEmpty()) queue.removeFirst() queue.removeFirstOrNull() ?: defaultValue
ThrowingNull-safeAction
addofferEnqueue
removepollDequeue
elementpeekView front
Iterating & Draining
// ── Drain (dequeue all elements) ── while (queue.isNotEmpty()) { val item = queue.removeFirst() // process item (FIFO order) } // ── Iterate without modifying ── for (item in queue) { } // front → back order // ── Level-based processing (BFS pattern) ── while (queue.isNotEmpty()) { val levelSize = queue.size // snapshot current level repeat(levelSize) { val node = queue.removeFirst() // process node, enqueue children } // entire level processed — move to next }
Implementing from Scratch
// ── Circular Array Queue ── class CircularArrayQueue(private val capacity: Int) { private val data = IntArray(capacity) private var front = 0 private var size = 0 fun enqueue(value: Int) { if (size == capacity) throw IllegalStateException("Full") val back = (front + size) % capacity data[back] = value size++ } fun dequeue(): Int { if (size == 0) throw NoSuchElementException() val value = data[front] front = (front + 1) % capacity size-- return value } fun peek(): Int { if (size == 0) throw NoSuchElementException() return data[front] } fun isEmpty() = size == 0 } // ── Linked Queue ── class LinkedQueue<T> { private class Node<T>(val value: T, var next: Node<T>? = null) private var head: Node<T>? = null private var tail: Node<T>? = null fun enqueue(item: T) { val node = Node(item) if (tail != null) tail!!.next = node else head = node tail = node } fun dequeue(): T { val node = head ?: throw NoSuchElementException() head = node.next if (head == null) tail = null return node.value } fun peek(): T = head?.value ?: throw NoSuchElementException() }
AspectCircularArrayQueueLinkedQueue
enqueueO(1)O(1)
dequeueO(1)O(1)
Memory per element~4-8 bytes~24-32 bytes
Cache localityExcellentPoor
ResizeFixed capacity (or amortized copy)Never needed
The Deque
// ── ArrayDeque as deque — O(1) at both ends ── val deque = ArrayDeque<Int>() deque.addFirst(10) // push front [10] deque.addLast(20) // push back [10, 20] deque.addFirst(5) // push front [5, 10, 20] deque.removeFirst() // pop front → 5 [10, 20] deque.removeLast() // pop back → 20 [10] deque.first() // peek front → 10 deque.last() // peek back → 10 // ── Use as queue: addLast / removeFirst ── val queue = ArrayDeque<Int>() queue.addLast(1) // enqueue queue.removeFirst() // dequeue // ── Use as stack: addLast / removeLast ── val stack = ArrayDeque<Int>() stack.addLast(1) // push stack.removeLast() // pop
PatternUse Deque When
Sliding windowNeed to add/remove from both ends as window slides
Palindrome checkCompare first and last characters simultaneously
Work stealingOwner pops from one end, thieves steal from the other
01-BFS0-weight edges go to front, 1-weight edges go to back
Priority Queue (Heap)
Min-heap — tree representation: 1 / \ 3 2 / \ / \ 5 8 4 7 Array representation: Index: [0] [1] [2] [3] [4] [5] [6] Value: 1 3 2 5 8 4 7 Index formulas: Parent = (i - 1) / 2 Left child = 2 * i + 1 Right child= 2 * i + 2
// ── Min-heap (default) ── val minHeap = java.util.PriorityQueue<Int>() minHeap.add(5); minHeap.add(1); minHeap.add(3) minHeap.peek() // 1 — smallest element minHeap.poll() // 1 — removes smallest // ── Max-heap ── val maxHeap = PriorityQueue<Int>(reverseOrder()) maxHeap.add(5); maxHeap.add(1); maxHeap.add(3) maxHeap.peek() // 5 — largest element maxHeap.poll() // 5 — removes largest // ── Custom comparator ── data class Task(val name: String, val priority: Int) val taskPQ = java.util.PriorityQueue<Task>(compareBy { it.priority })
OperationTime
add (insert)O(log n)
poll (remove min/max)O(log n)
peek (view min/max)O(1)
containsO(n)
Build from collectionO(n)
Sliding Window → see Arrays page for general sliding window patterns  |  Merge K Sorted Lists → see Linked Lists page for linked-list merge  |  Monotonic Stack → see Stacks page for monotonic stack patterns
BFS — Breadth-First Search
O(V+E) O(V)
Layer-by-layer exploration: A Layer 0: A / \ B C Layer 1: B, C / \ \ D E F Layer 2: D, E, F Queue state per step: [A] → process A, enqueue B,C → [B,C] → process B, enqueue D,E → [C,D,E] → process C, enqueue F → [D,E,F] → process D,E,F → []
// ── BFS graph traversal ── fun bfs(graph: Map<Int, List<Int>>, start: Int) { val visited = mutableSetOf(start) val queue = ArrayDeque<Int>() queue.addLast(start) while (queue.isNotEmpty()) { val node = queue.removeFirst() // process node for (neighbor in graph[node].orEmpty()) { if (visited.add(neighbor)) { // mark visited when ENQUEUING queue.addLast(neighbor) } } } } // ── BFS with distance tracking ── fun bfsWithDistance(graph: Map<Int, List<Int>>, start: Int): Map<Int, Int> { val dist = mutableMapOf(start to 0) val queue = ArrayDeque<Int>() queue.addLast(start) while (queue.isNotEmpty()) { val node = queue.removeFirst() for (neighbor in graph[node].orEmpty()) { if (neighbor !in dist) { dist[neighbor] = dist[node]!! + 1 queue.addLast(neighbor) } } } return dist }
When to Use
shortest path (unweighted)Why: BFS explores by distance. All neighbors at distance d are visited before d+1, so the first time you reach a node is the shortest path. number of islandsWhy: Start BFS from each unvisited land cell to flood-fill the entire island. Each BFS = one connected component. minimum moves/stepsWhy: Each BFS level = one move. First arrival at target = fewest moves. Works for grid puzzles, state-space search. nearest target searchWhy: BFS radiates outward uniformly. The first target found is guaranteed to be the closest one. clone graphWhy: BFS visits every node exactly once. Use a HashMap to map original→clone, wiring neighbors as you go. is graph bipartiteWhy: BFS with 2-coloring. Alternate colors at each level. If a neighbor has the same color → not bipartite.
▶ Visualize
Level-Order Tree Traversal
O(n) O(w)
// ── Level-order traversal (returns list of levels) ── fun levelOrder(root: TreeNode?): List<List<Int>> { if (root == null) return emptyList() val result = mutableListOf<List<Int>>() val queue = ArrayDeque<TreeNode>() queue.addLast(root) while (queue.isNotEmpty()) { val levelSize = queue.size // snapshot! val level = mutableListOf<Int>() repeat(levelSize) { val node = queue.removeFirst() level.add(node.`val`) node.left?.let { queue.addLast(it) } node.right?.let { queue.addLast(it) } } result.add(level) } return result } // ── Zigzag level-order ── fun zigzagLevelOrder(root: TreeNode?): List<List<Int>> { if (root == null) return emptyList() val result = mutableListOf<List<Int>>() val queue = ArrayDeque<TreeNode>() queue.addLast(root) var leftToRight = true while (queue.isNotEmpty()) { val levelSize = queue.size val level = mutableListOf<Int>() repeat(levelSize) { val node = queue.removeFirst() level.add(node.`val`) node.left?.let { queue.addLast(it) } node.right?.let { queue.addLast(it) } } if (!leftToRight) level.reverse() result.add(level) leftToRight = !leftToRight } return result } // ── Right side view ── fun rightSideView(root: TreeNode?): List<Int> { if (root == null) return emptyList() val result = mutableListOf<Int>() val queue = ArrayDeque<TreeNode>() queue.addLast(root) while (queue.isNotEmpty()) { val levelSize = queue.size repeat(levelSize) { i -> val node = queue.removeFirst() if (i == levelSize - 1) result.add(node.`val`) node.left?.let { queue.addLast(it) } node.right?.let { queue.addLast(it) } } } return result }
When to Use
level-order traversalWhy: Process all nodes at depth d before depth d+1 using queue.size snapshot per level. zigzag level-orderWhy: Same as level-order but reverse every other level. Collect level, conditionally reverse. right side viewWhy: The last (or first) node processed in each level is the rightmost (or leftmost) visible node. minimum depthWhy: BFS finds the first leaf node — that's the shallowest leaf = minimum depth. Faster than DFS which must explore all paths. average of levelsWhy: Process each level, compute sum/count. Level snapshot ensures clean separation. largest value in each rowWhy: Track max while processing each level. One pass per level, no sorting needed.
▶ Visualize
Multi-Source BFS
O(V+E) O(V)
// ── Rotting Oranges — multi-source BFS ── fun orangesRotting(grid: Array<IntArray>): Int { val rows = grid.size; val cols = grid[0].size val queue = ArrayDeque<IntArray>() var fresh = 0 // Enqueue ALL rotten oranges initially for (r in 0 until rows) for (c in 0 until cols) when (grid[r][c]) { 2 -> queue.addLast(intArrayOf(r, c)) 1 -> fresh++ } val dirs = listOf( -1 to 0, 1 to 0, 0 to -1, 0 to 1 ) var minutes = 0 while (queue.isNotEmpty() && fresh > 0) { val size = queue.size repeat(size) { val (r, c) = queue.removeFirst() for (d in dirs) { val nr = r + d[0]; val nc = c + d[1] if (nr in 0 until rows && nc in 0 until cols && grid[nr][nc] == 1) { grid[nr][nc] = 2 fresh-- queue.addLast(intArrayOf(nr, nc)) } } } minutes++ } return if (fresh == 0) minutes else -1 }
When to Use
rotten orangesWhy: All rotten oranges start spreading simultaneously. Enqueue all rotten cells, BFS radiates in parallel — each level = 1 minute. walls and gatesWhy: Enqueue all gates, BFS fills each empty room with distance to nearest gate. Multi-source = all gates radiate simultaneously. shortest path from multiple startsWhy: Virtual super-source connected to all start points with 0-weight edges. One BFS covers all. 01 matrix (nearest 0)Why: Enqueue all 0-cells, BFS outward. Each 1-cell gets filled with its distance to the nearest 0 on first visit. pacific atlantic water flowWhy: Reverse multi-source BFS from each ocean border inward. Cells reachable from both oceans are the answer.
▶ Visualize
Word Ladder
O(n×L²) O(n×L)
// ── Word Ladder — BFS shortest transformation ── fun ladderLength( beginWord: String, endWord: String, wordList: List<String> ): Int { val wordSet = wordList.toMutableSet() if (endWord !in wordSet) return 0 val queue = ArrayDeque<String>() queue.addLast(beginWord) var steps = 1 while (queue.isNotEmpty()) { val size = queue.size repeat(size) { val word = queue.removeFirst() val chars = word.toCharArray() for (i in chars.indices) { val original = chars[i] for (c in 'a'..'z') { chars[i] = c val newWord = String(chars) if (newWord == endWord) return steps + 1 if (wordSet.remove(newWord)) { queue.addLast(newWord) } } chars[i] = original } } steps++ } return 0 }
When to Use
word ladderWhy: Implicit graph: words are nodes, edges connect words differing by 1 letter. BFS finds shortest transformation. open the lockWhy: State = lock combination (4-digit string). Each turn = 1 edge. BFS from "0000" to target avoiding deadends. minimum genetic mutationWhy: Same as word ladder but with gene strings (ACGT). BFS through valid mutations in the gene bank. sliding puzzleWhy: State = board configuration. Each move = swap blank with neighbor. BFS through state space for shortest solution.
▶ Visualize
Sliding Window Maximum (Monotonic Deque)
O(n) O(k)
Monotonic decreasing deque — front = max of window: Array: [1, 3, -1, -3, 5, 3, 6, 7] k=3 Window [1,3,-1] → deque: [3, -1] max=3 Window [3,-1,-3] → deque: [3, -1, -3] max=3 Window [-1,-3,5] → deque: [5] max=5 Window [-3,5,3] → deque: [5, 3] max=5 Window [5,3,6] → deque: [6] max=6 Window [3,6,7] → deque: [7] max=7 Stores indices in decreasing value order Front always holds current window maximum
// ── Sliding Window Maximum ── fun maxSlidingWindow(nums: IntArray, k: Int): IntArray { val deque = ArrayDeque<Int>() // stores indices val result = IntArray(nums.size - k + 1) for (i in nums.indices) { // Remove expired indices from front while (deque.isNotEmpty() && deque.first() <= i - k) { deque.removeFirst() } // Remove smaller values from back (maintain decreasing order) while (deque.isNotEmpty() && nums[deque.last()] <= nums[i]) { deque.removeLast() } deque.addLast(i) if (i >= k - 1) { result[i - k + 1] = nums[deque.first()] } } return result }
When to Use
sliding window maximumWhy: Monotonic decreasing deque: front = window max. Remove expired from front, remove smaller from back. O(n) total. sliding window minimumWhy: Same pattern but monotonic increasing deque. Front = window min. max in all subarrays of size kWhy: Identical to sliding window maximum. Output result when window reaches size k. shortest subarray with sum ≥ kWhy: Monotonic deque on prefix sums. Remove from front when valid, from back when not useful. Handles negative numbers. jump game VIWhy: DP + monotonic deque: dp[i] = nums[i] + max(dp[j]) for j in window. Deque tracks max dp value in sliding window.
▶ Visualize
Implement Stack Using Queues
O(n) push O(1) pop
// ── Stack using two queues ── class StackUsingQueues { private var q1 = ArrayDeque<Int>() private var q2 = ArrayDeque<Int>() fun push(x: Int) { q2.addLast(x) while (q1.isNotEmpty()) q2.addLast(q1.removeFirst()) val tmp = q1; q1 = q2; q2 = tmp // swap } fun pop(): Int = q1.removeFirst() fun top(): Int = q1.first() fun empty(): Boolean = q1.isEmpty() } // ── Stack using single queue ── class StackUsingSingleQueue { private val queue = ArrayDeque<Int>() fun push(x: Int) { queue.addLast(x) // Rotate: move all elements before x to the back repeat(queue.size - 1) { queue.addLast(queue.removeFirst()) } } fun pop(): Int = queue.removeFirst() fun top(): Int = queue.first() fun empty(): Boolean = queue.isEmpty() }
When to Use
stack using queuesWhy: Classic data structure design question. Tests understanding of LIFO vs FIFO inversion. Push-heavy or pop-heavy variants.
▶ Visualize
Implement Queue Using Stacks
O(1) amortized O(n)
Pour pushStack → popStack reverses order = FIFO: Push 1,2,3: pushStack: [1, 2, 3] ← top popStack: [] Dequeue (popStack empty, so pour): pushStack: [] popStack: [3, 2, 1] ← top pop → 1 (correct FIFO order!) Each element moves at most twice: push → pop stack
// ── Queue using two stacks (amortized O(1)) ── class QueueUsingStacks { private val pushStack = ArrayDeque<Int>() private val popStack = ArrayDeque<Int>() fun push(x: Int) = pushStack.addLast(x) fun pop(): Int { transfer() return popStack.removeLast() } fun peek(): Int { transfer() return popStack.last() } fun empty(): Boolean = pushStack.isEmpty() && popStack.isEmpty() private fun transfer() { if (popStack.isEmpty()) { while (pushStack.isNotEmpty()) { popStack.addLast(pushStack.removeLast()) } } } }
When to Use
queue using stacksWhy: Classic design question. Tests amortized analysis. Two-stack lazy transfer achieves O(1) amortized per operation.
▶ Visualize
Moving Average from Data Stream
O(1) per call O(k)
// ── Moving Average with queue + running sum ── class MovingAverage(private val windowSize: Int) { private val queue = ArrayDeque<Int>() private var sum = 0.0 fun next(value: Int): Double { queue.addLast(value) sum += value if (queue.size > windowSize) { sum -= queue.removeFirst() } return sum / queue.size } } // Usage: val ma = MovingAverage(3) ma.next(1) // 1.0 ma.next(10) // 5.5 (1+10)/2 ma.next(3) // 4.666... (1+10+3)/3 ma.next(5) // 6.0 (10+3+5)/3 — 1 evicted
When to Use
moving averageWhy: Queue holds the last k values. On each new value: enqueue, dequeue oldest if over k. Running sum gives O(1) average. hit counterWhy: Queue of timestamps. Dequeue expired hits (older than 5 minutes). Queue size = hit count in window. rate limiterWhy: Queue of request timestamps. If queue size ≥ limit within time window → reject. Dequeue expired entries. recent calls counterWhy: Queue stores call timestamps. On each call, dequeue entries older than 3000ms. Queue size = count.
▶ Visualize
01-BFS (Deque-Based)
O(V+E) O(V)
// ── 01-BFS — shortest path with 0/1 weights ── fun shortestPath01BFS( graph: Map<Int, List<Pair<Int, Int>>>, // node → [(neighbor, weight 0 or 1)] start: Int, n: Int ): IntArray { val dist = IntArray(n) { Int.MAX_VALUE } dist[start] = 0 val deque = ArrayDeque<Int>() deque.addFirst(start) while (deque.isNotEmpty()) { val u = deque.removeFirst() for ((v, w) in graph[u].orEmpty()) { val newDist = dist[u] + w if (newDist < dist[v]) { dist[v] = newDist if (w == 0) deque.addFirst(v) // 0-weight → front else deque.addLast(v) // 1-weight → back } } } return dist }
When to Use
min cost grid pathWhy: Grid cells have direction arrows. Following costs 0, changing costs 1. 0/1 weights → 01-BFS instead of Dijkstra. shortest path binary matrixWhy: When movement costs are 0 (same terrain) or 1 (obstacle bypass), 01-BFS is O(V+E) vs Dijkstra's O((V+E) log V). graph with 0/1 weightsWhy: Deque replaces priority queue: 0-weight → front (same distance), 1-weight → back (one more). Linear time.
▶ Visualize
Task Scheduler
O(n×m) O(m)
// ── Task Scheduler with max-heap + cooldown queue ── fun leastInterval(tasks: CharArray, n: Int): Int { val freq = IntArray(26) for (t in tasks) freq[t - 'A']++ val maxHeap = PriorityQueue<Int>(reverseOrder()) for (f in freq) if (f > 0) maxHeap.add(f) // (remaining count, available at time) val cooldown = ArrayDeque<Pair<Int, Int>>() var time = 0 while (maxHeap.isNotEmpty() || cooldown.isNotEmpty()) { time++ if (maxHeap.isNotEmpty()) { val count = maxHeap.poll() - 1 if (count > 0) cooldown.addLast(count to time + n) } if (cooldown.isNotEmpty() && cooldown.first().second == time) { maxHeap.add(cooldown.removeFirst().first) } } return time }
When to Use
task schedulerWhy: Greedy: always run the most frequent available task. Max-heap picks it, cooldown queue holds it until ready. Minimizes idle slots. CPU scheduling with cooldownWhy: Same pattern: pick highest priority ready task, enforce minimum gap between same-type tasks. rearrange string k apartWhy: Place most frequent char, wait k positions before reusing. Max-heap + cooldown queue enforces the gap.
▶ Visualize
Top K Frequent Elements
O(n log k) O(n)
// ── Top K Frequent with min-heap of size k ── fun topKFrequent(nums: IntArray, k: Int): IntArray { val freq = mutableMapOf<Int, Int>() for (n in nums) freq[n] = freq.getOrDefault(n, 0) + 1 // Min-heap by frequency — smallest frequency at top val minHeap = java.util.PriorityQueue<Int>(compareBy { freq[it] }) for (num in freq.keys) { minHeap.add(num) if (minHeap.size > k) minHeap.poll() // remove least frequent } return minHeap.toIntArray() }
When to Use
top K frequent elementsWhy: Count frequencies, then min-heap of size k. Poll smallest when over k. What remains = top k. O(n log k). top K frequent wordsWhy: Same pattern but with custom comparator for alphabetical tie-breaking. sort characters by frequencyWhy: Count frequencies, max-heap by frequency, build result by polling. O(n log k) where k = distinct chars. K closest points to originWhy: Max-heap of size k by distance. Poll farthest when over k. Remaining k points are closest. O(n log k).
▶ Visualize
Kth Largest / Merge K Lists
O(n log k) O(k)
// ── Kth Largest Element (min-heap of size k) ── fun findKthLargest(nums: IntArray, k: Int): Int { val minHeap = java.util.PriorityQueue<Int>() for (num in nums) { minHeap.add(num) if (minHeap.size > k) minHeap.poll() } return minHeap.peek() // root = kth largest } // ── Merge K Sorted Lists ── fun mergeKLists(lists: Array<ListNode?>): ListNode? { val minHeap = java.util.PriorityQueue<ListNode>(compareBy { it.`val` }) for (head in lists) head?.let { minHeap.add(it) } val dummy = ListNode(0) var tail = dummy while (minHeap.isNotEmpty()) { val node = minHeap.poll() tail.next = node tail = node node.next?.let { minHeap.add(it) } } return dummy.next }
When to Use
kth largest elementWhy: Min-heap of size k. After all elements: root = kth largest. O(n log k) — better than full sort when k ≪ n. kth largest in a streamWhy: Same min-heap of size k, maintained as elements arrive. Root always = current kth largest. O(log k) per add. merge K sorted listsWhy: Min-heap of k list heads. Poll smallest, push its successor. Always merging the global minimum. O(N log k). kth smallest in sorted matrixWhy: Min-heap with (value, row, col). Poll k times, each time pushing the next element from same row. O(k log k). k pairs with smallest sumsWhy: Min-heap of (a[i]+b[j], i, j). Poll and expand neighbors. Lazily explores pairs in sorted order.
▶ Visualize
½
Median from Data Stream (Two Heaps)
O(log n) add O(1) find
Two heaps — balanced partition: maxHeap(smaller) minHeap(larger) ┌─────────┐ ┌─────────┐ │ 1 3 5 │ │ 7 9 11 │ │ ↑top │ │ ↑top │ └─────────┘ └─────────┘ Median = average of tops = (5 + 7) / 2 = 6.0 Invariant: sizes differ by at most 1 If odd count: larger heap's top = median
// ── MedianFinder with two heaps ── class MedianFinder { // maxHeap: smaller half (top = largest of small half) private val maxHeap = PriorityQueue<Int>(reverseOrder()) // minHeap: larger half (top = smallest of large half) private val minHeap = java.util.PriorityQueue<Int>() fun addNum(num: Int) { maxHeap.add(num) minHeap.add(maxHeap.poll()) // balance: move largest of small half if (minHeap.size > maxHeap.size) { maxHeap.add(minHeap.poll()) // keep maxHeap ≥ minHeap in size } } fun findMedian(): Double { return if (maxHeap.size > minHeap.size) maxHeap.peek().toDouble() else (maxHeap.peek() + minHeap.peek()) / 2.0 } }
When to Use
find median from data streamWhy: Two heaps split data into halves. maxHeap top = largest of small half, minHeap top = smallest of large half. Median from tops. sliding window medianWhy: Same two-heap approach but with lazy deletion. Track elements to remove, clean up when they reach heap top. balance two halvesWhy: Any problem where you need O(1) access to the "middle" of a sorted stream. Percentile tracking, balanced partitions.
▶ Visualize
Dijkstra's Shortest Path
O((V+E) log V) O(V+E)
// ── Dijkstra's with PriorityQueue ── fun dijkstra( graph: Map<Int, List<Pair<Int, Int>>>, // node → [(neighbor, weight)] start: Int, n: Int ): IntArray { val dist = IntArray(n) { Int.MAX_VALUE } dist[start] = 0 // Min-heap of (distance, node) val pq = java.util.PriorityQueue<Pair<Int, Int>>(compareBy { it.first }) pq.add(0 to start) while (pq.isNotEmpty()) { val (d, u) = pq.poll() if (d > dist[u]) continue // skip stale entries for ((v, w) in graph[u].orEmpty()) { val newDist = dist[u] + w if (newDist < dist[v]) { dist[v] = newDist pq.add(newDist to v) } } } return dist }
When to Use
shortest path (weighted)Why: Min-heap PQ processes closest unvisited node. With non-negative weights, settled distance is final. O((V+E) log V). network delay timeWhy: Dijkstra from source, find max of all shortest distances. If any node unreachable → return -1. cheapest flights within K stopsWhy: Modified Dijkstra/Bellman-Ford with stop limit. Track (cost, node, stops) in priority queue. path with minimum effortWhy: Edge weight = abs height difference. Dijkstra finds path minimizing the maximum single-step effort. swim in rising waterWhy: Grid cells have elevation. Min-heap BFS: always expand the cell with lowest elevation. First to reach (n-1,n-1) = answer.
▶ Visualize
Reorganize String
O(n log k) O(k)
// ── Reorganize String — no adjacent duplicates ── fun reorganizeString(s: String): String { val freq = IntArray(26) for (c in s) freq[c - 'a']++ // Check impossible case val maxFreq = freq.max() if (maxFreq > (s.length + 1) / 2) return "" // Max-heap of (frequency, char) val maxHeap = PriorityQueue<Pair<Int, Char>>( compareByDescending { it.first } ) for (i in 0..25) { if (freq[i] > 0) maxHeap.add(freq[i] to ('a' + i)) } val sb = StringBuilder() var prev: Pair<Int, Char>? = null while (maxHeap.isNotEmpty()) { val (count, ch) = maxHeap.poll() sb.append(ch) prev?.let { if (it.first > 0) maxHeap.add(it) } prev = (count - 1) to ch } return sb.toString() }
When to Use
reorganize stringWhy: No two adjacent chars the same. Max-heap greedily picks most frequent available char. Hold previous aside to prevent repeats. distant barcodesWhy: Same pattern: rearrange array so no two adjacent elements are equal. Max-heap by frequency, alternate placement. interleave charactersWhy: Any problem requiring "spread out" the most frequent element. If maxFreq > (n+1)/2 → impossible.
▶ Visualize
Queue Pitfalls
// ── PITFALL 1: Wrong end of ArrayDeque ── val queue = ArrayDeque<Int>() queue.addLast(1); queue.addLast(2); queue.addLast(3) queue.removeLast() // ✗ This is STACK behavior (LIFO), not queue! queue.removeFirst() // ✓ This is QUEUE behavior (FIFO) // ── PITFALL 2: BFS — marking visited at wrong time ── // ✗ WRONG — mark when processing (causes duplicate enqueues) while (queue.isNotEmpty()) { val node = queue.removeFirst() visited.add(node) // ✗ Too late! Node may be enqueued multiple times for (neighbor in graph[node].orEmpty()) { if (neighbor !in visited) queue.addLast(neighbor) } } // ✓ CORRECT — mark when ENQUEUING if (visited.add(neighbor)) queue.addLast(neighbor) // ── PITFALL 3: Iterating PriorityQueue expecting sorted order ── val pq = java.util.PriorityQueue(listOf(3, 1, 2)) for (x in pq) println(x) // ✗ NOT guaranteed sorted! while (pq.isNotEmpty()) // ✓ poll() gives sorted order println(pq.poll()) // ── PITFALL 4: Mutating elements inside PriorityQueue ── data class Item(var priority: Int) val pq2 = java.util.PriorityQueue<Item>(compareBy { it.priority }) val item = Item(5) pq2.add(item) item.priority = 1 // ✗ Heap is now CORRUPTED! No re-heapify happens // ✓ Remove → modify → re-add pq2.remove(item) item.priority = 1 pq2.add(item) // ── PITFALL 5: Forgetting level size snapshot in BFS ── // ✗ WRONG — queue.size changes during iteration for (i in 0 until queue.size) { // size changes as children are added! val node = queue.removeFirst() queue.addLast(child) // increases queue.size mid-loop } // ✓ CORRECT — snapshot before loop val levelSize = queue.size repeat(levelSize) { /* ... */ } // ── PITFALL 6: Using java.util.Stack instead of ArrayDeque ── val bad = java.util.Stack<Int>() // ✗ Legacy, synchronized, extends Vector val good = ArrayDeque<Int>() // ✓ Modern, fast, unsynchronized
Queue Operations Complexity
OperationArrayDequeLinkedList
enqueue (addLast)O(1) amortizedO(1)
dequeue (removeFirst)O(1)O(1)
peek (first)O(1)O(1)
containsO(n)O(n)
sizeO(1)O(1)
Memory per element~4-8 bytes~24-32 bytes
Cache localityExcellentPoor
Priority Queue OperationTime
add (insert)O(log n)
poll (remove min/max)O(log n)
peek (view min/max)O(1)
containsO(n)
Build from collectionO(n)
Algorithm Complexities
AlgorithmTimeSpace
BFS (graph)O(V+E)O(V)
Level-order traversalO(n)O(w)
Shortest path (unweighted)O(V+E)O(V)
Multi-source BFSO(V+E)O(V)
Word ladderO(n×L²)O(n×L)
Sliding window max/minO(n)O(k)
Stack using queuesO(n) pushO(n)
Queue using stacksO(1) amortizedO(n)
Moving averageO(1) per callO(k)
01-BFSO(V+E)O(V)
Task schedulerO(n×m)O(m)
Top K frequentO(n log k)O(n)
Merge K sorted listsO(n log k)O(k)
Kth largest elementO(n log k)O(k)
Median from streamO(log n) addO(n)
Dijkstra's shortest pathO((V+E) log V)O(V+E)
Reorganize stringO(n log k)O(k)
Which Queue Variant?
NeedUse
FIFO processingArrayDeque
Priority-based processingPriorityQueue
Insert/remove at both endsDeque (ArrayDeque)
Sliding window max/minMonotonic deque
Bounded bufferCircular queue
Weighted shortest pathPQ + Dijkstra
0/1 edge weights01-BFS deque
Running medianTwo PQs (max-heap + min-heap)
Top-K elementsPQ of size k
🎯
Five Essential Queue Patterns
1. BFS (Breadth-First Search) Layer-by-layer traversal, shortest path unweighted, level-order 2. Multi-source BFS Rotten oranges, walls & gates — enqueue ALL sources, radiate in parallel 3. Monotonic Deque Sliding window max/min — O(n) via decreasing/increasing deque 4. Priority Queue (Heap) Top-K, merge-K, Dijkstra, task scheduling — greedy by priority 5. Two Heaps Running median, balanced partition — maxHeap + minHeap boundary