🏔️ Heaps & Priority Queues Made Easy

Master the art of efficient priority management with visual explanations and real-world analogies

🎯 Data Structures ⏱️ 25 min read 🏔️ Tree-based ⚡ O(log n) Operations

🤔 Why Should You Care About Heaps?

🏥

Hospital Emergency Room

Patients aren't treated first-come-first-serve - they're prioritized by severity! That's exactly what a heap does - it's like a smart waiting line that always knows who should go next.

✈️

Airline Boarding

First class boards first, then priority members, then everyone else. A heap manages this priority system efficiently, always knowing who's next!

🖥️

Computer Task Scheduling

Your OS uses heaps to decide which program gets CPU time next. Critical system tasks jump the queue ahead of your cat video!

🎮

Game AI Decision Making

Game enemies use heaps to prioritize threats - they attack the weakest player or biggest threat first, making games more challenging!

📈

Stock Trading Systems

Trading algorithms use heaps to execute the best buy/sell orders first, processing millions of trades efficiently!

🚗

GPS Navigation

Finding the shortest route? Dijkstra's algorithm uses a heap to efficiently explore paths, getting you there faster!

🌟 Level 1: The Basics (Start Here!)

Level 1

What's a Heap? Think of it as a Smart Pyramid! 🏔️

Imagine organizing a sports tournament where the champion always sits at the top. That's a heap! It's a special tree where every parent is "better" than its children (bigger in max-heap, smaller in min-heap).

🎮 Try It Yourself: Build a Max Heap

Add numbers and watch how they bubble up to maintain the heap property!

Heap is empty. Add some numbers!

The Two Types of Heaps 🔄

📈 Max Heap

Rule: Parent ≥ Children

Root: Maximum element

Use: When you need the largest quickly

Example: Finding top scores

📉 Min Heap

Rule: Parent ≤ Children

Root: Minimum element

Use: When you need the smallest quickly

Example: Processing urgent tasks first

Your First Heap Implementation in Python 🐍
# Let's build a max heap step by step! class MaxHeap: def __init__(self): # We use a list to store our heap - it's that simple! self.heap = [] def parent(self, i): # Parent is always at (i-1)/2 - like going up a family tree! return (i - 1) // 2 def left_child(self, i): # Left child is at 2*i + 1 return 2 * i + 1 def right_child(self, i): # Right child is at 2*i + 2 return 2 * i + 2 def insert(self, value): # Step 1: Add to the end (like joining a queue) self.heap.append(value) # Step 2: Bubble up until we find the right spot! self._bubble_up(len(self.heap) - 1) def _bubble_up(self, i): # Keep swapping with parent if we're bigger while i > 0 and self.heap[i] > self.heap[self.parent(i)]: # Swap with parent parent_idx = self.parent(i) self.heap[i], self.heap[parent_idx] = self.heap[parent_idx], self.heap[i] i = parent_idx # Move up and continue def extract_max(self): if not self.heap: return None # Save the champion (root) max_val = self.heap[0] # Move last element to root self.heap[0] = self.heap[-1] self.heap.pop() # Let it sink down to its proper place if self.heap: self._bubble_down(0) return max_val def _bubble_down(self, i): # Keep swapping with larger child until we're in the right spot while self.left_child(i) < len(self.heap): # Find the bigger child bigger_child = self.left_child(i) right = self.right_child(i) if right < len(self.heap) and self.heap[right] > self.heap[bigger_child]: bigger_child = right # If we're bigger than both children, we're done! if self.heap[i] >= self.heap[bigger_child]: break # Otherwise, swap and continue self.heap[i], self.heap[bigger_child] = self.heap[bigger_child], self.heap[i] i = bigger_child # Let's test our heap! heap = MaxHeap() numbers = [10, 20, 15, 30, 40] for num in numbers: heap.insert(num) print(f"Inserted {num}, heap: {heap.heap}") # Extract all elements (they come out sorted!) while heap.heap: print(f"Extracted: {heap.extract_max()}")

⚠️ Common Mistake #1: Confusing Array Indices

# ❌ WRONG - Off by one error! def parent(self, i): return i // 2 # This works for 1-indexed, not 0-indexed! # ✅ CORRECT - For 0-indexed arrays def parent(self, i): return (i - 1) // 2

✅ Pro Tip: Remember the Magic Formulas!

For 0-indexed arrays (most common):

  • Parent: (i - 1) / 2
  • Left Child: 2 * i + 1
  • Right Child: 2 * i + 2

Think of it as: "Multiply by 2 to go down, divide by 2 to go up!"

🎨 Level 2: Common Heap Patterns

Level 2

Pattern 1: Top K Elements 🏆

Need to find the K largest or smallest elements? Heaps are your best friend!

Finding K Largest Elements - The Smart Way!
import heapq def find_k_largest(nums, k): """ Find K largest elements using a MIN heap of size K Counterintuitive but brilliant: Keep only K elements, smallest on top, so we can easily kick it out! """ # Use a min heap of size k min_heap = [] for num in nums: heapq.heappush(min_heap, num) # If we have more than k elements, remove the smallest if len(min_heap) > k: heapq.heappop(min_heap) # Kicks out the smallest! # What's left? The K largest elements! return min_heap # Example: Find top 3 scores scores = [50, 80, 90, 75, 95, 85, 70] top_3 = find_k_largest(scores, 3) print(f"Top 3 scores: {sorted(top_3, reverse=True)}") # [95, 90, 85]

Pattern 2: Running Median 📊

Track the median of a stream of numbers - a classic two-heap solution!

🎮 Interactive: Running Median Tracker

Add numbers and watch how two heaps work together to track the median!

Max Heap (smaller half)
[]
Median
-
Min Heap (larger half)
[]

Pattern 3: Merge K Sorted Lists 🔀

Merging Multiple Sorted Lists Efficiently
import heapq def merge_k_sorted_lists(lists): """ Merge K sorted lists using a heap Time: O(N log K) where N = total elements, K = number of lists """ min_heap = [] result = [] # Add first element from each list to heap for i, lst in enumerate(lists): if lst: # If list is not empty # Push (value, list_index, element_index) heapq.heappush(min_heap, (lst[0], i, 0)) while min_heap: # Get smallest element val, list_idx, elem_idx = heapq.heappop(min_heap) result.append(val) # Add next element from same list if exists if elem_idx + 1 < len(lists[list_idx]): next_val = lists[list_idx][elem_idx + 1] heapq.heappush(min_heap, (next_val, list_idx, elem_idx + 1)) return result # Example: Merge sorted lists lists = [ [1, 4, 7], [2, 5, 8], [3, 6, 9] ] merged = merge_k_sorted_lists(lists) print(f"Merged: {merged}") # [1, 2, 3, 4, 5, 6, 7, 8, 9]

Pattern 4: Task Scheduling ⏰

CPU Task Scheduling Algorithm

1
Sort tasks by start time - Know when each task becomes available
2
Use min heap for processing time - Always pick shortest task
3
Add available tasks to heap - As time progresses
4
Process shortest available task - Minimize average wait time

💪 Level 3: Practice Problems

Practice Time!
Easy

Problem 1: Last Stone Weight

You have stones with weights. Each turn, smash the two heaviest stones together. If x == y, both destroyed. If x != y, one stone left with weight y - x. Find final stone weight.

def lastStoneWeight(stones): """ Hint: Use a max heap! Python's heapq is min heap, so negate values """ # Your code here! pass
💡 Show Solution
import heapq def lastStoneWeight(stones): # Convert to max heap by negating max_heap = [-stone for stone in stones] heapq.heapify(max_heap) while len(max_heap) > 1: # Get two heaviest stones first = -heapq.heappop(max_heap) second = -heapq.heappop(max_heap) # If different weights, push difference back if first != second: heapq.heappush(max_heap, -(first - second)) return -max_heap[0] if max_heap else 0
Medium

Problem 2: K Closest Points to Origin

Given points on a 2D plane, find K closest points to origin (0, 0).

def kClosest(points, k): """ Distance = sqrt(x² + y²) But we can skip sqrt since comparing! """ # Your code here! pass
💡 Show Solution
import heapq def kClosest(points, k): # Use max heap to keep k closest max_heap = [] for x, y in points: dist = -(x*x + y*y) # Negative for max heap if len(max_heap) < k: heapq.heappush(max_heap, (dist, x, y)) elif dist > max_heap[0][0]: # Closer than furthest heapq.heapreplace(max_heap, (dist, x, y)) return [[x, y] for _, x, y in max_heap]
Hard

Problem 3: Find Median from Data Stream

Design a data structure that supports adding numbers and finding median efficiently.

class MedianFinder: def __init__(self): # Hint: Use two heaps! pass def addNum(self, num): pass def findMedian(self): pass
💡 Show Solution
import heapq class MedianFinder: def __init__(self): self.small = [] # Max heap (smaller half) self.large = [] # Min heap (larger half) def addNum(self, num): # Add to max heap (negate for max behavior) heapq.heappush(self.small, -num) # Ensure every small element ≤ every large element if self.small and self.large: if -self.small[0] > self.large[0]: val = -heapq.heappop(self.small) heapq.heappush(self.large, val) # Balance sizes (differ by at most 1) if len(self.small) > len(self.large) + 1: val = -heapq.heappop(self.small) heapq.heappush(self.large, val) elif len(self.large) > len(self.small): val = heapq.heappop(self.large) heapq.heappush(self.small, -val) def findMedian(self): if len(self.small) > len(self.large): return -self.small[0] return (-self.small[0] + self.large[0]) / 2.0

🚀 Level 4: Advanced Concepts

Level 4

Heap Sort: The Elegant Sorting Algorithm 🎯

Heap Sort Implementation
def heap_sort(arr): """ Heap Sort: Build max heap, then extract elements Time: O(n log n), Space: O(1) - In-place! """ n = len(arr) # Build max heap (heapify) for i in range(n // 2 - 1, -1, -1): heapify(arr, n, i) # Extract elements one by one for i in range(n - 1, 0, -1): # Move root to end arr[0], arr[i] = arr[i], arr[0] # Heapify reduced heap heapify(arr, i, 0) return arr def heapify(arr, n, i): """Make subtree rooted at i a max heap""" largest = i left = 2 * i + 1 right = 2 * i + 2 # Find largest among root and children if left < n and arr[left] > arr[largest]: largest = left if right < n and arr[right] > arr[largest]: largest = right # If largest is not root, swap and continue if largest != i: arr[i], arr[largest] = arr[largest], arr[i] heapify(arr, n, largest) # Test it! numbers = [64, 34, 25, 12, 22, 11, 90] sorted_nums = heap_sort(numbers.copy()) print(f"Sorted: {sorted_nums}")

Dijkstra's Algorithm with Heap 🗺️

Finding Shortest Paths Efficiently
import heapq from collections import defaultdict def dijkstra(graph, start): """ Find shortest paths from start to all vertices Using min heap for efficient vertex selection """ distances = defaultdict(lambda: float('inf')) distances[start] = 0 # Min heap: (distance, vertex) min_heap = [(0, start)] visited = set() while min_heap: curr_dist, u = heapq.heappop(min_heap) # Skip if already visited if u in visited: continue visited.add(u) # Check all neighbors for v, weight in graph[u]: if v not in visited: new_dist = curr_dist + weight # If found shorter path, update if new_dist < distances[v]: distances[v] = new_dist heapq.heappush(min_heap, (new_dist, v)) return dict(distances) # Example: City distances graph = defaultdict(list) graph['A'].extend([('B', 4), ('C', 2)]) graph['B'].extend([('C', 1), ('D', 5)]) graph['C'].extend([('D', 8), ('E', 10)]) graph['D'].append(('E', 2)) distances = dijkstra(graph, 'A') print(f"Shortest distances from A: {distances}")

Advanced Heap Variations 🔬

🌳 Fibonacci Heap

  • Decrease-key: O(1) amortized
  • Better for Dijkstra theoretically
  • Complex implementation
  • Rarely used in practice

🎯 Binomial Heap

  • Merge: O(log n)
  • Collection of binomial trees
  • Good for priority queue merging
  • Used in some advanced algorithms

⚡ D-ary Heap

  • Each node has D children
  • Shallower tree
  • Better cache performance
  • Used in practice (D=4 common)

📖 Quick Reference Guide

🎯 Heap Operations Cheat Sheet

Operation Time Complexity What It Does Real-World Analogy
Insert O(log n) Add element & bubble up New patient arrives at ER
Extract Max/Min O(log n) Remove root & bubble down Next patient gets treated
Peek O(1) Look at root See who's next in line
Build Heap O(n) Convert array to heap Organize waiting room
Heap Sort O(n log n) Sort using heap Process all patients by priority

✅ When to Use Heaps

  • 🏆 Finding K largest/smallest elements
  • 📊 Running median or statistics
  • 🔀 Merging sorted sequences
  • ⏰ Task scheduling by priority
  • 🗺️ Graph algorithms (Dijkstra, Prim)
  • 🎮 Game AI decision making
  • 💹 Order book in trading systems

⚠️ When NOT to Use Heaps

  • ❌ Need to search for specific element (O(n))
  • ❌ Need sorted order throughout (use BST)
  • ❌ Need to access middle elements frequently
  • ❌ Small, fixed-size collections (array is simpler)
  • ❌ Need to iterate in order (heap isn't sorted)

Python's heapq Module

import heapq # Create heap heap = [] # Insert heapq.heappush(heap, 5) # Extract min min_val = heapq.heappop(heap) # Peek min min_val = heap[0] # Convert list to heap heapq.heapify(nums) # Get n largest largest = heapq.nlargest(3, nums) # Get n smallest smallest = heapq.nsmallest(3, nums)

Max Heap Trick in Python

# Python only has min heap # For max heap, negate values! max_heap = [] # Insert (negate) heapq.heappush(max_heap, -5) # Extract max (negate back) max_val = -heapq.heappop(max_heap) # Peek max max_val = -max_heap[0]

🎓 Key Takeaways

  1. Heaps are complete binary trees - Always filled level by level
  2. Parent-child relationship is key - Parent ≥ children (max) or ≤ children (min)
  3. Array representation is efficient - No pointers needed!
  4. Perfect for priority management - O(1) access to top priority
  5. Two-heap technique is powerful - For median, balance problems
  6. Python's heapq is min heap only - Negate for max heap behavior