632. Smallest Range Covering Elements from K Lists
At a Glance
- 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
Problem (one-liner)
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).
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
Diagram
At-a-glance flow (replace with problem-specific Mermaid as you refine this note). camelCase node IDs; no spaces in IDs.
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
Last updated on
Spotted something unclear or wrong on this page?