Deque and Priority Queue
TL;DR
Deques support O(1) insertion and deletion at both ends, enabling problems like sliding window optimization and palindrome detection. Priority queues using heaps efficiently maintain k largest elements or dynamic ranking. Heaps guarantee O(log n) insertion/deletion while maintaining partial ordering. Two-heap approach finds running median in O(log n) per insertion. Understanding deques versus priority queues helps select optimal data structure for each problem's characteristics.
Core Concepts
Double-Ended Queue Operations
Deque structure allows adding/removing elements efficiently at both front and back. Implemented via circular buffer (array) achieving O(1) access and modification at both ends, or linked list offering memory flexibility.
Deque vs Queue vs Stack differs by access points. Queues use FIFO (one end in, other out). Stacks use LIFO (one end). Deques support both, making them versatile for problems requiring two-end access.
Circular buffer implementation uses fixed array with two pointers (front/rear). Adding at front or rear wraps indices modulo capacity. Offers cache efficiency but requires manual size management.
Linked list implementation uses doubly-linked nodes, adding at head/tail by updating pointers. More flexible with size but cache-unfriendly and requires extra memory per node.
Heap Structure and Properties
Binary heap is complete binary tree in array form. Parent at index i has children at 2i+1 (left) and 2i+2 (right). Min-heap maintains parent ≤ children; max-heap maintains parent ≥ children.
Heapify operation restores heap property after insertion/deletion. Sift-up moves new element up while violating property; sift-down moves elements down. Both achieve O(log n) complexity.
Heap insertion appends element at end, then sift-up. O(log n) time. Heap deletion removes root, moves last element to root, then sift-down. O(log n) time.
Build heap from array iterates from last non-leaf node downward, sifting each. O(n) time, more efficient than n insertions.
Top-K Problems with Heaps
Top-K largest elements uses min-heap of size k. Maintain heap of k largest; when encountering larger element, remove min and insert new. Final heap contains k largest in O(nlog k) time.
Top-K frequent elements first counts frequencies (hash map), then applies top-k algorithm. Heap comparator uses frequency. O(n + klog n) total.
KClosest points calculates distance, then applies top-k pattern. Points with larger distance replace minimum in heap.
Advantages versus sorting: sorting is O(n log n); heap approach is O(n log k) when k << n.
Running Median with Two Heaps
Two-heap approach maintains max-heap for lower half and min-heap for upper half. Max-heap's root ≤ min-heap's root at all times.
Size balance keeps heaps within one element of each other. If size differs by 2, move root from larger to smaller heap.
Median calculation: if equal sizes, median is average of two roots; if unequal, median is larger heap's root.
Complexity achieves O(log n) insertion and O(1) median query, enabling efficient stream processing.
Key Algorithms and Techniques
Deque with Circular Buffer
Two-pointer system managing front/rear with modulo arithmetic for wraparound.
Heap Sift Operations
Sift-up for insertion, sift-down for deletion, maintaining heap property.
Min-Heap for Top-K Largest
Keep k largest in min-heap; remove min when new element larger appears.
Two-Heap Median Finding
Balance max-heap and min-heap; maintain invariant that max-heap max ≤ min-heap min.
Practical Example
- Python
- Java
- TypeScript
import heapq
from collections import deque
# Deque operations
def deque_operations():
"""Demonstrate deque bidirectional access."""
dq = deque([1, 2, 3])
dq.appendleft(0) # O(1)
dq.append(4) # O(1)
dq.popleft() # O(1)
dq.pop() # O(1)
return list(dq)
# Top-K largest elements using min-heap
def top_k_largest(nums, k):
"""Find k largest elements using min-heap."""
if k == 0:
return []
heap = nums[:k]
heapq.heapify(heap)
for num in nums[k:]:
if num > heap[0]:
heapq.heapreplace(heap, num)
return sorted(heap, reverse=True)
# Top-K frequent elements
def top_k_frequent(nums, k):
"""Find k most frequent elements."""
from collections import Counter
count = Counter(nums)
# Min-heap of (frequency, element)
heap = [(-freq, num) for num, freq in count.items()]
heapq.heapify(heap)
result = []
for _ in range(k):
if heap:
result.append(heapq.heappop(heap)[1])
return result
# Running median with two heaps
class MedianFinder:
def __init__(self):
self.max_heap = [] # Max-heap (negate for Python's min-heap)
self.min_heap = []
def addNum(self, num):
"""Add number and maintain heaps."""
# Add to max-heap (negate to simulate max)
if not self.max_heap or num <= -self.max_heap[0]:
heapq.heappush(self.max_heap, -num)
else:
heapq.heappush(self.min_heap, num)
# Balance heaps
if len(self.max_heap) > len(self.min_heap) + 1:
val = -heapq.heappop(self.max_heap)
heapq.heappush(self.min_heap, val)
elif len(self.min_heap) > len(self.max_heap):
val = heapq.heappop(self.min_heap)
heapq.heappush(self.max_heap, -val)
def findMedian(self):
"""Return current median."""
if len(self.max_heap) == len(self.min_heap):
return (-self.max_heap[0] + self.min_heap[0]) / 2.0
return float(-self.max_heap[0])
# Build heap in O(n)
def build_heap(arr):
"""Build heap from array."""
heapq.heapify(arr)
return arr
# Test cases
print(f"Deque ops: {deque_operations()}")
print(f"Top-3 largest in [1,2,3,5,6,4]: {top_k_largest([1,2,3,5,6,4], 3)}")
print(f"Top-2 frequent in [1,1,1,2,2,3]: {top_k_frequent([1,1,1,2,2,3], 2)}")
mf = MedianFinder()
for num in [1, 2, 3, 4, 5]:
mf.addNum(num)
print(f"Median after adding {num}: {mf.findMedian()}")
import java.util.*;
public class DequeAndPriorityQueue {
// Top-K largest elements using min-heap
static List<Integer> topKLargest(int[] nums, int k) {
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
for (int num : nums) {
minHeap.offer(num);
if (minHeap.size() > k) {
minHeap.poll();
}
}
List<Integer> result = new ArrayList<>(minHeap);
Collections.sort(result, Collections.reverseOrder());
return result;
}
// Top-K frequent elements
static List<Integer> topKFrequent(int[] nums, int k) {
Map<Integer, Integer> count = new HashMap<>();
for (int num : nums) {
count.put(num, count.getOrDefault(num, 0) + 1);
}
PriorityQueue<Integer> minHeap = new PriorityQueue<>(
(a, b) -> count.get(a) - count.get(b)
);
for (int num : count.keySet()) {
minHeap.offer(num);
if (minHeap.size() > k) {
minHeap.poll();
}
}
return new ArrayList<>(minHeap);
}
// Running median with two heaps
static class MedianFinder {
private PriorityQueue<Integer> maxHeap; // Lower half
private PriorityQueue<Integer> minHeap; // Upper half
public MedianFinder() {
maxHeap = new PriorityQueue<>((a, b) -> b - a);
minHeap = new PriorityQueue<>();
}
public void addNum(int num) {
if (maxHeap.isEmpty() || num <= maxHeap.peek()) {
maxHeap.offer(num);
} else {
minHeap.offer(num);
}
// Balance heaps
if (maxHeap.size() > minHeap.size() + 1) {
minHeap.offer(maxHeap.poll());
} else if (minHeap.size() > maxHeap.size()) {
maxHeap.offer(minHeap.poll());
}
}
public double findMedian() {
if (maxHeap.size() == minHeap.size()) {
return (maxHeap.peek() + minHeap.peek()) / 2.0;
}
return maxHeap.peek();
}
}
public static void main(String[] args) {
int[] nums = {1, 2, 3, 5, 6, 4};
System.out.println("Top-3 largest: " + topKLargest(nums, 3));
int[] freqNums = {1, 1, 1, 2, 2, 3};
System.out.println("Top-2 frequent: " + topKFrequent(freqNums, 2));
MedianFinder mf = new MedianFinder();
int[] stream = {1, 2, 3, 4, 5};
for (int num : stream) {
mf.addNum(num);
System.out.println("Median after " + num + ": " + mf.findMedian());
}
}
}
// Top-K largest elements using min-heap
function topKLargest(nums: number[], k: number): number[] {
// Simple implementation using sorting (production would use heap)
const sorted = [...nums].sort((a, b) => b - a);
return sorted.slice(0, k);
}
// Top-K frequent elements
function topKFrequent(nums: number[], k: number): number[] {
const count = new Map<number, number>();
for (const num of nums) {
count.set(num, (count.get(num) || 0) + 1);
}
const byFreq = [...count.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, k)
.map(entry => entry[0]);
return byFreq;
}
// Running median with two heaps (using sorted arrays)
class MedianFinder {
private maxHeap: number[] = []; // Lower half
private minHeap: number[] = []; // Upper half
addNum(num: number): void {
// Add to appropriate heap
if (this.maxHeap.length === 0 || num <= this.maxHeap[0]) {
this._addToMax(num);
} else {
this._addToMin(num);
}
// Balance
if (this.maxHeap.length > this.minHeap.length + 1) {
const top = this.maxHeap.shift()!;
this._addToMin(top);
} else if (this.minHeap.length > this.maxHeap.length) {
const top = this.minHeap.shift()!;
this._addToMax(top);
}
}
findMedian(): number {
if (this.maxHeap.length === this.minHeap.length) {
return (this.maxHeap[0] + this.minHeap[0]) / 2;
}
return this.maxHeap[0];
}
private _addToMax(num: number): void {
this.maxHeap.push(num);
this.maxHeap.sort((a, b) => b - a);
}
private _addToMin(num: number): void {
this.minHeap.push(num);
this.minHeap.sort((a, b) => a - b);
}
}
// Test
console.log("Top-3 largest:", topKLargest([1, 2, 3, 5, 6, 4], 3));
console.log("Top-2 frequent:", topKFrequent([1, 1, 1, 2, 2, 3], 2));
const mf = new MedianFinder();
for (const num of [1, 2, 3, 4, 5]) {
mf.addNum(num);
console.log(`Median after ${num}: ${mf.findMedian()}`);
}
Common Pitfalls
Heap index formula errors: Left child at 2i+1, right at 2i+2. Using 2i or 2i+1 for both causes incorrect tree structure.
Forgetting to heapify after building: Simply placing elements in array doesn't create heap. Must call heapify to restore property.
Top-K min-heap confusion: Using max-heap for top-K largest wastes space. Min-heap of size k is optimal; remove min when larger appears.
Median heap balance forgotten: Two-heap median requires constant balancing. Skipping this produces incorrect results when sizes differ significantly.
Deque wraparound errors: Circular buffer indices must wrap with modulo. Forgetting wraparound causes array bounds errors.
Self-Check
- Explain how min-heap finds top-K largest in O(n log k) time. Why is this better than sorting?
- Why do two heaps find running median efficiently? What invariant must hold?
- How does circular buffer implement deque? Why is modulo necessary?
One Takeaway
Master deques for bidirectional access and priority queues (heaps) for efficient selection and ranking. Two-heap approach elegantly solves running median. Top-K problems showcase how choosing right data structure—heap over sorting—yields better complexity.
References
- Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2009). Introduction to Algorithms (3rd ed.). MIT Press.
- McDowell, G. L. (2015). Cracking the Coding Interview (6th ed.). CareerCup.