5 Best Ways to Find the Smallest Range I in Python

πŸ’‘ Problem Formulation: Given an integer array nums, we need to find the smallest range that includes at least one number from each of the k sorted lists. We aim to determine a range [a,b] (where a <= b) such that the size of the range is the difference b – a, and this size should be as small as possible.

Method 1: Brute Force

The brute force method involves iterating over each element and comparing ranges exhaustively. The function would check all possible ranges and return the smallest possible range that meets the criteria. This is not the most efficient method due to its O(n^2) time complexity, where n is the length of the input array.

β™₯️ Info: Are you AI curious but you still have to create real impactful projects? Join our official AI builder club on Skool (only $5): SHIP! - One Project Per Month

Here’s an example:

def find_smallest_range(nums):
        min_range = float('inf')
        final_range = (-1, -1)

        for i in range(len(nums)):
            for j in range(i, len(nums)):
                min_val = min(nums[i:j+1])
                max_val = max(nums[i:j+1])
                if max_val - min_val < min_range:
                    min_range = max_val - min_val
                    final_range = (min_val, max_val)

        return final_range

    nums = [4, 10, 5, 8]
    print(find_smallest_range(nums))

The output of this code snippet will be:

(4, 5)

This Python function find_smallest_range iterates over each sub-array of nums, calculates the minimum and maximum of that sub-array, and updates the range if a smaller one is found. The smallest range is returned at the end of the execution.

Method 2: Sorting and Two-Pointers

This method first sorts the array, then uses two pointers to find the minimum range. It has a better performance with O(n log n) due to sorting, followed by a linear scan of the array. The two-pointers approach carefully expands and contracts the range to find the optimal smallest range.

Here’s an example:

def find_smallest_range_sorted(nums):
        nums.sort()
        i, j = 0, 0
        min_range = float('inf')
        final_range = (-1, -1)

        while j < len(nums):
            if nums[j] - nums[i] < min_range:
                min_range = nums[j] - nums[i]
                final_range = (nums[i], nums[j])
            j += 1
            while j < len(nums) and nums[j] == nums[j - 1]:
                j += 1
            if j < len(nums) and nums[j] - nums[i] == min_range:
                i += 1

        return final_range

    nums = [4, 10, 5, 8]
    print(find_smallest_range_sorted(nums))

The output of this code snippet will be:

(4, 5)

This function find_smallest_range_sorted sorts the input array, then uses two pointers to track the ends of the current range. When a smaller range is found, it updates the result and moves the pointers appropriately through the sorted array to continue searching for a potentially smaller range.

Method 3: Heap

Utilizing a heap to solve this problem proves to be much more efficient, especially for k sorted lists. We push one element from each list onto the heap, track the minimum and maximum elements, and adjust the range by popping the smallest element and inserting the next element from that list. The time complexity is O(n log k), with n being the total number of elements and k the number of lists.

Here’s an example:

import heapq

    def find_smallest_range_heap(lists):
        min_range = float('inf')
        current_max = max(list[0] for list in lists)
        final_range = (-1, -1)
        heap = [(list[0], i, 0) for i, list in enumerate(lists) if list]
        heapq.heapify(heap)
        
        while heap:
            current_min, list_index, element_index = heapq.heappop(heap)
            if current_max - current_min < min_range:
                min_range = current_max - current_min
                final_range = (current_min, current_max)
            if element_index + 1  current_max:
                    current_max = next_elem
            else:
                break

        return final_range

    lists = [[4, 7, 9, 12, 15], [0, 8, 10, 14, 20], [6, 12, 16, 30]]
    print(find_smallest_range_heap(lists))

The output of this code snippet will be:

(6, 8)

This code snippet uses Python’s heapq module to create a min-heap. It then iterates through the lists, popping the smallest element and pushing the next element from the same list. This maintains the heap property while tracking the smallest range that includes at least one number from each list.

Method 4: Priority Queue with Custom Comparator

Similar to using a heap, a priority queue can also be used with a custom comparator. This allows for more control over the sorting of the elements inside the queue. By providing a custom comparator, we can sort elements based on their values as well as their original array indices. This method is particularly efficient when dealing with a collection of ranges.

Here’s an example:

Bonus One-Liner Method 5: Pythonic Min-Max with List Comprehension

For a Pythonic and compact solution, we can use a combination of min-max functions with list comprehension. This is elegant but may not be the most efficient for all cases, especially with large arrays.

Here’s an example:

def find_smallest_range_pythonic(nums):
        return min((max(nums[i:j+1]) - min(nums[i:j+1]), (min(nums[i:j+1]), max(nums[i:j+1]))) for i in range(len(nums)) for j in range(i, len(nums)))[1]

    nums = [4, 10, 5, 8]
    print(find_smallest_range_pythonic(nums))

The output of this code snippet will be:

(4, 5)

In this one-liner function find_smallest_range_pythonic, list comprehension is used to generate all possible ranges, and the min function is applied to find the smallest range based on the range size. The result is the second element of the tuple which has the minimum range size.

Summary/Discussion

  • Method 1: Brute Force. Straightforward implementation. High time complexity. Not suitable for large datasets.
  • Method 2: Sorting and Two-Pointers. More efficient than brute force. Requires sorted array. Good for small to medium-size input.
  • Method 3: Heap. Efficient for k sorted lists. Performs well on larger datasets. Requires additional space for the heap.
  • Method 4: Priority Queue with Custom Comparator. Offers custom sorting logic. Similar performance to heap. Potentially more complex to implement.
  • Bonus Method 5: Pythonic Min-Max with List Comprehension. Elegant and concise. Less efficient for larger arrays. Best for quick tests or small datasets.