632. Smallest Range Covering Elements from K Lists
Quick Identifier
- Topic: heap-priority-queue
- Pattern: Top-K / Heap (k-way min tracking + global max) with Sliding Window-style pointer advance
- Difficulty: Hard
- Companies: Google, Amazon, Microsoft, LinkedIn, DoorDash
- Frequency: High
- LeetCode: 632
Quick Sights
- Time Complexity:
kfrom the optimal approach. - Space Complexity:
maxValuefrom the optimal approach. - Core Mechanism: You are given
ksorted integer lists. Choose exactly one number from each list (any position) so that the numeric range from min to max among thosekpicks is as small as possible. Return[min, max]for an optimal choice (any smallest range if tied).
Concept Explanation
You are given k sorted integer lists. Choose exactly one number from each list (any position) so that the numeric range from min to max among those k picks is as small as possible. Return [min, max] for an optimal choice (any smallest range if tied).
State the invariant before coding: what partial result is already correct, what work remains, and what value or state each recursive/iterative step must preserve.
Recognition cues:
- “Smallest range” across k sorted streams with one pick per stream
- Always maintain one active element per list — the best configuration is discoverable by advancing only the list that currently contributes the minimum (greedy + exchange argument)
- Naive: all combinations — need heap + running maximum
Study Pattern (SDE3+)
- 0-3 min: restate I/O and brittle edges aloud, then verbalize two approaches with time/space before you touch the keyboard.
- Implementation pass: one-line invariant above the tight loop; state what progress you make each iteration.
- 5 min extension: pick one constraint twist (immutable input, stream, bounded memory, parallel readers) and explain what in your solution breaks without a refactor.
Diagram
This diagram shows the algorithm movement for this problem family.
Approaches
- Brute force — enumerate all tuples of indices — product of list sizes, intractable.
- Better — for each possible
minvalue, binary search in other lists — still messy and log factors stack. - Acceptable — merge all values with (value, listId) tags, then slide a window that hits all lists —
O(n log n)if you sortntotal elements; worse than needed. - Optimal — min-heap of size
kover the current head of each list, trackmaxValueover those heads, repeatedly pop min and push next from the same list —O(n log k)time /O(k)space forn= total elements across lists.
Optimal Solution
Go
import "container/heap"
type listHead struct {
value int
listIndex int
nextIndex int
}
type minByValue []listHead
func (items minByValue) Len() int { return len(items) }
func (items minByValue) Less(indexA, indexB int) bool {
return items[indexA].value < items[indexB].value
}
func (items minByValue) Swap(indexA, indexB int) { items[indexA], items[indexB] = items[indexB], items[indexA] }
func (items *minByValue) Push(value any) {
*items = append(*items, value.(listHead))
}
func (items *minByValue) Pop() any {
old := *items
lastIndex := len(old) - 1
popped := old[lastIndex]
*items = old[:lastIndex]
return popped
}
func smallestRange(lists [][]int) []int {
active := &minByValue{}
heap.Init(active)
maxValue := 0
for listIndex, numbers := range lists {
firstValue := numbers[0]
heap.Push(active, listHead{
value: firstValue,
listIndex: listIndex,
nextIndex: 1,
})
if listIndex == 0 || firstValue > maxValue {
maxValue = firstValue
}
}
// invariant: heap root is the current minimum among the k chosen front elements
bestLow := (*active)[0].value
bestHigh := maxValue
for {
smallest := heap.Pop(active).(listHead)
if smallest.nextIndex >= len(lists[smallest.listIndex]) {
break
}
nextValue := lists[smallest.listIndex][smallest.nextIndex]
if nextValue > maxValue {
maxValue = nextValue
}
heap.Push(active, listHead{
value: nextValue,
listIndex: smallest.listIndex,
nextIndex: smallest.nextIndex + 1,
})
newMin := (*active)[0].value
if maxValue-newMin < bestHigh-bestLow {
bestLow = newMin
bestHigh = maxValue
}
}
return []int{bestLow, bestHigh}
}JavaScript
class MinHeap {
constructor() {
this.values = [];
}
push(entry) {
this.values.push(entry);
this.bubbleUp(this.values.length - 1);
}
pop() {
const top = this.values[0];
const last = this.values.pop();
if (this.values.length > 0) {
this.values[0] = last;
this.bubbleDown(0);
}
return top;
}
peek() {
return this.values[0];
}
get size() {
return this.values.length;
}
bubbleUp(index) {
while (index > 0) {
const parentIndex = Math.floor((index - 1) / 2);
if (this.values[parentIndex].value <= this.values[index].value) {
break;
}
[this.values[parentIndex], this.values[index]] = [
this.values[index],
this.values[parentIndex],
];
index = parentIndex;
}
}
bubbleDown(index) {
const length = this.values.length;
while (true) {
const leftIndex = index * 2 + 1;
const rightIndex = index * 2 + 2;
let smallest = index;
if (
leftIndex < length &&
this.values[leftIndex].value < this.values[smallest].value
) {
smallest = leftIndex;
}
if (
rightIndex < length &&
this.values[rightIndex].value < this.values[smallest].value
) {
smallest = rightIndex;
}
if (smallest === index) {
break;
}
[this.values[index], this.values[smallest]] = [
this.values[smallest],
this.values[index],
];
index = smallest;
}
}
}
function smallestRange(lists) {
const heap = new MinHeap();
let maxValue = Number.NEGATIVE_INFINITY;
for (let listIndex = 0; listIndex < lists.length; listIndex++) {
const firstValue = lists[listIndex][0];
heap.push({
value: firstValue,
listIndex,
nextIndex: 1,
});
maxValue = Math.max(maxValue, firstValue);
}
let bestLow = heap.peek().value;
let bestHigh = maxValue;
while (true) {
const smallest = heap.pop();
if (smallest.nextIndex >= lists[smallest.listIndex].length) {
break;
}
const nextValue = lists[smallest.listIndex][smallest.nextIndex];
maxValue = Math.max(maxValue, nextValue);
heap.push({
value: nextValue,
listIndex: smallest.listIndex,
nextIndex: smallest.nextIndex + 1,
});
const newMin = heap.peek().value;
if (maxValue - newMin < bestHigh - bestLow) {
bestLow = newMin;
bestHigh = maxValue;
}
}
return [bestLow, bestHigh];
}Walkthrough
Lists: [4,10,15], [1,9,12], [7,11,18]. Initialize heap with (4,0), (1,1), (7,2) → maxValue=10, min=1 → range 9. Pop 1 from list 1, push 9 → active {4,9,7}, max=10, min=4 → range 6. Continue until a list exhausts; track narrowest range.
Invariant: at each step the heap holds exactly one “pointer” per non-exhausted list; popping the smallest and advancing that list is the only move that can shrink the minimum without missing optimality (exchange argument).
Edge Cases
k == 1— range is[min(list), min(list)](single element on each side).- Duplicates across lists —
maxValuemust track the current window max, not the global max ever seen incorrectly. - One list much longer — algorithm stops when any list runs out during pop (cannot keep one-per-list anymore).
Pitfalls
- Initializing
bestHigh - bestLowwithout recording the first full window before any pop. - Using global max over all elements ever inserted instead of current
kheads. - Off-by-one on
nextIndexwhen pushing the successor element.
Similar Problems
- 21. Merge Two Sorted Lists — two-list merge (Easy stepping stone before k-way).
- 215. Kth Largest Element in an Array — heap selection on batch data (Medium; heap muscle memory).
- 23. Merge k Sorted Lists — identical k-pointer + heap advance pattern (Hard sibling).
Variants
- Smallest range with frequency / multiset — each list contributes multiple picks; problem statement changes feasibility check.
- Dynamic lists (streaming append) — rerun from scratch or maintain augmented structure; ask bounded delay vs exact optimum.
- Tie-break: smallest
bestLowwhen ranges tie — extend comparison to(range, bestLow)lexicographically.
Mind-Map Tags
#k-way-merge #min-heap #running-max #sorted-lists #smallest-range #greedy-advance-min
Mark this page when you finish learning it.
Last updated on
Spotted something unclear or wrong on this page?