DEV Community

Timevolt
Timevolt

Posted on

Heap & Priority Queue: The Gandalf Guide to Prioritizing Tasks

The Quest Begins (The "Why")

I was building a little game‑matchmaking service the other day. Players kept joining, each with a skill rating, and I needed to always pull the highest‑rated player waiting for a match. My first instinct? Dump everyone into an array, sort it, and pop the first element. Works fine… until the queue hits a few thousand players and the sort starts to feel like watching paint dry. I remember staring at the profiling output, thinking, “There has to be a better way.”

That “better way” turned out to be a heap — specifically, a priority queue backed by a binary heap. It’s the kind of tool that feels like discovering a secret shortcut in a dungeon crawler: once you know it, you wonder how you ever survived without it.

The Revelation (The Insight)

So what makes a heap so special? At its heart, a heap is just a binary tree stored in an array that satisfies the heap property: every parent node is ≤ (for a min‑heap) or ≥ (for a max‑heap) its children. Because of that property, the smallest (or largest) element always sits at the root, ready to be removed in O(1) time.

The real magic, though, is how we can turn an arbitrary array into a valid heap in linear time — O(n). Yeah, you heard me: building a heap from scratch isn’t O(n log n) like repeatedly inserting elements; it’s O(n).

Why does that work? Imagine the tree levels from the bottom up. Nodes on the lowest level have height 0, so they need zero swaps. The level above has height 1, each node might swap at most once, and there are at most n/2 of them. The next level up has height 2, at most n/4 nodes, each might swap twice, and so on. If you sum the work across all levels you get:

 Σ (height * number_of_nodes_at_that_height) 
 ≤ Σ (h * n / 2^{h+1}) 
 = O(n)
Enter fullscreen mode Exit fullscreen mode

In plain English: most nodes live near the bottom of the tree where they hardly need to move, and only a few nodes near the top might travel far, but their scarcity keeps the total work linear.

That insight changed everything for me. Instead of sorting the whole collection every time I needed the top element, I could heapify once in O(n) and then repeatedly pop the root in O(log n). The overall cost for extracting k items becomes O(n + k log n), which is practically linear when k is small relative to n.

Wielding the Power (Code & Examples)

The Struggle: Naïve Sorting

def get_top_k_naive(arr, k):
    # Sort each time we need the top k – O(n log n) per call
    return sorted(arr, reverse=True)[:k]
Enter fullscreen mode Exit fullscreen mode

If you call this repeatedly (say, for every new player that joins), you’re paying the sort tax over and over.

The Victory: Heap‑Based Priority Queue

First, we heapify the whole list in O(n). Python’s heapq module gives us a min‑heap, so we’ll store negative values to simulate a max‑heap, or we’ll just use a min‑heap and keep track of the size we need.

import heapq

def build_max_heap(arr):
    """Transform arr into a max‑heap in‑place (O(n))."""
    # heapq only provides min‑heap, so we invert the sign
    for i in range(len(arr)):
        arr[i] = -arr[i]
    heapq.heapify(arr)          # this is the O(n) heapify step
    return arr

def pop_max(heap):
    """Extract the current maximum (O(log n))."""
    return -heapq.heappop(heap)

def push_max(heap, val):
    """Insert a new value while preserving heap property (O(log n))."""
    heapq.heappush(heap, -val)
Enter fullscreen mode Exit fullscreen mode

Now the top‑k query becomes a breeze:

def get_top_k_heap(arr, k):
    """Return the k largest elements using a heap (O(n + k log n))."""
    heap = build_max_heap(arr[:])          # copy to avoid mutating caller
    result = []
    for _ in range(k):
        if not heap: break                 # guard against k > len(arr)
        result.append(pop_max(heap))
    return result
Enter fullscreen mode Exit fullscreen mode

Why this is faster:

  • heapq.heapify runs in O(n) (the heapify step we just geeked out over).
  • Each heappop is O(log n).
  • If k is small (say, you only need the top 10 players out of 100k), the dominant term is the linear heapify, not the repeated log‑n pops.

Common Traps (The “Traps” to Avoid)

  1. Forgetting to heapify – If you just heapq.heappush each element one by one, you’ll end up with O(n log n) build time. Remember: the bulk‑load heapify is the secret sauce.
  2. Mixing up min‑ vs max‑heap semantics – Python’s heapq is a min‑heap by default. If you need a max‑heap, either invert the sign (as shown) or store tuples like (-priority, payload).
  3. Mutating the original arrayheapify works in‑place. If you need the original list later, copy it first (arr[:]) as we did above.

A Second Real‑World Problem: Merging K Sorted Lists

Imagine you have K logs, each already sorted by timestamp, and you need to produce a single merged log. The classic solution? Throw the first element of each list into a min‑heap, repeatedly pop the smallest, and push the next element from the same list.

def merge_k_sorted(lists):
    """Merge K sorted lists into one sorted list (O(N log K))."""
    heap = []
    # Initialise heap with the first element of each list
    for i, lst in enumerate(lists):
        if lst:                                 # skip empty lists
            heapq.heappush(heap, (lst[0], i, 0))  # (value, list_id, index_in_list)

    result = []
    while heap:
        val, list_id, idx = heapq.heappop(heap)
        result.append(val)
        if idx + 1 < len(lists[lists_id]):      # there is a next element in that list
            nxt = lists[list_id][idx + 1]
            heapq.heappush(heap, (nxt, list_id, idx + 1))
    return result
Enter fullscreen mode Exit fullscreen mode

Here the heap never grows larger than K, so each push/pop is O(log K). With N total elements across all lists, the runtime is O(N log K) – far better than the O(NK) naïve approach of repeatedly scanning all lists for the minimum.

Why This New Power Matters

Armed with a heap‑based priority queue, you can:

  • Schedule tasks with varying urgency in real time without re‑sorting the whole queue.
  • Find the K‑largest/smallest elements in a stream with minimal memory (just keep a heap of size K).
  • Merge multiple sorted feeds efficiently – think of merging sorted logs, social‑media feeds, or even external merge‑sort chunks.

The best part? The data structure is tiny (just an array) and the operations are intuitive once you grasp the heap property. It’s like giving your code a Gandalf‑staff: a simple stick that can summon immense power when you know the right spell.

A Quick Challenge

Take the “Kth largest element in an unsorted array” problem. Implement it twice: first with the naïve sort‑and‑pick approach, then with a min‑heap of size K that you maintain while iterating through the array. Compare the runtimes on a large random array (say, 1 million numbers, K = 1000). You’ll see the heap version stay snappy while the sort version lags.

Give it a try, tweet me your results, and let’s keep leveling up our algorithmic toolkit together! 🚀

Top comments (0)