Heap · 11 of 18
Smallest range in K Lists
Find the shortest stretch of the number line that catches at least one number from every sorted list.
- Simple way
- time O(N * k log n)
room O(1) - Clever way
- time O(N log k)
room O(k)
The problem, simply
You get k lists of numbers, each sorted from small to big. Find the smallest range [a, b] that holds at least one number from every list. Smaller means b - a is smaller. If two ranges have the same width, the one that starts lower wins. N is the count of all numbers, and n is the length of one list.
For [[1, 5, 8], [4, 12], [7, 8, 10]] the answer is [4, 7]: it holds 5 from the first list, 4 from the second and 7 from the third.
A story to remember it
Picture it
The heap holds one number from each list. Its top, 4, is the smallest and the biggest is 7, so the range [4, 7] covers all three lists.
- max
- 7
- range
- [4, 7]
- looking now
- done
- path
Examples
A small case
- In
[[1, 5, 8], [4, 12], [7, 8, 10]]- Out
[4, 7]
Width 3. No other choice of one number per list is closer together.
The LeetCode case
- In
[[4, 10, 15, 24, 26], [0, 9, 12, 20], [5, 18, 22, 30]]- Out
[20, 24]
24 from the first list, 20 from the second, 22 from the third.
A shared number
- In
[[1, 2, 3], [1, 2, 3], [1, 2, 3]]- Out
[1, 1]
1 is in every list, so the range has width 0. It starts lower than [2, 2] and [3, 3].
One list
- In
[[10, 20]]- Out
[10, 10]
Any single number covers the only list. The lowest one wins the tie.
Way 1: the simple way
The best range starts at one of the numbers. So try every number x as the left end. For each list, find its smallest number that is at least x with binary search. The right end must reach the biggest of those.
- For every number
xin every list: - For every list, binary search the first number
>= x. If a list has none,xcannot be a left end. - The right end is the biggest of those numbers. Keep the narrowest range, with lower start on ties.
import java.util.List;
class Solution {
public int[] smallestRange(List<List<Integer>> nums) {
long bestWidth = Long.MAX_VALUE;
int bestLo = 0, bestHi = 0;
for (List<Integer> list : nums) {
for (int x : list) { // try x as the left end
int hi = x;
boolean ok = true;
for (List<Integer> other : nums) {
int j = firstAtLeast(other, x);
if (j == other.size()) { ok = false; break; }
hi = Math.max(hi, other.get(j));
}
long width = (long) hi - x;
if (ok && (width < bestWidth || (width == bestWidth && x < bestLo))) {
bestWidth = width; bestLo = x; bestHi = hi;
}
}
}
return new int[] { bestLo, bestHi };
}
private int firstAtLeast(List<Integer> list, int x) { // binary search
int lo = 0, hi = list.size();
while (lo < hi) {
int mid = (lo + hi) / 2;
if (list.get(mid) < x) lo = mid + 1; else hi = mid;
}
return lo;
}
}How long it takes: N left ends, each doing k binary searches, O(N * k log n). Extra room: O(1).
Way 2: the clever way
Walk through all lists together, like merging them, and keep exactly one number from each list in a min heap. Also remember the biggest number in the heap.
- Put the first number of each list in the heap. Set
maxto the biggest of them. - Repeat:
- The heap top is the smallest. The range
[top, max]covers every list. Keep it if it is the best so far. - Poll the top. If its list has no next number, stop.
- Offer that list's next number, and raise
maxif needed.
- The heap top is the smallest. The range
Why move only the smallest? Every range must cover the current smallest's list. Keeping the smallest and moving any other number can only push max up, so the range can not get better that way.
Smallest range of [1, 5, 8], [4, 12] and [7, 8, 10]
01 / 07
Put the first number of each list in a min heap: 1, 4 and 7. The top, 1, is the smallest and the biggest is 7. So 1 to 7 covers every list. Width 6. That is the best so far.
- max
- 7
- best
- [1, 7]
- looking now
- checking
- done
- path
- just added
import java.util.List;
import java.util.PriorityQueue;
class Solution {
public int[] smallestRange(List<List<Integer>> nums) {
// {value, which list, position in that list}
PriorityQueue<int[]> heap = new PriorityQueue<>((x, y) -> Integer.compare(x[0], y[0]));
int max = Integer.MIN_VALUE;
for (int i = 0; i < nums.size(); i++) {
int v = nums.get(i).get(0);
heap.offer(new int[] { v, i, 0 });
max = Math.max(max, v);
}
int bestLo = 0, bestHi = 0;
long bestWidth = Long.MAX_VALUE;
while (true) {
int[] top = heap.poll(); // the smallest of the group
long width = (long) max - top[0];
if (width < bestWidth) { // equal width: keep the lower start
bestWidth = width; bestLo = top[0]; bestHi = max;
}
int list = top[1], next = top[2] + 1;
if (next == nums.get(list).size()) break; // this list ran out
int v = nums.get(list).get(next);
heap.offer(new int[] { v, list, next });
max = Math.max(max, v);
}
return new int[] { bestLo, bestHi };
}
}How long it takes: every number enters and leaves the heap at most once, and the heap holds k entries, so O(N log k). Extra room: the heap, O(k).
Watch out for
- Keep
maxas a separate variable. A min heap cannot tell you its biggest item quickly. - Use
longfor widths.max - mincan overflow when numbers are near theintlimits. - On a tie in width, keep the range with the lower start. Since the smallest only goes up, updating only on strictly smaller width does that.
- Read numbers with
get(i)onList<Integer>. Comparing twoIntegerobjects with==compares boxes, not values.
What interviewers ask next
- Another way without a heap? Merge all numbers into one list tagged with their list number, sort it, then slide a window that keeps a count per list. Shrink from the left while all
klists are still covered. That isO(N log N). - Smallest range covering all elements of a pattern in one array? That is the minimum window substring idea, with a sliding window and counts.
- Why is moving the smallest always safe? The range must include something from the smallest's list. Any range that keeps the current smallest and changes something else is at least as wide, so we lose nothing by moving on.
- Same problem but the lists are huge files? Stream each file in order. The heap only ever needs one number per file in memory.
Remember this
Grown-up words: the classes are sorted lists, the group is a min heap of size k (a k-way merge), the tallest pick is the running maximum, and the gap between shortest and tallest is the range width.