Heap / Priority Queue Field Notes
A tree that lives inside an array and always keeps the smallest (or largest) item at the front. After this page you can draw a heap as a tree and as an array, push and pop by hand, build one in O(n), and use Python's heapq without looking anything up.
0Why heaps exist
A hospital emergency room. People arrive in some order, but the doctor does not take them in that order. The doctor takes the most urgent patient next, every time. Nobody re-sorts the whole waiting room when a new patient walks in; the room only needs to know who is most urgent right now.
A normal queue is FIFO: first in, first out. A priority queue removes items by priority instead: the item with the highest priority comes out first, no matter when it was inserted. A heap is the data structure that implements a priority queue, and the two words are often used as if they mean the same thing. It is a special tree-based structure, and its trick is that it can give you the minimum (or maximum) in O(1) and add or remove an item in O(log n), without ever fully sorting.
Two types. A min heap keeps the smallest value at the root, so the smallest has the highest priority to be removed. A max heap keeps the largest at the root. The lessons focus on min heaps; a max heap is the same code with the comparisons flipped.
1Heap properties
Two rules a binary tree must satisfy to be a heap
A binary heap is a binary tree with two extra rules. Both must hold at all times.
- Structure property. The tree is a complete binary tree: every level is completely filled except possibly the lowest level, and the lowest level is filled contiguously from left to right (no gaps).
- Order property (min heap). Every descendant is greater than or equal to its ancestors. If the root has value
y, every node in the left subtree and the right subtree has a value>= y. This is recursive, like the BST property: it holds again at every subtree. In a max heap every node in the subtrees is<= y.
Unlike a BST, a heap may contain duplicate values. The lesson heap below has 19 twice.
Implementation: arrays under the hood
Heaps are drawn as trees but stored as arrays. No node objects, no left or right pointers. The lesson's example heap is the tree below: root 14; children 19 and 16; 19's children 21 and 26; 16's children 19 and 68; 21's children 65 and 30. In NeetCode's drawing the array is [14,19,16,21,26,19,68,65,30,null,null,null,null,null,null], with unused slots shown as null.
How to fill the array: take an array of size n + 1 where n is the number of nodes. Visit the nodes in BFS order (level by level, left to right) and write them one after another, but start at index 1, not index 0.
One-based indexing: the three formulas
Starting at index 1 lets us compute where a node's children and parent live. Because a binary heap is a complete tree, the array has no gaps, so no pointers are needed. For a node at index i:
leftChild = 2 * i
rightChild = 2 * i + 1
parent = i // 2 # integer division
# example: node 19 sits at index 2
# left child = 4 → 21
# right child = 5 → 26
# parent = 1 → 14
These formulas only work when the tree is complete and the array is filled contiguously from left to right. That is exactly why the structure property exists.
If 14 were stored at index 0, the formula 2 * 0 = 0 would say its left child is also at index 0. That is wrong. Starting at index 1 makes all three formulas clean. (Python's heapq uses index 0 with slightly different formulas; section 4 shows them.)
class Heap:
def __init__(self):
self.heap = [0] # index 0 holds a dummy value; real data starts at index 1
The number of real elements is len(self.heap) - 1. The last real element is self.heap[-1], which is also the right-most node of the lowest level. Push and pop both use that fact.
2Push and pop
Reading the min (or max) is O(1): return self.heap[1]. Push and pop are more work, because they must keep both properties true, but each is still O(log n). Both use the same idea: put the value in the only place the structure property allows, then swap it up or down until the order property is true again. That swapping is called percolating (or bubbling).
Push: append, then percolate up
Same heap as before; push 17. The structure property says a complete tree is filled contiguously, so 17 must go at index 10 (the next free slot, which is the left child of 26 at index 5). That may break the order property, because 17 is smaller than 26. Fix it by percolating 17 up until its parent is smaller:
- Compare 17 with its parent 26 (index 5). Since 17 < 26, swap.
- Compare 17 with its new parent 19 (index 2). 17 < 19, swap.
- Compare 17 with its new parent 14 (index 1). 17 > 14, stop.
def push(self, val):
self.heap.append(val)
i = len(self.heap) - 1
# Percolate up
while i > 1 and self.heap[i] < self.heap[i // 2]:
tmp = self.heap[i]
self.heap[i] = self.heap[i // 2]
self.heap[i // 2] = tmp
i = i // 2
// is floor division: it rounds down (5.5 becomes 5), which we need because indices must be whole numbers. The loop condition i > 1 stops at the root, which has no parent. The three-line tmp swap is NeetCode's version; in Python you can write it as self.heap[i], self.heap[i // 2] = self.heap[i // 2], self.heap[i]. For a max heap, flip the comparison to >.
| Step | i | Compare | Action | Array after (index 0 is the dummy) |
|---|---|---|---|---|
| append | 10 | - | 17 goes to the next free slot | [0, 14, 19, 16, 21, 26, 19, 68, 65, 30, 17] |
| 1 | 10 | 17 vs parent 26 at 5 | 17 < 26 → swap, i = 5 | [0, 14, 19, 16, 21, 17, 19, 68, 65, 30, 26] |
| 2 | 5 | 17 vs parent 19 at 2 | 17 < 19 → swap, i = 2 | [0, 14, 17, 16, 21, 19, 19, 68, 65, 30, 26] |
| 3 | 2 | 17 vs parent 14 at 1 | 17 > 14 → loop ends | unchanged |
Pop: the obvious way is wrong
Pop removes the highest-priority value, which is the root. The tempting way: remove the root, pull up the smaller child to fill the gap, then fill that gap with the smaller of its children, and so on. The order property stays fine. But look at the structure: after popping 14 and pulling up 16, then pulling 19 up to replace 16, node 19 at index 3 is missing its left child. The tree is no longer complete, the array has a hole, and every index formula breaks.
Pop: the right way
- Read the root element. That is the value to return.
- Take the right-most node of the last level (the last array element) and move it into the root. Now the structure property is maintained, because you removed a node from the only place a complete tree can lose one. The order property is violated instead.
- Percolate the new root down: swap it with
min(left_child, right_child)again and again, until both children are>=it or it has no children. Here: 30 goes to the root, swaps with 16, then swaps with 19.
def pop(self):
if len(self.heap) == 1:
return None
if len(self.heap) == 2:
return self.heap.pop()
res = self.heap[1]
# Move last value to root
self.heap[1] = self.heap.pop()
i = 1
# Percolate down
while 2 * i < len(self.heap):
if (2 * i + 1 < len(self.heap) and
self.heap[2 * i + 1] < self.heap[2 * i] and
self.heap[i] > self.heap[2 * i + 1]):
# Swap right child
tmp = self.heap[i]
self.heap[i] = self.heap[2 * i + 1]
self.heap[2 * i + 1] = tmp
i = 2 * i + 1
elif self.heap[i] > self.heap[2 * i]:
# Swap left child
tmp = self.heap[i]
self.heap[i] = self.heap[2 * i]
self.heap[2 * i] = tmp
i = 2 * i
else:
break
return res
Walking through pop() line by line
- Heap is empty (length 1, only the dummy): return
None. - One node (length 2):
self.heap.pop()removes and returns it. No adjustment needed. - Otherwise store the root in
res, then move the last value into the root withself.heap[1] = self.heap.pop(). Python'slist.pop()with no argument removes the last element and returns it, which is exactly "take the right-most node of the last level". - The
whileloop runs as long as a left child exists:2 * i < len(self.heap). There are three cases for a node: no children, only a left child, two children. A heap can never have only a right child, because that would not be complete. - Inside the loop a left child is guaranteed. So first check whether a right child exists (
2 * i + 1in bounds), whether it is smaller than the left child, AND whether it is smaller than the current node. If all three: swap with the right child. - Else, if the current node is greater than its left child: swap with the left child. (This also handles the case where the right child exists but the left one is smaller or equal.)
- Else the node is already in the right position:
break.
| Step | i | Children | Action | Array after |
|---|---|---|---|---|
| start | - | - | res = 14; last element 30 moves to index 1 | [0, 30, 19, 16, 21, 26, 19, 68, 65] |
| 1 | 1 | left 19 at 2, right 16 at 3 | right < left and 30 > 16 → swap right, i = 3 | [0, 16, 19, 30, 21, 26, 19, 68, 65] |
| 2 | 3 | left 19 at 6, right 68 at 7 | right is not smaller; 30 > 19 → swap left, i = 6 | [0, 16, 19, 19, 21, 26, 30, 68, 65] |
| 3 | 6 | 2 * 6 = 12 >= 9: none | loop ends; return 14 | unchanged |
The right child is chosen only when it is strictly smaller than the left child. If both children are 19, the code swaps with the left one. Either choice gives a valid heap, but if you trace by hand you must follow the code's rule or your array will not match.
| Operation | Big-O |
|---|---|
| Get min / max | O(1) |
| Push | O(log n) |
| Pop | O(log n) |
Push and pop are O(log n) because a complete tree is always balanced: its height is about log n, and each percolation moves at most one level per swap.
3Heapify: build a heap in O(n)
Building a BST with n elements costs O(n log n): n insertions, each O(log n). Building a heap by pushing the elements one at a time also costs O(n log n). But there is a faster way. Heapify takes any array and turns it into a valid heap in O(n).
The idea
The structure property is free: an array with no gaps is a complete tree. Only the order property (parent <= children) needs work. We get it by percolating each element down until it is in the correct position, exactly like the second half of pop(). Leaf nodes cannot move lower, so we skip the whole last level and start from the last non-leaf node, working backwards to the root.
def heapify(self, arr):
# 0-th position is moved to the end
arr.append(arr[0])
self.heap = arr
cur = (len(self.heap) - 1) // 2
while cur > 0:
# Percolate down
i = cur
while 2 * i < len(self.heap):
if (2 * i + 1 < len(self.heap) and
self.heap[2 * i + 1] < self.heap[2 * i] and
self.heap[i] > self.heap[2 * i + 1]):
# Swap right child
tmp = self.heap[i]
self.heap[i] = self.heap[2 * i + 1]
self.heap[2 * i + 1] = tmp
i = 2 * i + 1
elif self.heap[i] > self.heap[2 * i]:
# Swap left child
tmp = self.heap[i]
self.heap[i] = self.heap[2 * i]
self.heap[2 * i] = tmp
i = 2 * i
else:
break
cur -= 1
- Move the element at index 0 to the end, so index 0 becomes the dummy slot and the real data starts at index 1. (The old value is still sitting at index 0, but nothing ever reads it.)
- Compute the index of the first non-leaf node:
n // 2, wherenis the number of elements. In the code that is(len(self.heap) - 1) // 2. - Starting from that node, percolate down exactly as in
pop(). - Decrement the index by 1 and repeat for the next non-leaf node, until the root is done.
Take the unsorted array [68, 30, 65, 21, 19, 14, 26, 16, 19]. After step 1 the heap array is [68, 30, 65, 21, 19, 14, 26, 16, 19, 68]; the real tree is indices 1 to 9. With n = 9 the first non-leaf is index 4, so indices 5 to 9 are leaves and are skipped. The lesson's own visual does the same: heapify runs on the nodes from index 4 down to 1.
cur | Node | Children | What happens | Indices 1..9 after |
|---|---|---|---|---|
| 4 | 19 | 19 at 8, 68 at 9 | 19 is not greater than either child → no swap | [30, 65, 21, 19, 14, 26, 16, 19, 68] |
| 3 | 21 | 26 at 6, 16 at 7 | right 16 is smaller and 21 > 16 → swap right; 21 lands on a leaf | [30, 65, 16, 19, 14, 26, 21, 19, 68] |
| 2 | 65 | 19 at 4, 14 at 5 | right 14 is smaller and 65 > 14 → swap right; 65 lands on a leaf | [30, 14, 16, 19, 65, 26, 21, 19, 68] |
| 1 | 30 | 14 at 2, 16 at 3 | swap left (14); at 2: children 19, 65 → swap left (19); at 4: children 19, 68 → swap left (19); at 8: leaf, stop | [14, 19, 16, 19, 65, 26, 21, 30, 68] |
The result is a valid min heap: 14 at the top, and every parent is <= its children. Note that the root did 3 swaps while index 4 did none. That imbalance is the whole reason heapify is O(n).
Why O(n) and not O(n log n)
With n nodes, roughly n/2 are leaves and n/2 are non-leaves. Only the non-leaves percolate down. Nodes on the second-lowest level can percolate at most 1 level. The level above that can percolate at most 2. And so on: the root can percolate all the way down. But the number of nodes halves each level you go up, while the distance they can travel only grows by one. The sum of all that work simplifies to O(n).
| Operation | Big-O |
|---|---|
| Get min / max | O(1) |
| Push | O(log n) |
| Pop | O(log n) |
| Heapify | O(n) |
Heapify is O(n), but popping everything out afterwards is n pops of O(log n) each, so "heapify then pop all" is O(n log n). That is heap sort. Heapify only wins when you need the heap itself, not a fully sorted list.
4heapq: the heap you actually use in Python
The lesson code is for understanding. In interviews and real code you use the heapq module from the standard library. It is a min heap built on a plain Python list. Three things to know before anything else: it is 0-indexed (no dummy slot), it is a set of functions that operate on your list (not a class), and it only does min heaps directly.
2i+1, 2i+2 and (i-1)//2. The minimum is always heap[0]. You rarely write these formulas yourself with heapq, but you must know that heap[0] is the min and that the rest of the list is not sorted.The API
import heapq
heap = []
heapq.heappush(heap, 19) # O(log n), percolate up
heapq.heappush(heap, 14)
heapq.heappush(heap, 16)
heap[0] # → 14 peek at the min, O(1). Do NOT pop to look.
heapq.heappop(heap) # → 14 O(log n), last-to-root then percolate down
nums = [14, 19, 16, 21, 26, 19, 68, 65, 30]
heapq.heapify(nums) # O(n), in place, reorders the SAME list
nums[0] # → 14
heapq.heappushpop(heap, x) # push x, then pop the min (faster than two calls)
heapq.heapreplace(heap, x) # pop the min, then push x (heap must be non-empty)
heapq.nlargest(2, [5, 1, 9, 3, 7]) # → [9, 7]
heapq.nsmallest(2, [5, 1, 9, 3, 7]) # → [1, 3]
heapq.nsmallest(1, ["kiwi", "fig", "banana"], key=len) # → ["fig"]
nlargest and nsmallest return a sorted list of the k extreme items and accept a key= like sorted does. For small k they run in about O(n log k); for k close to n it is cheaper to just sort.
Max heap: negate on the way in, negate on the way out
heapq has no max-heap mode. The standard trick: push -x instead of x. The smallest negative is the largest original, so -heap[0] is the max. Every place you read a value, put the minus sign back.
maxh = []
for v in [3, 10, 7]:
heapq.heappush(maxh, -v) # store negatives
-maxh[0] # → 10 the max
-heapq.heappop(maxh) # → 10
-heapq.heappop(maxh) # → 7
# LeetCode 1046 Last Stone Weight: always smash the two heaviest
def lastStoneWeight(stones):
heap = [-s for s in stones]
heapq.heapify(heap)
while len(heap) > 1:
first = -heapq.heappop(heap) # heaviest
second = -heapq.heappop(heap) # second heaviest
if first != second:
heapq.heappush(heap, -(first - second))
return -heap[0] if heap else 0
lastStoneWeight([2, 7, 4, 1, 8, 1]) # → 1
Tuples: priority first, tie-breaker second, payload last
Python compares tuples element by element, so a heap of tuples is ordered by the first element, then the second, and so on. Put the priority first. If two items can have the same priority, put a tie-breaker (a counter, an index) second, so the comparison never reaches the payload. This matters when the payload is an object that cannot be compared: Python raises TypeError.
tasks = []
heapq.heappush(tasks, (2, 1, "write tests")) # (priority, insertion order, payload)
heapq.heappush(tasks, (1, 2, "fix bug"))
heapq.heappush(tasks, (1, 0, "answer pager"))
heapq.heappop(tasks) # → (1, 0, "answer pager") priority 1, earliest
heapq.heappop(tasks) # → (1, 2, "fix bug")
heapq.heappop(tasks) # → (2, 1, "write tests")
# without a tie-breaker, equal priorities compare the payloads:
heapq.heappush(bad, (1, Job("a")))
heapq.heappush(bad, (1, Job("b"))) # TypeError: '<' not supported between Job and Job
heapq.heapify(nums) returns None and reorders nums in place. Writing heap = heapq.heapify(nums) gives you heap = None. Also, never call heappush on a list you did not heapify first (unless it started empty): the functions assume the list is already a valid heap.
5Moves that solve most heap problems
The lessons teach the structure. Interviews test whether you recognize when a heap is the tool. These four moves cover most heap questions.
Move 1: Top-K with a heap of size k
When: "find the k largest / smallest / closest / most frequent". Keep a heap of size k. For the k largest, use a min heap: push every element, and whenever the heap grows past k, pop the smallest. What survives is the k largest, and the root is the k-th largest. Cost O(n log k), which beats sorting when k is small.
def findKthLargest(nums, k): # LeetCode 215
heap = []
for x in nums:
heapq.heappush(heap, x)
if len(heap) > k:
heapq.heappop(heap) # throw away the smallest
return heap[0] # k-th largest sits at the root
findKthLargest([3, 2, 1, 5, 6, 4], 2) # → 5
class KthLargest: # LeetCode 703: same idea, but the stream keeps coming
def __init__(self, k, nums):
self.k = k
self.heap = nums
heapq.heapify(self.heap)
while len(self.heap) > k:
heapq.heappop(self.heap)
def add(self, val):
heapq.heappush(self.heap, val)
if len(self.heap) > self.k:
heapq.heappop(self.heap)
return self.heap[0]
def kClosest(points, k): # LeetCode 973: k SMALLEST → max heap of size k
heap = []
for x, y in points:
d = x * x + y * y
heapq.heappush(heap, (-d, x, y)) # negate: max heap on distance
if len(heap) > k:
heapq.heappop(heap) # drop the farthest
return [[x, y] for _, x, y in heap]
def topKFrequent(nums, k): # LeetCode 347: count, then nlargest by count
count = Counter(nums)
return [x for x, _ in heapq.nlargest(k, count.items(), key=lambda p: p[1])]
k largest → min heap of size k (the root is the weakest survivor, so pop it). k smallest → max heap of size k (negate). Beginners get this backwards. Say out loud: "the root is the one I am willing to throw away".
Move 2: k-way merge
When: "merge k sorted lists / arrays / streams". Put the first element of every list in a heap, tagged with which list it came from. Pop the smallest, then push the next element from that same list. Every element passes through a heap of size at most k, so the total is O(N log k) for N elements. The tuple is (value, list_index, position): the list index is the tie-breaker.
def mergeKSorted(lists): # LeetCode 23 on plain lists
heap = []
for li, lst in enumerate(lists):
if lst:
heapq.heappush(heap, (lst[0], li, 0)) # (value, which list, index in it)
out = []
while heap:
val, li, j = heapq.heappop(heap)
out.append(val)
if j + 1 < len(lists[li]):
heapq.heappush(heap, (lists[li][j + 1], li, j + 1))
return out
mergeKSorted([[1, 4, 5], [1, 3, 4], [2, 6]]) # → [1, 1, 2, 3, 4, 4, 5, 6]
For linked lists (the real LeetCode 23) push (node.val, i, node) and advance with node.next; the i keeps two equal values from ever comparing ListNode objects.
Move 3: Two heaps for a running median
When: "median of a stream", or any "split the data into a lower half and an upper half" problem. Keep the lower half in a max heap and the upper half in a min heap. The median is then at one root or the average of both. Two rules after every insert: every element of the lower half must be <= every element of the upper half, and the sizes may differ by at most 1.
class MedianFinder: # LeetCode 295
def __init__(self):
self.small = [] # max heap (negated): lower half
self.large = [] # min heap: upper half
def addNum(self, num):
heapq.heappush(self.small, -num)
# rule 1: every element of small must be <= every element of large
if self.large and -self.small[0] > self.large[0]:
heapq.heappush(self.large, -heapq.heappop(self.small))
# rule 2: sizes may differ by at most 1
if len(self.small) > len(self.large) + 1:
heapq.heappush(self.large, -heapq.heappop(self.small))
if len(self.large) > len(self.small) + 1:
heapq.heappush(self.small, -heapq.heappop(self.large))
def findMedian(self):
if len(self.small) > len(self.large):
return -self.small[0]
if len(self.large) > len(self.small):
return self.large[0]
return (-self.small[0] + self.large[0]) / 2
Move 4: Heap or sort? The decision
A heap is not "a faster sort". It is a structure for when the data keeps changing or when you need only a few extremes. Use this table when you feel the urge to call sorted().
| You need | Use | Cost |
|---|---|---|
| Everything in order, once, data is fixed | sorted() | O(n log n) |
| The k smallest / largest, k much smaller than n | heap of size k, or nsmallest / nlargest | O(n log k) |
| Only the min (or max), once | min() / max() | O(n) |
| The min repeatedly, with inserts in between | heap | O(log n) each |
| Items arrive over time (a stream) and you must answer as you go | heap (or two heaps) | O(log n) each |
| Merge k sorted inputs | heap of size k | O(N log k) |
| Always process the "most urgent" thing next (scheduling, Dijkstra) | heap of (priority, item) | O(log n) each |
Rule of thumb: if the word "stream", "repeatedly", "each time", "k-th", or "top k" appears in the problem, think heap. If it says "sort" and the data does not change, just sort.
6Big-O summary
Say these out loud in interviews. n is the number of elements in the heap; k is the size of a bounded heap.
| Operation | Binary heap | Sorted Python list | Unsorted Python list |
|---|---|---|---|
Peek min (heap[0]) | O(1) | O(1) | O(n) |
| Push | O(log n) | O(n) insert + shift | O(1) |
| Pop min | O(log n) | O(n) pop(0) | O(n) find + remove |
| Build from n items | O(n) heapify | O(n log n) sort | O(n) |
| Build by n pushes | O(n log n) | - | - |
| Search for an arbitrary value | O(n) | O(log n) binary search | O(n) |
| Top-k of n (size-k heap) | O(n log k) | O(n log n) sort then slice | - |
| Space | O(n), one array, no pointers | O(n) | O(n) |
The heap gives up one thing to get its speed: there is no fast search for a random value, and no fast "delete this specific element". A heap only knows where its min is. If you need arbitrary deletes, the usual trick is lazy deletion: mark the item as dead in a set, and skip dead items when they reach the root.
7Mistakes to watch for
Only heap[0] is guaranteed to be the min. heap[1] is not the second smallest; it is just the smaller of two children. To get items in order you must pop repeatedly.
The lesson's class uses a dummy at index 0 and 2i, 2i+1, i//2. heapq has no dummy and uses 2i+1, 2i+2, (i-1)//2. Pick one and stay there; mixing them gives an invalid heap that looks fine at the root.
Replacing the root with the smaller child, repeatedly, keeps order but leaves a hole: the tree is no longer complete. Always move the last element to the root first, then percolate down.
You must swap with the smaller child, or the larger one becomes the parent of the smaller one and the order property breaks one level down. Check the right child exists before reading it.
heapify works in place and returns None. Write heapq.heapify(nums) on its own line and keep using nums.
With the negation trick, every read must be negated: -heap[0], -heapq.heappop(heap). Forgetting once returns the wrong sign and usually the wrong element.
For the k smallest you need a size-k max heap, so the root is the largest survivor and can be thrown away. A size-k min heap throws away the small ones, the opposite of what you want.
(priority, node) raises TypeError the first time two priorities tie. Insert a counter or index: (priority, i, node).
heapq.heappop([]) raises IndexError. Guard with while heap: or if heap:. The lesson's class returns None instead; know which behavior you are relying on.
Interview one-liners
- "A heap is a complete binary tree stored in an array, with every parent smaller than its children (min heap). Peek is O(1), push and pop are O(log n)."
- "Complete means filled level by level, left to right, so the children of index i are at 2i and 2i+1 and the parent is at i//2 with 1-based indexing."
- "Push appends at the end and percolates up; pop moves the last element to the root and percolates down toward the smaller child."
- "Heapify is O(n), not O(n log n): half the nodes are leaves and do nothing, and the number of nodes halves each level while the distance grows by one."
- "For the top k I keep a heap of size k, so the whole pass is O(n log k) instead of sorting in O(n log n)."
- "Python's
heapqis a 0-indexed min heap over a list; for a max heap I push negated values, and I use tuples with a tie-breaker for priorities." - "A heap does not support search or arbitrary delete; if I need that I use lazy deletion or a different structure."
8Practice, in this order
Notes do not make you good at this. Solving does. Do these on LeetCode with no notes open, then check against the templates above.
| # | Problem | Move it trains | Level |
|---|---|---|---|
| 1046 | Last Stone Weight | max heap via negation, pop two / push one | Easy |
| 703 | Kth Largest Element in a Stream | size-k min heap on a stream | Easy |
| 215 | Kth Largest Element in an Array | size-k min heap (also try nlargest) | Medium |
| 973 | K Closest Points to Origin | size-k max heap of (−distance, x, y) | Medium |
| 347 | Top K Frequent Elements | Counter + heap of (count, value) | Medium |
| 373 | Find K Pairs with Smallest Sums | k-way merge with (sum, i, j) tuples | Medium |
| 621 | Task Scheduler | max heap of counts + a cooldown queue | Medium |
| 767 | Reorganize String | max heap of counts, always take the most frequent | Medium |
| 355 | Design Twitter | k-way merge of per-user feeds with a heap | Medium |
| 23 | Merge k Sorted Lists | k-way merge, (val, i, node) tie-breaker | Hard |
| 295 | Find Median from Data Stream | two heaps | Hard |
Your next step today: LeetCode 1046. Write it with heapq and the negation trick from memory, run it, then re-read the max heap block in section 4 and compare.
9Self-check
Answer out loud before opening. If you cannot answer in one breath, reread that section.
Q1What are the two heap properties? Which one does "complete" belong to?
Structure property: the tree is complete, every level full except the last, which is filled left to right. Order property: in a min heap every descendant is >= its ancestors (every parent <= its children). "Complete" is the structure property.
Q2With 1-based indexing, where are the children and the parent of index 5?
Left child at 2 * 5 = 10, right child at 2 * 5 + 1 = 11, parent at 5 // 2 = 2. In the lesson heap, index 5 holds 26 and its parent at index 2 is 19.
Q3Why does the lesson start at index 1 and not 0?
With index 0, 2 * 0 = 0 would say the root's left child is the root itself. Starting at 1 makes 2i, 2i+1, i//2 all correct. heapq uses 0 with the adjusted formulas 2i+1, 2i+2, (i-1)//2.
Q4Can a heap contain duplicate values? Can a BST?
A heap can: the lesson heap has 19 twice, and the order property uses >=. A standard BST cannot (or must pick a side rule).
Q5Describe push in one sentence. Why is it O(log n)?
Append the value at the end (the next free slot of the complete tree), then swap it with its parent while it is smaller than the parent. It moves up one level per swap and the tree has about log n levels, because a complete tree is always balanced.
Q6Why is "replace the root with its smaller child, repeatedly" the wrong way to pop?
It keeps the order property but breaks the structure property: the last node moved up leaves a hole somewhere in the middle of the tree, so the tree is no longer complete and the index formulas no longer work.
Q7In percolate-down, which child do you swap with, and what happens if you pick the other one?
The smaller child. If you swap with the larger child, that larger value becomes the parent of the smaller child, which violates the order property one level down.
Q8Why is heapify O(n) when n pushes cost O(n log n)?
Heapify percolates down from the last non-leaf to the root. About n/2 nodes are leaves and do nothing; n/4 nodes can move at most 1 level, n/8 at most 2, and so on. The number of nodes halves while the distance grows by 1, and the sum is bounded by n. Pushing one at a time percolates up, and most nodes end up near the bottom where the path up is long.
Q9Which node does heapify start from, for n elements, and which nodes does it skip?
It starts at index n // 2, the last non-leaf node, and walks down to index 1. It skips indices n // 2 + 1 to n, which are all leaves. For n = 9, it processes 4, 3, 2, 1 and skips 5 to 9.
Q10How do you get a max heap from heapq, and what is the bug people write?
Push -x and negate again on every read: -heap[0], -heapq.heappop(heap). The bug is forgetting the second minus sign on the way out.
Q11You want the k smallest items from a huge list in O(n log k). Min heap or max heap of size k, and why?
A max heap of size k. Its root is the largest of the survivors, which is exactly the one to throw away when the heap grows past k. (For the k largest, use a min heap.)
Q12Trace it: push 12 onto the lesson heap [0, 14, 19, 16, 21, 26, 19, 68, 65, 30]. Give the array after each swap.
Append: 12 at index 10, parent 26 at 5. 12 < 26 → [0, 14, 19, 16, 21, 12, 19, 68, 65, 30, 26]. Parent now 19 at 2, 12 < 19 → [0, 14, 12, 16, 21, 19, 19, 68, 65, 30, 26]. Parent now 14 at 1, 12 < 14 → [0, 12, 14, 16, 21, 19, 19, 68, 65, 30, 26]. i = 1, loop ends. 12 is the new root.
Q13Trace it: pop twice from the lesson heap. What is returned each time and what is the array at the end?
Pop 1 returns 14: 30 goes to the root, swaps with 16, then with 19 → [0, 16, 19, 19, 21, 26, 30, 68, 65]. Pop 2 returns 16: 65 goes to the root; children are 19 and 19, the tie goes left → [0, 19, 65, 19, 21, 26, 30, 68]; at index 2 the children are 21 and 26, swap left → [0, 19, 21, 19, 65, 26, 30, 68]; index 4 has no children, stop.