Facebook Pixel

910. Smallest Range II

Problem Description

You have an integer array nums and an integer k. Your task is to modify each element in the array by either adding k to it or subtracting k from it. After modifying all elements, the score of the array is calculated as the difference between the maximum and minimum values in the modified array.

Your goal is to find the minimum possible score after applying the modifications to each element.

For example, if you have nums = [1, 3, 6] and k = 3:

  • Element at index 0: can become either 1 + 3 = 4 or 1 - 3 = -2
  • Element at index 1: can become either 3 + 3 = 6 or 3 - 3 = 0
  • Element at index 2: can become either 6 + 3 = 9 or 6 - 3 = 3

You need to choose whether to add or subtract k for each element such that the resulting array has the smallest possible difference between its maximum and minimum values.

The key insight is that to minimize the range, you want to bring the extreme values closer together. This means larger values should generally be decreased by k while smaller values should be increased by k. The solution sorts the array first, then considers different split points where elements before that point are increased by k and elements from that point onward are decreased by k.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

Let's think about what happens when we modify the array. Since we can either add or subtract k from each element, we're essentially given two choices for each number. The challenge is figuring out which operation to apply to minimize the final range.

Consider a sorted array. The original range is nums[-1] - nums[0]. Now, if we want to minimize the range after modifications, we should try to make the smallest values larger and the largest values smaller. This suggests:

  • Smaller elements should have k added to them
  • Larger elements should have k subtracted from them

But where exactly should we switch from adding to subtracting? This is the key question.

Imagine drawing a dividing line somewhere in the sorted array. Everything to the left of this line gets +k, and everything to the right gets -k. The new minimum value would be either:

  • The smallest element plus k (from the left group)
  • The smallest element from the right group minus k

Similarly, the new maximum would be either:

  • The largest element from the left group plus k
  • The largest element minus k (from the right group)

Since we don't know where the optimal dividing line is, we need to try all possible positions. For each position i, we consider:

  • Elements [0, i-1] get +k
  • Elements [i, n-1] get -k

For each split point, the minimum could be min(nums[0] + k, nums[i] - k) and the maximum could be max(nums[i-1] + k, nums[-1] - k). We calculate the range for each split and keep track of the minimum range found.

This greedy approach works because once the array is sorted, the optimal solution must follow this pattern of adding k to a prefix and subtracting k from a suffix.

Learn more about Greedy, Math and Sorting patterns.

Solution Approach

The implementation follows a greedy approach combined with enumeration of all possible split points:

  1. Sort the array: First, we sort nums in ascending order. This allows us to systematically consider which elements should be increased and which should be decreased.

  2. Initialize the answer: We start with the original range as our initial answer: ans = nums[-1] - nums[0]. This represents the case where all elements are modified in the same direction (all +k or all -k), which doesn't change the range.

  3. Enumerate split points: We iterate through each possible split position i from 1 to len(nums) - 1. At each position i:

    • Elements from index 0 to i-1 will have k added
    • Elements from index i to the end will have k subtracted
  4. Calculate new range for each split: For each split point i:

    • The new minimum value is mi = min(nums[0] + k, nums[i] - k)
      • Either the smallest element increased by k
      • Or the first element in the "subtract" group decreased by k
    • The new maximum value is mx = max(nums[i-1] + k, nums[-1] - k)
      • Either the last element in the "add" group increased by k
      • Or the largest element decreased by k
    • The range for this split is mx - mi
  5. Track the minimum: We update ans = min(ans, mx - mi) to keep track of the minimum range found across all split points.

  6. Return the result: After trying all possible splits, we return the minimum range found.

The time complexity is O(n log n) due to sorting, where n is the length of the array. The space complexity is O(1) as we only use a constant amount of extra space.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the algorithm with nums = [1, 3, 6] and k = 3.

Step 1: Sort the array Array is already sorted: [1, 3, 6]

Step 2: Initialize answer ans = nums[2] - nums[0] = 6 - 1 = 5

Step 3: Try each split point

Split at i=1: Elements before index 1 get +k, elements from index 1 onward get -k

  • Elements: [1] get +3 β†’ [4]
  • Elements: [3, 6] get -3 β†’ [0, 3]
  • Modified array values: [4, 0, 3]
  • New minimum: mi = min(nums[0] + k, nums[1] - k) = min(1 + 3, 3 - 3) = min(4, 0) = 0
  • New maximum: mx = max(nums[0] + k, nums[2] - k) = max(1 + 3, 6 - 3) = max(4, 3) = 4
  • Range: mx - mi = 4 - 0 = 4
  • Update: ans = min(5, 4) = 4

Split at i=2: Elements before index 2 get +k, elements from index 2 onward get -k

  • Elements: [1, 3] get +3 β†’ [4, 6]
  • Elements: [6] get -3 β†’ [3]
  • Modified array values: [4, 6, 3]
  • New minimum: mi = min(nums[0] + k, nums[2] - k) = min(1 + 3, 6 - 3) = min(4, 3) = 3
  • New maximum: mx = max(nums[1] + k, nums[2] - k) = max(3 + 3, 6 - 3) = max(6, 3) = 6
  • Range: mx - mi = 6 - 3 = 3
  • Update: ans = min(4, 3) = 3

Step 4: Return result The minimum possible score is 3.

This occurs when we add k to the first two elements [1, 3] β†’ [4, 6] and subtract k from the last element [6] β†’ [3], giving us the modified array [4, 6, 3] with range 6 - 3 = 3.

Solution Implementation

1class Solution:
2    def smallestRangeII(self, nums: List[int], k: int) -> int:
3        # Sort the array to analyze potential split points
4        nums.sort()
5      
6        # Initial answer: range when all elements move in same direction
7        # (either all +k or all -k gives the same range)
8        min_range = nums[-1] - nums[0]
9      
10        # Try each possible split point
11        # Elements before index i get +k, elements from index i get -k
12        for i in range(1, len(nums)):
13            # After the split:
14            # - Elements [0, i-1] are increased by k
15            # - Elements [i, n-1] are decreased by k
16          
17            # The new minimum is either:
18            # - The smallest element increased by k (nums[0] + k)
19            # - The element at split point decreased by k (nums[i] - k)
20            new_min = min(nums[0] + k, nums[i] - k)
21          
22            # The new maximum is either:
23            # - The element just before split increased by k (nums[i - 1] + k)
24            # - The largest element decreased by k (nums[-1] - k)
25            new_max = max(nums[i - 1] + k, nums[-1] - k)
26          
27            # Update the minimum range if this split gives a better result
28            min_range = min(min_range, new_max - new_min)
29      
30        return min_range
31
1class Solution {
2    public int smallestRangeII(int[] nums, int k) {
3        // Sort the array to analyze the problem systematically
4        Arrays.sort(nums);
5      
6        int arrayLength = nums.length;
7      
8        // Initial range without any modifications (max - min of sorted array)
9        int minRange = nums[arrayLength - 1] - nums[0];
10      
11        // Try different partition points where we add k to elements before index i
12        // and subtract k from elements starting at index i
13        for (int partitionIndex = 1; partitionIndex < arrayLength; partitionIndex++) {
14            // Calculate the minimum value after modifications
15            // Either the first element + k or current element - k
16            int minValue = Math.min(nums[0] + k, nums[partitionIndex] - k);
17          
18            // Calculate the maximum value after modifications
19            // Either the previous element + k or last element - k
20            int maxValue = Math.max(nums[partitionIndex - 1] + k, nums[arrayLength - 1] - k);
21          
22            // Update the minimum range if current configuration gives a smaller range
23            minRange = Math.min(minRange, maxValue - minValue);
24        }
25      
26        return minRange;
27    }
28}
29
1class Solution {
2public:
3    int smallestRangeII(vector<int>& nums, int k) {
4        // Sort the array to work with ordered elements
5        sort(nums.begin(), nums.end());
6      
7        int n = nums.size();
8      
9        // Initial answer: difference between max and min without any operations
10        int answer = nums[n - 1] - nums[0];
11      
12        // Try different partition points
13        // Elements before index i will be increased by k
14        // Elements from index i onwards will be decreased by k
15        for (int i = 1; i < n; ++i) {
16            // Calculate the minimum value after operations
17            // Either the first element + k or current element - k
18            int minValue = min(nums[0] + k, nums[i] - k);
19          
20            // Calculate the maximum value after operations
21            // Either the previous element + k or last element - k
22            int maxValue = max(nums[i - 1] + k, nums[n - 1] - k);
23          
24            // Update answer with the minimum range found
25            answer = min(answer, maxValue - minValue);
26        }
27      
28        return answer;
29    }
30};
31
1/**
2 * Finds the smallest possible range of an array after adding +k or -k to each element
3 * @param nums - Array of integers to process
4 * @param k - The value to add or subtract from each element
5 * @returns The minimum possible difference between max and min values after modification
6 */
7function smallestRangeII(nums: number[], k: number): number {
8    // Sort the array in ascending order to optimize the solution
9    nums.sort((a, b) => a - b);
10  
11    // Initialize answer with the original range (max - min)
12    let minRange: number = nums[nums.length - 1] - nums[0];
13  
14    // Try different split points where we add +k to elements before index i
15    // and subtract k from elements at index i and after
16    for (let i = 1; i < nums.length; i++) {
17        // Calculate the minimum value after modification
18        // Either the first element + k or current element - k
19        const minValue: number = Math.min(nums[0] + k, nums[i] - k);
20      
21        // Calculate the maximum value after modification
22        // Either the last element - k or previous element + k
23        const maxValue: number = Math.max(nums[nums.length - 1] - k, nums[i - 1] + k);
24      
25        // Update the minimum range if we found a smaller one
26        minRange = Math.min(minRange, maxValue - minValue);
27    }
28  
29    return minRange;
30}
31

Time and Space Complexity

Time Complexity: O(n Γ— log n)

The time complexity is dominated by the sorting operation nums.sort(), which takes O(n Γ— log n) time where n is the length of the input array. After sorting, there is a single loop that iterates through the array once with O(1) operations inside the loop, contributing O(n) time. Therefore, the overall time complexity is O(n Γ— log n) + O(n) = O(n Γ— log n).

Space Complexity: O(log n)

The space complexity comes from the sorting algorithm. Python's built-in sort() method uses Timsort, which requires O(log n) space in the worst case for its recursive call stack. The rest of the algorithm uses only a constant amount of extra space for variables like ans, mi, and mx. Therefore, the overall space complexity is O(log n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Forgetting to Handle the No-Split Cases

A common mistake is not considering the cases where all elements should be modified in the same direction (all +k or all -k). While the code initializes with nums[-1] - nums[0], developers sometimes forget this initialization and only focus on the split enumeration, leading to incorrect results.

Why it matters: For some inputs, the optimal solution is to move all elements in the same direction. For example, if the array is already tightly clustered, splitting might actually increase the range.

Solution: Always initialize the answer with the original range before iterating through split points:

min_range = nums[-1] - nums[0]  # Don't forget this initialization!

2. Incorrect Split Point Logic

Developers often confuse which elements should get +k and which should get -k. The intuition that "smaller values should increase and larger values should decrease" can lead to off-by-one errors in the split implementation.

Common mistake:

# Wrong: Using nums[i] + k instead of nums[i] - k
new_min = min(nums[0] + k, nums[i] + k)  # Incorrect!

Solution: Remember that at split point i:

  • Elements [0, i-1] get +k (move up)
  • Elements [i, n-1] get -k (move down)

So the correct calculation is:

new_min = min(nums[0] + k, nums[i] - k)  # First element of "subtract" group
new_max = max(nums[i-1] + k, nums[-1] - k)  # Last element of "add" group

3. Not Sorting the Array First

Some developers attempt to solve this problem without sorting, thinking they can apply the operations directly. This makes it impossible to determine the optimal split point efficiently.

Why sorting is essential: Without sorting, you cannot systematically decide which elements should be increased versus decreased. The sorted order allows you to consider contiguous groups for each operation.

Solution: Always sort first:

nums.sort()  # Critical first step!

4. Edge Case: Single Element Array

While the given code handles this correctly due to the loop range, developers writing their own solution might not handle arrays with only one element properly.

Solution: The loop range(1, len(nums)) naturally handles this by not executing when len(nums) == 1, returning the initialized value of 0 (since nums[-1] - nums[0] = 0 for a single element).

5. Integer Overflow Concerns (Language-Specific)

In languages with fixed integer sizes (like C++ or Java), calculating nums[i] + k or nums[i] - k might cause overflow for large values.

Solution for other languages: Use appropriate data types (e.g., long long in C++) or check for potential overflow conditions. Python handles arbitrary precision integers automatically, so this isn't a concern in the provided code.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More