Chapter 9: Heaps & Priority Queues
Master heap data structures and priority queues for efficient element retrieval. Learn heap operations, Python's heapq module, and solve classic interview problems including k closest points, merging sorted lists, kth largest element, string reorganization, and running median.
- Topics
- 6
- Demos
- 6
- Min read
- 25
Chapter Overview
A heap is a specialized tree-based data structure that satisfies the heap property: in a min-heap, every parent is smaller than or equal to its children; in a max-heap, every parent is larger than or equal to its children. This guarantees the minimum (or maximum) element is always at the root, accessible in time.
Heaps power priority queues --- abstract data structures where the highest-priority element is always served first, regardless of insertion order. Operations like push and pop run in time, making heaps ideal for problems that repeatedly ask for the smallest or largest element from a dynamic collection.
This chapter covers:
- Heap Fundamentals: How heaps work internally, heapify, and Python's heapq
- K Closest Points: Using a heap to find the k nearest points to the origin
- Merge K Sorted Lists: Efficiently merging multiple sorted sequences with a heap
- Kth Largest Element: Maintaining a min-heap of size k for streaming data
- Reorganize String: Greedy character placement using a max-heap
- Find Median from Data Stream: The elegant two-heap technique for running median
Chapter Roadmap
Click any topic to jump in
Heap Fundamentals
Complete binary tree with the heap property — $O(\log n)$ push/pop via sift-up and sift-down.
Bounded-size heaps
K Closest Points
Find the $k$ nearest neighbors to the origin — max-heap of size $k$ keeps it $O(n \log k)$.
Merge K Sorted Lists
K-way merge with a min-heap — always pop the smallest next element across all lists.
Kth Largest Element
Maintain a running min-heap of size $k$ — the root is always the answer.
Reorganize String
Greedy max-heap on character counts — interleave the most frequent letters to avoid adjacency.
Median from Stream
Two heaps split the data in half — $O(\log n)$ insert, $O(1)$ median query.
Topics
6 topics in this chapter
An emergency room does not treat patients in arrival order. It treats the most urgent case first, and new patients keep arriving while others are being seen. An operating system scheduler, a router's packet queue, and an event-driven simulation all make the same demand: insert items at any time, and repeatedly remove the one with the highest priority. A sorted array makes removal cheap but costs shifts per insertion; an unsorted list makes insertion cheap but every removal scans all items.
The previous chapter, Advanced Graphs, already relied on this structure: Dijkstra's and Prim's algorithms pulled the closest unsettled vertex from a min-heap without looking inside it. A binary heap balances the two costs. It keeps only a partial order, every parent no larger than its children, which is enough to put the minimum at the root while making both insertion and removal . Because the tree is complete, it fits in a plain array with index arithmetic instead of pointers.
We first build the array layout and the sift-up and sift-down operations. Then we see why heapify runs in linear time and how Python's heapq module exposes all of it.
Definition
A binary heap is a complete binary tree stored in an array, where the node at index has children at and . In a min-heap every parent is less than or equal to its children, so the minimum sits at index 0. Push and pop take time, peek takes , and heapify builds a heap from items in .
In this topic
- 1Min-Heap and Max-Heap Structure
- 2Heapify and Python heapq
Min-Heap and Max-Heap Structure
A min-heap ensures that every parent node is less than or equal to its children. The smallest element sits at the root (index 0). A max-heap is the reverse --- every parent is greater than or equal to its children, so the largest element is at the root.
Since a heap is a complete binary tree stored as an array, the parent-child relationships follow simple index formulas:
- Parent of node at index :
- Left child:
- Right child:
Push (insert): Add to the end, then sift up by swapping with the parent until the heap property is restored --- . Pop (extract min/max): Swap root with the last element, remove it, then sift down by swapping with the smaller (min-heap) or larger (max-heap) child --- . Peek: Return the root --- .
A binary heap is a complete binary tree where every parent satisfies the heap property: in a min-heap, ; in a max-heap, . Stored as an array, the children of index are at and , and the parent is at . Completeness means height is , so insert (bubble up) and extract-root (sift down) are both . Peek at the root is . A heap of elements uses exactly space with no pointers.
Insert elements [5, 3, 8, 1, 4] into a min-heap one by one. Show the heap state after each insertion.
Heapify and Python heapq
Heapify converts an arbitrary list into a valid heap in time --- faster than inserting elements one by one (). The algorithm starts from the last non-leaf node and sifts down each node. Because most nodes are near the bottom (where sift-down is cheap), the total work is linear.
Python's heapq module provides a min-heap API:
- heapq.heappush(heap, val) --- push a value,
- heapq.heappop(heap) --- pop the smallest,
- heapq.heapify(lst) --- convert list to heap in-place,
- heapq.heappushpop(heap, val) --- push then pop, optimized to
- heapq.nsmallest(k, iterable) and heapq.nlargest(k, iterable) --- find k extreme values
For a max-heap in Python, negate values before pushing and negate again after popping.
Python's heapq module implements a min-heap on a regular list. heapq.heapify(arr) converts an arbitrary list into a heap in — not — by sifting down from the last non-leaf upward; the proof uses a telescoping sum . For a max-heap, negate values (heappush(heap, -x)). heappush and heappop are ; heapq.nlargest(k, arr) uses a size- heap for total.
Given the list [9, 4, 7, 1, 3, 6], show the result of heapify and demonstrate push/pop.
Related Problems on PixelBank
A ride-hailing app receives a request and must find the few drivers nearest to the rider among thousands on the map. A recommender looks for the handful of items whose feature vectors sit closest to a user's. In both cases a full ranking of every candidate is wasted work, since only the top few matter.
The previous topic, Heap Fundamentals, built the heap and its push and pop. K Closest Points to Origin is the first problem that uses them to answer a top- question. Given points in the plane, return the with the smallest Euclidean distance to the origin. The square root is increasing, so comparing gives the same order and the root can be skipped entirely. Sorting every point costs and ignores that may be tiny. A heap lets the cost depend on instead.
We first heapify every point by squared distance and pop times. Then we flip the idea around: keep a max-heap of only the best candidates so far, and evict the farthest one whenever a closer point arrives, which brings memory down to .
Definition
K Closest Points to Origin asks for the points of a list with the smallest distance to the origin, returned in any order. Ranking by the squared distance gives the same result. Heapify-and-pop runs in time; a size- max-heap runs in time and space.
In this topic
- 1Min-Heap Approach
- 2Max-Heap of Size K
Min-Heap Approach
The straightforward approach pushes all points into a min-heap keyed by squared distance, then pops times to get the closest.
Time complexity: for pushes, plus for pops. Space complexity: for the heap.
Alternatively, heapify the list in , then pop times for . When is small relative to , this is nearly linear.
The straightforward solution puts all points in a min-heap keyed by squared distance from origin, then pops times. Building the heap is via heapify, and popping times costs . Total time: . This is wasteful when because we're maintaining full state for points we will never return. Still optimal when .
Find the 2 closest points to the origin from: [(3,3), (1,1), (-2,2), (5,1)]. Use squared distances.
Max-Heap of Size K
A more space-efficient approach maintains a max-heap of size . The max-heap tracks the closest points seen so far, with the farthest of the at the root.
For each new point:
- If the heap has fewer than elements, push it.
- If the new point is closer than the root (farthest of the current ), pop the root and push the new point.
Time complexity: --- each of elements does at most one push/pop on a heap of size . Space complexity: .
Since Python's heapq is a min-heap, simulate a max-heap by negating distances.
A smarter approach maintains a max-heap of size exactly containing the closest points seen so far. For each new point, if it is closer than the max (the root), replace the root with the new point (). Total time: , which beats when is small. Space is instead of . In Python, negate distances to simulate a max-heap with heapq. This pattern generalizes to all 'top-k' problems.
Use a max-heap of size 2 to find the 2 closest points from [(3,3), (1,1), (-2,2), (5,1)].
Related Problems on PixelBank
A database sorts a table too large for memory by sorting chunks that fit, writing each chunk to disk as a sorted run, and then combining the runs into one sorted file. A search engine combines ranked results from many index shards the same way, and a log aggregator merges time-ordered streams from many servers. Each case has sequences that are already sorted, and the goal is one sorted output.
The previous topic, K Closest Points, used a heap to pick the best items out of . Here the heap plays a different role: it holds exactly one candidate per input list, the smallest element of that list not yet output. The next output must be the smallest of these heads, because every later element of a list is at least as large as its head. Pop it, append it, and push the next element from the same list. Concatenating and sorting costs for total elements, and merging lists one at a time into an accumulator can cost . The heap brings it to .
We first trace the k-way merge itself. Then we handle ties, where Python's tuple comparison can crash on incomparable list nodes.
Definition
Merge K Sorted Lists combines individually sorted sequences with elements in total into one sorted sequence. A k-way merge keeps a min-heap holding the current head of each non-empty list; each step pops the smallest head, outputs it, and pushes the next element of that list. It runs in time with extra space.
In this topic
- 1K-Way Merge with Min-Heap
- 2Handling Ties and Custom Comparisons
K-Way Merge with Min-Heap
The algorithm maintains a min-heap of size , where each entry contains the current smallest element from one of the lists along with an identifier for which list it came from.
Steps:
- Push the first element from each of the lists into the heap (with list index and element index).
- Pop the minimum element from the heap and add it to the result.
- If that list has more elements, push the next element from the same list.
- Repeat until the heap is empty.
Time: where is the total element count across all lists. Space: for the heap (ignoring the output).
To merge sorted lists with total elements, use a min-heap of size containing the current head of each list. Initialize with the first element from each list ( heapify). Repeatedly pop the minimum, append to output, and push the next element from the same list ( per element). Total time: , space: . Compare to merging lists pairwise (merging list into the accumulator), which is — strictly worse for .
Merge three sorted lists: [1, 4, 7], [2, 5, 8], [3, 6, 9] using a min-heap.
Handling Ties and Custom Comparisons
When two elements from different lists have the same value, Python's heapq will try to compare the next item in the tuple. If the next item is not comparable (e.g., a list node), this raises a TypeError.
Solution: Include a unique tiebreaker (like a counter or list index) as the second element of the tuple:
(value, tiebreaker_index, list_id)
This guarantees a total ordering. The tiebreaker does not affect correctness --- it only ensures the heap can resolve equal values without comparing incomparable objects.
This pattern is essential in Python because heapq does not support custom comparator functions like Java's PriorityQueue.
Python's heapq orders by the full tuple comparison, so when two keys tie, it falls through to the next element. If that element is a list node (not comparable), you get a TypeError. The standard fix is to include a unique tie-breaker: push tuples (key, counter, node) where counter is a monotonically increasing integer. This guarantees the tuple comparison never reaches the uncomparable node. Tie-breaking does not affect asymptotic complexity — only a small constant per push.
Merge [[1, 3, 3], [1, 2, 4]] using a heap. Show how to handle the duplicate value 1.
Related Problems on PixelBank
A leaderboard shows the score needed to reach the top 100, a monitoring system reports a latency percentile, and a trading system tracks the tenth-largest order seen today. Each asks for one order statistic, the element that would sit at position if everything were sorted, and in the streaming cases the data never stops arriving.
The previous topic, Merge K Sorted Lists, used a heap as a frontier across several sorted inputs. Kth Largest Element returns to a single unsorted collection. Sorting answers the question in but orders far more than needed. The heap insight is to keep a min-heap of the largest values seen so far. Its root is the smallest of those , which is exactly the th largest overall. A new value below the root cannot enter the top and is discarded; a larger value evicts the root. The cost becomes with memory, and the answer stays ready after every arrival. Quickselect, which partitions like quicksort, is faster on average for a fixed array but cannot serve a stream.
We first solve the static array version. Then we wrap the size- heap in a class that answers queries on a stream.
Definition
The th largest element of a collection of values is the value at position when the collection is sorted in descending order, counting duplicates. A min-heap holding the largest values seen so far keeps it at the root, giving time and space, with per update on a stream.
In this topic
- 1Min-Heap of Size K
- 2Streaming Kth Largest (KthLargest Class)
Min-Heap of Size K
To find the th largest element, maintain a min-heap of exactly elements. After processing all elements, the root of the heap is the th largest.
Algorithm:
- Push the first elements into a min-heap.
- For each remaining element: if it is larger than the heap root, replace the root (pop + push, or use heapreplace).
- After all elements are processed, the root is the th largest.
Why min-heap, not max-heap? The min-heap lets us efficiently check and remove the smallest of our top- candidates. If we used a max-heap, we would need to track the minimum of the elements, which is .
Time: . Space: .
To find the -th largest in an array, maintain a min-heap of size containing the largest values seen so far. For each new value: if heap size , push; else if the value exceeds the root, replace the root. The root is always the -th largest. Total time: , space: . Quickselect achieves average but worst case; the heap approach offers a guaranteed worst case, which is preferred when latency matters.
Find the 3rd largest element in [7, 2, 9, 1, 5, 8, 3] using a min-heap of size 3.
Streaming Kth Largest (KthLargest Class)
The heap-of-size- approach naturally extends to a streaming scenario where elements arrive continuously. The LeetCode problem "Kth Largest Element in a Stream" asks you to design a class that:
- Initializes with and an initial set of numbers.
- Has an add(val) method that returns the th largest after adding val.
The class maintains a min-heap of size . On each add:
- If the heap has fewer than elements, push val.
- Else if val > root, replace the root.
- Return the root (the th largest).
Each add call is , making this highly efficient for long streams.
For an online setting where elements arrive one at a time, maintain a min-heap of size exactly . Each add(val) pushes and, if size exceeds , pops the minimum — amortized. The current -th largest is always the root, retrievable in . For adds, total time is . The heap only ever stores the top- candidates, using space regardless of how many elements have streamed through.
Design a KthLargest stream with k=3, initial = [4, 5, 8, 2]. Then add: 3, 5, 10, 9, 4.
Related Problems on PixelBank
A task scheduler must run jobs so that two jobs of the same type never run back to back, and a playlist shuffler should avoid two songs by the same artist in a row. Reorganize String is the simplest form of this constraint: rearrange the characters of a string so that no two adjacent characters are equal, or report that it is impossible. For example, "aab" becomes "aba", but "aaab" has no valid arrangement.
The previous topic, Kth Largest Element, used a heap to track an order statistic. Here the heap drives a greedy decision. Characters are keyed by their remaining count, and at every position we place the most frequent character that differs from the one just placed. Spending the most common character early is what keeps the arrangement feasible: the character with the most copies needs the most separators, and postponing it can leave several copies at the end with nothing to put between them. A counting argument gives an instant impossibility test: if some character occurs more than times, no arrangement exists.
We first build the max-heap greedy with a held-back previous character. Then we see a heap-free alternative that fills even indices first.
Definition
Reorganize String asks for a permutation of a string of length in which no two adjacent characters are equal, or an empty string if none exists. A valid arrangement exists if and only if no character occurs more than times. The max-heap greedy builds one in time for an alphabet of size .
In this topic
- 1Greedy with Max-Heap
- 2Two-Character Alternation Pattern
Greedy with Max-Heap
The algorithm uses a max-heap (priority queue) sorted by character frequency:
- Count character frequencies.
- Push all characters with their (negated) counts into a max-heap.
- At each step, pop the most frequent character, add it to the result.
- Before pushing it back (with decremented count), pop and place the next most frequent. Then push the first character back.
Alternatively, at each step pop the top, append it, hold it aside, pop the next top, append, push back the held character, and so on.
Impossibility check: If any character appears more than times, rearrangement is impossible. With slots, the most any character can occupy without adjacency conflicts is every other slot.
Time: where is the alphabet size (at most 26). Space: for the heap.
To reorganize a string so no two adjacent characters are equal, greedily place the most-frequent unused character each step. Use a max-heap keyed by count. This fails iff some character's count exceeds , because it cannot be separated by other characters. Each step pops the heap, appends the character, decrements its count, and temporarily holds it until the next step (to prevent repeating). Total time: where is the alphabet size (26 for lowercase letters, so effectively ).
Reorganize the string "aaabbc" so no two adjacent characters are the same.
Two-Character Alternation Pattern
A simpler approach that avoids a heap works specifically for the "reorganize string" problem:
- Sort characters by frequency (descending).
- Fill even indices (0, 2, 4, ...) first with the most frequent characters.
- Then fill odd indices (1, 3, 5, ...) with the remaining characters.
This works because placing the most frequent character at even positions guarantees no two adjacent characters are the same --- adjacent characters come from different index groups.
Time: (using counting sort since we have at most 26 characters). Space: for the result array.
Both approaches are valid. The heap approach generalizes better --- for instance, to problems where the gap between same characters must be instead of 1.
After the greedy placement, the resulting string has adjacent characters alternating. The mathematical condition for feasibility is : if any character appears more than half the time, the pigeonhole principle forces two copies into adjacent slots. A variation uses a buffer slot to hold the previously used character until it can be re-pushed — guaranteeing the second-most-frequent is used between consecutive uses of the most-frequent.
Reorganize "aabbcc" using the index-fill approach.
A monitoring dashboard shows the median response time of a service, updated with every request, because the median resists the few extreme outliers that distort an average. The numbers arrive one at a time without end, and the current median must be available after each arrival. Re-sorting on every query costs , and even inserting into a sorted array costs per arrival for the shifting.
The previous topic, Reorganize String, used a single max-heap to drive a greedy choice. Find Median from Data Stream needs two heaps working together. The median depends only on the one or two values in the middle, so the data can be split into a lower half and an upper half. A max-heap holds the lower half and exposes its largest value; a min-heap holds the upper half and exposes its smallest. If every lower value is at most every upper value and the sizes differ by at most one, the median sits at the heap roots. Each insertion costs and each query .
We first set up the two-heap architecture and its invariants. Then we derive a fixed rebalancing protocol that keeps both invariants true after every insertion.
Definition
Find Median from Data Stream maintains a growing multiset of numbers under two operations: addNum inserts a value, and findMedian returns the middle value, or the mean of the two middle values when the count is even. A max-heap for the lower half and a min-heap for the upper half, with sizes differing by at most one, give inserts and queries.
In this topic
- 1Two-Heap Architecture
- 2Rebalancing Strategy
Two-Heap Architecture
Split the data into two halves:
- max_heap (left half): stores the smaller half of numbers. The largest of the small half is at the root.
- min_heap (right half): stores the larger half of numbers. The smallest of the large half is at the root.
Invariants:
- Every element in max_heap every element in min_heap.
- The heaps are balanced: their sizes differ by at most 1.
Adding a number:
- Push to max_heap (negate for Python).
- Move max_heap's root to min_heap (ensures invariant 1).
- If min_heap is larger, move its root back to max_heap (ensures invariant 2).
Finding the median:
- If heaps are equal size: average of both roots.
- If max_heap is larger: max_heap root.
Time: per add, per findMedian. Space: total.
Maintain a max-heap lo for the lower half and a min-heap hi for the upper half, with the invariant . On insert, push to lo then move lo's max to hi; if , move hi's min back to lo. The median is lo.top() (odd ) or (even ). Insert is , median query is . Space is .
Add numbers 5, 2, 8, 1, 9 one at a time. Show the median after each addition.
Rebalancing Strategy
The rebalancing after each insertion follows a fixed 3-step protocol that guarantees both invariants:
def addNum(num):
heappush(max_heap, -num) # Step 1: add to left half
heappush(min_heap, -heappop(max_heap)) # Step 2: move max of left to right
if len(min_heap) > len(max_heap): # Step 3: rebalance sizes
heappush(max_heap, -heappop(min_heap))
Why this works:
- Step 2 ensures the largest element in the left half moves right, maintaining the ordering invariant.
- Step 3 ensures the left half is never smaller than the right, so the median is always accessible.
This protocol handles all edge cases: first element, equal values, negative numbers, already-sorted input, reverse-sorted input. Each step is a single heap operation, so the total is exactly 3 or 5 heap operations per insertion.
The two heaps must maintain balance after every insert. The cleanest invariant is: always push to lo first, then pop-and-move to hi; if hi becomes larger than lo, pop-and-move back to lo. This guarantees at all times. Each rebalance is — at most 2 pushes and 2 pops per insert. The constant factor is small, but rebalancing every step is the price of median queries. Approximate online medians (e.g., Greenwald-Khanna quantile sketches) can be faster but give only -approximations.
Trace the heap states when adding [3, 1, 4, 1, 5] and show the median after each step.