A heap answers one question fast: what is the smallest thing right now? K-way merge asks it of K sorted lists at once. Two heaps ask it from both sides of the middle, which gives you a running median.
Pattern 9 used a heap to keep the K best items. It named two other heap moves and left them for later. This page covers both. They share one tool, heapq, and one habit: push tuples with a safe tie-break so Python never has to compare two objects it cannot order.
Reading the figure. Each row is one sorted list. Dashed cells are already in the output. The amber cells are the current heads, and the heap holds exactly those three as (value, list). The heap top is the next item to emit. After it leaves, only list 2 moves forward, so only one push follows.
(value, list_index, payload), never (value, payload). If two values tie, Python compares the next field. A list index is an int, so the tie ends there. A linked-list node has no <, so comparing two nodes raises TypeError. Only one entry per list sits in the heap at a time, so the index alone makes every entry unique.heapq is min-only, so the max-heap stores negated values.Reading the figure. The left tree is the lower half, with its largest value on top. The right tree is the upper half, with its smallest value on top. The amber tops face each other across the dashed median line. Every value on the left is at most every value on the right. So the median comes from the tops alone.
import heapq
def merge_sorted_arrays(arrays: list[list[int]]) -> list[int]:
"""All values from K sorted lists, in one sorted list.
Example:
>>> merge_sorted_arrays([[1, 4, 7], [2, 5], [3, 6, 9]])
[1, 2, 3, 4, 5, 6, 7, 9]
"""
# One entry per non-empty list: (head value, list index, position).
# The list index is the tie-break. Position 0 is each list's head.
heap = [(arr[0], index, 0) for index, arr in enumerate(arrays) if arr]
heapq.heapify(heap) # O(K) to build, cheaper than K pushes
merged: list[int] = []
# Invariant: heap holds the next unused item of every list not yet empty,
# so heap[0] is the smallest unused item overall.
while heap:
value, index, pos = heapq.heappop(heap)
merged.append(value)
# pos + 1 is the next item in the same list. Push it if it exists.
if pos + 1 < len(arrays[index]):
heapq.heappush(heap, (arrays[index][pos + 1], index, pos + 1))
return merged
In production code, list(heapq.merge(*arrays)) does the same thing lazily. Write the loop in the interview, then name heapq.merge.
def running_medians(stream: list[int]) -> list[float]:
"""The median after each new number arrives.
Example:
>>> running_medians([5, 15, 1, 3])
[5.0, 10.0, 5.0, 4.0]
"""
lower: list[int] = [] # max-heap of the small half, stored negated
upper: list[int] = [] # min-heap of the large half
medians: list[float] = []
# Invariant at the top of each pass: every value in lower is at most every
# value in upper, and len(lower) is len(upper) or len(upper) + 1.
for value in stream:
# Step 1: push into lower (negate it, max-heap). Then move lower's
# largest across, so upper gets the right value and order holds.
heapq.heappush(lower, -value)
heapq.heappush(upper, -heapq.heappop(lower)) # - undoes the negation
# Step 2: rebalance. Upper may now be one too big. Move its smallest
# back, negated, so lower is never smaller than upper.
if len(upper) > len(lower):
heapq.heappush(lower, -heapq.heappop(upper))
# Step 3: read the tops. lower[0] is the negated max of the low half.
if len(lower) > len(upper):
medians.append(float(-lower[0])) # odd count: lower top
else:
# Even count: average the two tops. / 2 is true division.
medians.append((-lower[0] + upper[0]) / 2)
return medians
Reading the figure. Each column is one moment while 1 is added. Blue cells are the lower half and violet cells the upper half. The amber cell is the value that just moved. The move up always sends the largest value of the lower half, so the order between halves holds. The move back only fixes the sizes.
The push-then-move dance in step 1 is the trick. It avoids comparing the new value with either top by hand, and it can never break the order between halves.
(value, node). On a tie, Python compares the nodes and raises TypeError. Put an int index in the middle.lower[0] is the negated max. Read it as -lower[0].// 2 on 1 + 2 gives 1, not 1.5. Use / 2.arr[0] on an empty list raises IndexError. Filter them out when you seed the heap.You are given k sorted linked lists. Merge them into one sorted linked list and return its head.
Seed a min-heap with the head node of every non-empty list. Pop the smallest node, link it onto the result, and push that node’s next. A dummy head node removes the “is the result empty yet” special case. The list index is the tie-break, so two nodes are never compared.
from __future__ import annotations
import heapq
class MergeNode:
"""A singly linked list node for this page's merge problems."""
def __init__(self, val: int = 0, next: MergeNode | None = None) -> None:
self.val = val # default 0: only the dummy head uses it
self.next = next
def build_merge_list(values: list[int]) -> MergeNode | None:
"""Linked list holding values, in order. Empty input gives None."""
head = None
# Build from the back, so each new node points at the one built before.
for value in reversed(values):
head = MergeNode(value, head)
return head
def merge_list_values(head: MergeNode | None) -> list[int]:
"""The values of a linked list, as a Python list."""
out: list[int] = []
while head is not None:
out.append(head.val)
head = head.next
return out
def merge_k_lists(lists: list[MergeNode | None]) -> MergeNode | None:
"""Merge k sorted linked lists into one sorted linked list.
Args:
lists: Heads of sorted linked lists. Any of them may be None.
Returns:
The head of the merged list, or None if every list is empty.
Example:
>>> heads = [build_merge_list([1, 4, 5]), build_merge_list([1, 3, 4]),
... build_merge_list([2, 6])]
>>> merge_list_values(merge_k_lists(heads))
[1, 1, 2, 3, 4, 4, 5, 6]
>>> merge_k_lists([None, None]) is None
True
"""
# (value, list index, node). The index breaks ties, so heapq never
# has to compare two MergeNode objects, which would raise TypeError.
heap: list[tuple[int, int, MergeNode]] = []
for index, head in enumerate(lists):
if head is not None: # skip empty lists: they have no head
heap.append((head.val, index, head))
heapq.heapify(heap) # O(k) build
dummy = MergeNode() # placeholder before the real head
tail = dummy # last node of the merged list so far
# Invariant: dummy.next .. tail is sorted and holds every popped node.
# heap holds the current head of every list not yet used up.
while heap:
_, index, node = heapq.heappop(heap) # _ : value, already in node
tail.next = node
tail = node
# Same index: the next node comes from the same list, and that list
# has no other entry in the heap, so the tie-break stays unique.
if node.next is not None:
heapq.heappush(heap, (node.next.val, index, node.next))
return dummy.next # skip the placeholder
Lists [1, 4, 5], [1, 3, 4], [2, 6]. Heap entries shown as (value, index):
(1,0) (1,1) (2,2). Pop (1,0), push (4,0). Out 1.(1,1), push (3,1). Out 1 1. The tie on value 1 was settled by index.(2,2), push (6,2). Then pop (3,1), push (4,1). Out 1 1 2 3.(4,0), push (5,0). Pop (4,1), nothing to push. Out 1 1 2 3 4 4.(5,0), then (6,2). Heap empty. Out 1 1 2 3 4 4 5 6.Reading the figure. Each row is the state after one step. Heap entries are (value, list index), smallest first. The amber entry is the next to pop. The violet entry was just pushed from the list that lost its head. Notice row 2: two entries tie on value 1, and the list index settles the order. Green cells are the merged output.
None: the heap starts empty and the result is None.TypeError appears.An n × n matrix has every row and every column sorted in ascending order. Return the k-th smallest element, counting duplicates.
Each row is a sorted list, so this is a K-way merge over n rows that stops early. Seed the heap with the first cell of each row. Pop k - 1 times, pushing the cell to the right each time. The heap top is then the k-th smallest. Only the first min(n, k) rows can matter, because the k-th smallest cannot sit below row k.
def kth_smallest_in_matrix(matrix: list[list[int]], k: int) -> int:
"""The k-th smallest value in a matrix with sorted rows and columns.
Args:
matrix: A non-empty n x n matrix. Rows and columns ascend.
k: Rank to find, 1 <= k <= n * n.
Returns:
The k-th smallest value, duplicates counted.
Example:
>>> kth_smallest_in_matrix([[1, 5, 9], [10, 11, 13], [12, 13, 15]], 8)
13
>>> kth_smallest_in_matrix([[-5]], 1)
-5
"""
n = len(matrix)
# Seed with column 0 of each row: (value, row, col). Rows past k cannot
# hold the answer, so min(n, k) rows is enough. row is the tie-break.
heap = [(matrix[row][0], row, 0) for row in range(min(n, k))]
heapq.heapify(heap)
# Pop k - 1 times: after that the smallest k - 1 are gone and the top
# of the heap is the k-th smallest.
# Invariant: heap holds the next unused cell of every seeded row.
for _ in range(k - 1):
_, row, col = heapq.heappop(heap)
# col + 1 is the next cell to the right. Push it if the row has one.
if col + 1 < n:
heapq.heappush(heap, (matrix[row][col + 1], row, col + 1))
return heap[0][0] # heap[0]: smallest entry. [0]: its value
Matrix rows [1, 5, 9], [10, 11, 13], [12, 13, 15], k = 8:
1, 10, 12, one per row.1, 5, 9. Row 0 is used up, so pop 3 pushes nothing.10, 11 and push row 1’s 11, then 13.12 and pushes row 2’s 13. Pop 7 removes row 1’s 13.13. Answer 13.Reading the figure. Each cell shows its value and the pop that removed it. Blue cells were popped. The order snakes across rows, because the heap always holds the next cell of each row. The green cell is on top of the heap after k - 1 = 7 pops. The grey 15 is never touched.
k = 1: no pops. Answer matrix[0][0].k = n * n: every cell but one is popped. Answer matrix[n - 1][n - 1].k: binary search on the value wins. Guess a value, count cells at or below it with a staircase walk in O(n), and narrow. That is O(n log(max - min)).Design a class with add_num(num), which adds a number from a stream, and find_median(), which returns the median of every number so far. Both should be fast for millions of calls.
Sorting on every call is O(n log n). Inserting into a sorted list is O(n). Two heaps give O(log n) to add and O(1) to read. The lower half lives in a max-heap of negated values. The upper half lives in a min-heap. Lower is allowed one extra item, so with an odd count the median is the lower top.
class MedianFinder:
"""Running median with two heaps.
lower is a max-heap (values negated) holding the smaller half.
upper is a min-heap holding the larger half. len(lower) is always
len(upper) or len(upper) + 1, and every lower value <= every upper value.
Example:
>>> finder = MedianFinder()
>>> finder.add_num(1)
>>> finder.add_num(2)
>>> finder.find_median()
1.5
>>> finder.add_num(3)
>>> finder.find_median()
2.0
"""
def __init__(self) -> None:
self.lower: list[int] = [] # negated values: lower[0] is -(max of low half)
self.upper: list[int] = [] # plain values: upper[0] is min of high half
def add_num(self, num: int) -> None:
"""Add num to the stream in O(log n)."""
# Push into lower, negated so heapq's min acts as a max.
heapq.heappush(self.lower, -num)
# Move lower's largest to upper. - undoes the negation. This keeps
# every lower value <= every upper value, whatever num was.
heapq.heappush(self.upper, -heapq.heappop(self.lower))
# Upper may now hold one more than lower. Move its smallest back,
# negated, so lower is equal or exactly 1 bigger.
if len(self.upper) > len(self.lower):
heapq.heappush(self.lower, -heapq.heappop(self.upper))
def find_median(self) -> float:
"""Median of every number added so far, in O(1).
Raises:
ValueError: If no number has been added.
"""
if not self.lower: # lower is empty only when both are
raise ValueError("no numbers added yet")
# Odd count: lower holds the extra one, and its top is the median.
# [0] is the heap top, and - turns the stored negative back.
if len(self.lower) > len(self.upper):
return float(-self.lower[0])
# Even count: average the two middle values. / 2 is true division,
# so 1 and 2 give 1.5, not 1.
return (-self.lower[0] + self.upper[0]) / 2
Add 1, 2, 3. Heaps shown as plain values:
{1} → move 1 up → upper too big, move back. lower {1}, upper {}. Median 1.0.{1, 2} → move max 2 up. lower {1}, upper {2}. Median (1 + 2) / 2 = 1.5.{1, 3} → move 3 up → upper {2, 3} too big, move 2 back. lower {1, 2}, upper {3}. Median 2.0.Reading the figure. Each column is the state after one add_num. Blue cells are the lower half and violet the upper half, shown sorted. The amber cells are the two heap tops, the only values find_median reads. With an odd count, lower is one bigger and its top is the answer.
find_median before any add: raise, or return None. Ask which they want.0..100: keep 101 counters and walk them. O(1) add, O(100) read.You have k sorted lists of integers. Find the smallest range [a, b] that includes at least one number from every list. A range is smaller if b - a is smaller, or if the widths tie and a is smaller.
Pick one number from each list. The range they cover runs from their min to their max. To shrink it, the only useful move is to raise the min, because raising anything else can only widen it. So keep a heap of the current pick from each list, which gives the min in O(1), and track the max by hand. Pop the min, record the range if it is the best yet, and replace it with the next number from the same list. Stop when any list runs out, because then no pick covers every list.
def smallest_range(nums: list[list[int]]) -> list[int]:
"""Smallest [lo, hi] holding at least one value from every list.
Args:
nums: k non-empty lists, each sorted ascending.
Returns:
[lo, hi] for the smallest such range. Ties go to the smaller lo.
Raises:
ValueError: If nums is empty or any list is empty.
Example:
>>> smallest_range([[4, 10, 15, 24, 26], [0, 9, 12, 20], [5, 18, 22, 30]])
[20, 24]
>>> smallest_range([[1, 2, 3], [1, 2, 3]])
[1, 1]
"""
if not nums or any(not row for row in nums):
raise ValueError("need at least one list, and no empty lists")
# One pick per list: (value, list index, position). Position 0 is the
# list's smallest value. The list index is the tie-break.
heap = [(row[0], index, 0) for index, row in enumerate(nums)]
heapq.heapify(heap)
# The heap gives the min. The max has to be tracked by hand.
current_max = max(row[0] for row in nums) # [0]: each list's head
# Start with the infinite range, so the first real range always wins.
best_lo, best_hi = float("-inf"), float("inf")
# Invariant: heap holds exactly one value from every list, and
# current_max is the largest of them. So [heap[0][0], current_max]
# covers every list.
while True:
low, index, pos = heapq.heappop(heap)
# Strict <: on a tie keep the earlier range, whose lo is smaller,
# because lo only grows as the loop runs.
if current_max - low < best_hi - best_lo:
best_lo, best_hi = low, current_max
# pos + 1 == len: this list has no next value, so no later pick can
# cover it. Every remaining range has been tried.
if pos + 1 == len(nums[index]):
return [best_lo, best_hi]
nxt = nums[index][pos + 1] # pos + 1: next in this list
heapq.heappush(heap, (nxt, index, pos + 1))
current_max = max(current_max, nxt) # the new pick may be the max
Lists [4, 10, 15, 24, 26], [0, 9, 12, 20], [5, 18, 22, 30]. Picks shown as a set:
{4, 0, 5}, range [0, 5], width 5. Best so far. Replace 0 with 9.{4, 9, 5}, range [4, 9], width 5. A tie, so keep [0, 5]. Replace 4 with 10.[5, 10], [9, 18], [10, 18], [12, 18], [15, 20], [18, 24]. None is narrower than 5.{24, 20, 22}: pop 20, range [20, 24], width 4. New best.[20, 24].Reading the figure. Each bar is the range of the current picks, one row per step, top to bottom. The white dots are the three picks, one from each list. The left dot is always the heap’s min, and it is the one replaced next. Green bars are a new best. The amber bar ties on width and loses on the left end. The search stops after the last row, because list 1 has run out.
[x, x], found on the first pop.0, as in the second doctest.(value, index, payload). The int index ends every tie, so heapq never compares nodes./ 2, not // 2, for an even-count median. Negate on the way out of the max-heap.Pattern 21, Weighted Shortest Paths, puts the same heap of (cost, tie-break, node) tuples at the centre of Dijkstra’s algorithm. There the heap holds the frontier of a graph instead of the heads of K lists.