{x}
blog image

Smallest Range Covering Elements from K Lists

You have k lists of sorted integers in non-decreasing order. Find the smallest range that includes at least one number from each of the k lists.

We define the range [a, b] is smaller than range [c, d] if b - a < d - c or a < c if b - a == d - c.

 

Example 1:

Input: nums = [[4,10,15,24,26],[0,9,12,20],[5,18,22,30]]
Output: [20,24]
Explanation: 
List 1: [4, 10, 15, 24,26], 24 is in range [20,24].
List 2: [0, 9, 12, 20], 20 is in range [20,24].
List 3: [5, 18, 22, 30], 22 is in range [20,24].

Example 2:

Input: nums = [[1,2,3],[1,2,3],[1,2,3]]
Output: [1,1]

 

Constraints:

  • nums.length == k
  • 1 <= k <= 3500
  • 1 <= nums[i].length <= 50
  • -105 <= nums[i][j] <= 105
  • nums[i] is sorted in non-decreasing order.

Solution Explanation for Smallest Range Covering Elements from K Lists

This problem asks to find the smallest range that includes at least one number from each of the k sorted lists. The solution can be efficiently tackled using two primary approaches: Sorting + Sliding Window and Priority Queue (Heap). Both approaches are explained below, including time and space complexity analysis.

Approach 1: Sorting + Sliding Window

This approach leverages the sorted nature of the input lists.

  1. Data Transformation: We first flatten the k lists into a single sorted array of tuples. Each tuple contains the number and its original list index. This sorting is crucial because it allows us to efficiently slide a window across the combined sorted data.

  2. Sliding Window: A sliding window approach is employed. The window maintains a count of elements from each list. The window expands until it includes at least one element from every list (all lists are represented in the window's count). The algorithm then shrinks the window from the left while maintaining the condition that all lists are represented. In this shrinking process, the smallest range is tracked.

Time Complexity: O(N log N), where N is the total number of elements across all k lists. This is dominated by the initial sorting step.

Space Complexity: O(N) in the worst case to store the sorted array of tuples and the hashmap for tracking elements in the window. This can be improved to O(k) if we only need to track the smallest element from each list.

Approach 2: Priority Queue (Heap)

This approach uses a min-heap to efficiently manage the smallest element from each list.

  1. Heap Initialization: A min-heap is initialized. Each element in the heap is a tuple containing (value, list_index, element_index). Initially, the heap contains the smallest element from each of the k lists. The maximum value among these initial elements is also tracked.

  2. Iterative Refinement: The algorithm repeatedly extracts the minimum element from the heap. The range between the minimum and the currently tracked maximum is updated if a smaller range is found. The next element from the list corresponding to the extracted minimum is then added to the heap, and the maximum value is updated accordingly. This process continues until the heap becomes empty.

Time Complexity: O(N log k), where N is the total number of elements and k is the number of lists. The log k factor comes from the heap operations (enqueue and dequeue). This is generally more efficient than sorting for large numbers of lists (k) with a relatively small number of elements per list.

Space Complexity: O(k) to store the heap. This approach avoids storing the entire sorted array of Approach 1, making it more memory-efficient for a large number of elements.

Code Examples (Python - Approach 1 & Approach 2)

Approach 1 (Sorting + Sliding Window):

import heapq
 
class Solution:
    def smallestRange(self, nums: List[List[int]]) -> List[int]:
        points = []
        for i, lst in enumerate(nums):
            for x in lst:
                points.append((x, i))
        points.sort()
        
        cnt = collections.Counter()
        l, r = 0, 0
        ans = [-float('inf'), float('inf')]
        while r < len(points):
            cnt[points[r][1]] += 1
            while len(cnt) == len(nums):
                if points[r][0] - points[l][0] < ans[1] - ans[0]:
                    ans = [points[l][0], points[r][0]]
                cnt[points[l][1]] -= 1
                if cnt[points[l][1]] == 0:
                    del cnt[points[l][1]]
                l += 1
            r += 1
        return ans

Approach 2 (Priority Queue):

import heapq
 
class Solution:
    def smallestRange(self, nums: List[List[int]]) -> List[int]:
        heap = [(nums[i][0], i, 0) for i in range(len(nums))]
        heapq.heapify(heap)
        max_val = max(x[0] for x in heap)
        ans = [-float('inf'), float('inf')]
        while True:
            min_val, i, j = heapq.heappop(heap)
            if max_val - min_val < ans[1] - ans[0]:
                ans = [min_val, max_val]
            if j + 1 == len(nums[i]):
                break
            next_val = nums[i][j + 1]
            heapq.heappush(heap, (next_val, i, j + 1))
            max_val = max(max_val, next_val)
        return ans

Remember to install the heapq and collections modules if you are using Python. The other code examples in different programming languages would follow similar logic and structure. Choose the approach that best suits the constraints and performance requirements of your specific use case. The heap-based approach is generally preferred for larger numbers of lists with a relatively small number of elements per list because of its better time complexity in such scenarios.