LearnAbout

Heaps & Priority Queue

On this page
Quick reference
pushO(log n)
popO(log n)
peekO(1)
heapify (from an existing array)O(n)
The Idea

Picture a hospital triage line. The most urgent patient is always at the front โ€” the front is never wrong. But the rest of the line isn't sorted at all; patient #5 might be more urgent than patient #3. Nobody's wasting time keeping the whole line in perfect order, because the only thing anyone actually needs is: who's next? A heap is that triage line, formalized. It keeps the smallest (or largest) item always instantly reachable at the top, and does just enough work โ€” a little reshuffling each time someone joins or leaves โ€” to keep that one guarantee true. Everything below the top is only "roughly enough" ordered.

Compare that to keeping a fully sorted array. Every new arrival has to be slotted into its exact position, which means shifting every element after it โ€” expensive, every single time. A heap refuses to pay that price: it doesn't promise the third-most-urgent item is at index 2, or that scanning left to right gives you sorted order. It promises exactly one thing โ€” the top is always correct โ€” and that narrower promise is what makes it cheap to maintain.

A sorted array guarantees everything is in order and it costs you on every insert. A heap guarantees only the top is correct and that's the entire reason it's cheap โ€” you're paying for exactly the guarantee you asked for, nothing more.
Build It
The array
# a heap is a plain array โ€” no node, no pointers.
# for the element at index i:
#   left child  -> index 2*i + 1
#   right child -> index 2*i + 2
#   parent      -> index floor((i - 1) / 2)
#
# (straight out of Arrays & Strings Fundamentals: index math gives instant
# access to "the next thing" without ever storing an address for it.)
#
# min-heap invariant: every parent's value <= both of its children's values.
# (nothing is said about how two children, or two cousins, compare to each other.)
push โ€” append, then sift up โ€” O(log n)
function push(heap, value):
    heap.append(value)                  // new element goes at the very end
    i = heap.length - 1
    while i > 0:
        parent = floor((i - 1) / 2)
        if heap[i] < heap[parent]:      // smaller than its parent โ€” invariant broken
            swap(heap[i], heap[parent])
            i = parent
        else:
            break                       // parent is already smaller โ€” done climbing
pop โ€” remove the root, sift down โ€” O(log n)
function pop(heap):
    if heap.length == 0:
        return NULL                     // nothing to remove
    top = heap[0]
    last = heap.removeLast()
    if heap.length > 0:
        heap[0] = last                  // plug the hole with whatever was last
        siftDown(heap, 0)
    return top

function siftDown(heap, i):
    n = heap.length
    while true:
        left = 2*i + 1
        right = 2*i + 2
        smallest = i
        if left < n and heap[left] < heap[smallest]:
            smallest = left
        if right < n and heap[right] < heap[smallest]:   // must check BOTH, keep the smaller
            smallest = right
        if smallest == i:
            break                       // both children are already >= this node โ€” done
        swap(heap[i], heap[smallest])
        i = smallest
The one line that's easy to get wrong: sift-down must compare against whichever child is smaller, not just "a" child. Swap with the bigger child by mistake and you plant a broken subtree one level down โ€” no crash, just a heap that quietly lies about its own top from then on.
peek โ€” O(1)
function peek(heap):
    if heap.length == 0:
        return NULL
    return heap[0]                      // the minimum is always sitting at index 0
heapify โ€” build a heap from an existing array bottom-up โ€” O(n)
function heapify(array):
    n = array.length
    // last parent down to the root โ€” skip leaves, they're trivially valid heaps of one
    for i from floor(n / 2) - 1 down to 0:
        siftDown(array, i)
    return array
Pushing n items one at a time costs O(n log n) โ€” every push pays up to log n. heapify starts from the bottom instead: most nodes are near the bottom and only sift down a level or two, and only a handful near the root sift far. Summed across the whole array that totals O(n), not O(n log n) โ€” build the heap once from the array you already have, don't push into it from empty.
Know It

A heap stored in an array is always a complete binary tree โ€” every level full except possibly the last, which fills left to right with no gaps. That's not a maintained invariant you have to check; it falls straight out of always inserting at the end and always removing from the end. A complete tree's height is always O(log n), full stop โ€” no rebalancing logic required, unlike a plain BST (next topic's neighbor) which can skew into a straight line. And because the whole structure lives in one array, there are no node objects and no pointers to allocate โ€” extra space beyond the array itself is O(1) across every operation.

OperationTimeSpaceWhy
pushO(log n)O(1)Worst case, the new element sifts up from a leaf all the way to the root โ€” one swap per level, and a complete tree of n nodes has O(log n) levels.
popO(log n)O(1)Same shape as push in reverse: the relocated element sifts down at most the height of the tree.
peekO(1)O(1)The minimum is guaranteed to be at index 0 โ€” no comparisons, no traversal.
heapify (from an existing array)O(n)O(1)Most of the n nodes sit near the bottom and sift down only a level or two; the sum of all that work across the whole array works out to O(n), not O(n log n) โ€” see the callout in Build It.
Break It

Empty heap

pop() or peek() called on a heap with zero elements

Both must check heap.length == 0 first and return cleanly (NULL, or however your language signals "nothing here") instead of reading index 0 of an empty array. Same discipline as checking head == NULL on an empty linked list before touching it.

"A heap is a sorted array" โ€” no, it isn't

reading heap[1], heap[2], heap[3]... expecting ascending order

The only promise a min-heap makes is parent <= children. heap[1] and heap[2] are both children of the root and could be in either order relative to each other, and heap[1] could easily be larger than heap[4] two levels down on the other side. The array is not sorted โ€” only the path from any node up to the root is guaranteed non-decreasing. To actually get sorted order out of a heap you'd have to pop every element one at a time (that's heap sort, and it costs the full O(n log n)).

Sifting down against the wrong child

comparing the parent to just one child, or to "whichever child happens to be checked first"

A min-heap's sift-down must compare against the smaller of the two children, then swap with that one โ€” never the other, never an arbitrary pick. Swap with the larger child and the smaller child ends up buried under a value that should have floated above it: the heap invariant breaks silently, one level down, with no error and no crash โ€” it just quietly starts returning wrong answers from pop() later.

Off-by-one in the index math

translating 2i+1 / 2i+2 / floor((i-1)/2) into code

Easy slips: using i/2 for the parent instead of floor((i-1)/2) (they only agree for even i), forgetting the -1 entirely, or swapping which formula is left vs. right. Just as common: forgetting the bounds check โ€” left < n and right < n โ€” before reading heap[left] or heap[right], which reads past the end of the array on any node near the bottom.

Use It
Kth Largest Element in an Array
Revisit from Phase 1's Sorting Intuition โ€” this time keep a min-heap of size k instead of sorting the whole array; the root is your answer once the heap is full.
Medium
Last Stone Weight
Max-heap (or negate values into a min-heap): pop the two biggest, push back the difference, repeat.
Easy
Top K Frequent Elements
Revisit from Phase 1's Hashing โ€” count frequencies with a hash map first, then keep a size-k min-heap over (frequency, value) pairs, same Top-K framing as Kth Largest above.
Medium
K Closest Points to Origin
Max-heap of size k keyed on squared distance โ€” no need for sqrt, and no need to keep anything beyond the k closest seen so far.
Medium
Kth Largest Element in a Stream
A min-heap capped at size k, kept alive across calls to add() โ€” push's callout about heapify vs. one-at-a-time inserts is exactly why you build it once in the constructor.
Easy
Relative Ranks
Max-heap of (score, original index) pairs โ€” pop in order to hand out 'Gold Medal', 'Silver Medal', 'Bronze Medal', then plain ranks.
Easy
Task Scheduler
Max-heap on task frequency: always schedule the currently-most-frequent task next, cooling down the ones you just ran.
Medium
Reorganize String
Same shape as Task Scheduler โ€” max-heap on character frequency, always place the most frequent character that isn't the one you just placed.
Medium
Kth Smallest Element in a Sorted Matrix
Min-heap seeded with the first element of each row (or just row 0); pop the smallest, push its neighbor, repeat k times โ€” the heap always holds the current frontier of candidates.
Medium
Sort Characters By Frequency
Count frequencies, then a max-heap (or heapify once and pop everything) to emit characters most-frequent first.
Medium