Facebook Pixel

2948. Make Lexicographically Smallest Array by Swapping Elements

Problem Description

You have an array of positive integers nums (0-indexed) and a positive integer limit.

You can perform swap operations on this array. In each operation, you can select any two indices i and j and swap the elements nums[i] and nums[j], but only if the absolute difference between these two elements is at most limit. That is, you can only swap if |nums[i] - nums[j]| <= limit.

Your goal is to find the lexicographically smallest array possible after performing any number of these swap operations (including zero operations).

What is lexicographically smaller? An array a is lexicographically smaller than array b if, at the first position where they differ, array a has a smaller element than array b. For example, [2,10,3] is lexicographically smaller than [10,2,3] because at index 0, we have 2 < 10.

Key insight: Elements can only be swapped if their difference is within the limit. This means that elements form groups where any element in a group can eventually be moved to any position occupied by another element in the same group through a series of swaps. Elements that differ by more than limit cannot be directly swapped and may not be able to reach each other's positions.

The solution identifies these swappable groups by sorting the elements and grouping consecutive elements that differ by at most limit. Within each group, the smallest available elements are placed in the earliest available positions to achieve the lexicographically smallest result.

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

Intuition

The key observation is that if we can swap elements when their difference is at most limit, then elements form "connected components" or groups. Think of it this way: if element A can swap with B (difference ≤ limit), and B can swap with C (difference ≤ limit), then A can eventually reach C's position through intermediate swaps, even if A and C differ by more than limit.

To find these groups, we need to identify which elements can reach each other through a chain of valid swaps. The clever insight is that if we sort the array first, elements that belong to the same swappable group will appear consecutively in the sorted order. Why? Because if there's a gap larger than limit between consecutive elements in sorted order, those elements cannot possibly be in the same group - there's no "bridge" to connect them.

For example, if we have nums = [10, 3, 30, 4] with limit = 1, after sorting we get [(3,1), (4,3), (10,0), (30,2)] (value, original index). We can see that 3 and 4 can swap (difference = 1), but there's a gap between 4 and 10 (difference = 6 > limit), so {3, 4} forms one group and {10, 30} forms separate groups.

Once we identify these groups, the strategy becomes clear: to get the lexicographically smallest array, we should place the smallest elements from each group in the earliest positions that belong to that group. Within each group, we sort the original indices to know which positions need to be filled, then assign the sorted values to these positions in order.

This greedy approach works because:

  1. Elements in different groups cannot exchange positions
  2. Within a group, any permutation is achievable through swaps
  3. To minimize lexicographically, we want the smallest available element at each position, respecting group constraints

Learn more about Union Find and Sorting patterns.

Solution Approach

The implementation follows these steps:

  1. Create sorted pairs with original indices: We zip the array values with their original indices and sort by values. This gives us arr = sorted(zip(nums, range(n))). Each element in arr is a tuple (value, original_index).

  2. Initialize result array: Create ans = [0] * n to store the final lexicographically smallest array.

  3. Identify and process groups: Use a two-pointer approach to find consecutive elements that can be swapped:

    • Start with pointer i = 0
    • For each position i, extend pointer j to find all elements that form a connected group
    • Elements belong to the same group if consecutive sorted elements differ by at most limit: arr[j][0] - arr[j-1][0] <= limit
  4. Extract and sort indices within each group:

    • Extract the original indices from the group: idx = sorted(k for _, k in arr[i:j])
    • This tells us which positions in the original array need to be filled by this group
  5. Assign sorted values to sorted positions:

    • Use zip(idx, arr[i:j]) to pair each sorted position with each sorted value
    • Place the smallest value at the earliest position: ans[k] = x
    • This ensures lexicographically smallest arrangement within the group
  6. Move to next group: Set i = j and repeat until all elements are processed.

Example walkthrough: For nums = [10, 3, 30, 4] with limit = 1:

  • After sorting: arr = [(3,1), (4,3), (10,0), (30,2)]
  • First group [3,4] at indices [1,3] → sorted indices [1,3]
  • Assign: ans[1] = 3, ans[3] = 4
  • Second group [10] at index [0]ans[0] = 10
  • Third group [30] at index [2]ans[2] = 30
  • Result: [10, 3, 30, 4]

The time complexity is O(n log n) due to sorting, and space complexity is O(n) for storing the paired array and result.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's walk through a detailed example with nums = [7, 9, 3, 5] and limit = 2.

Step 1: Create sorted pairs with original indices

  • Original array: [7, 9, 3, 5] at indices [0, 1, 2, 3]
  • After sorting by values: arr = [(3,2), (5,3), (7,0), (9,1)]
    • This means value 3 was originally at index 2, value 5 at index 3, etc.

Step 2: Identify swappable groups Starting from the first element in sorted array:

  • Group 1: Start with (3,2)
    • Check if (5,3) can join: 5 - 3 = 2 ≤ limit
    • Check if (7,0) can join: 7 - 5 = 2 ≤ limit
    • Check if (9,1) can join: 9 - 7 = 2 ≤ limit
    • All elements form one connected group!

Step 3: Process the group

  • Values in group: [3, 5, 7, 9] (already sorted)
  • Original indices of these values: [2, 3, 0, 1]
  • Sort the indices: [0, 1, 2, 3]

Step 4: Assign values to positions We want the lexicographically smallest result, so assign the smallest values to the earliest positions:

  • Position 0 gets value 3: ans[0] = 3
  • Position 1 gets value 5: ans[1] = 5
  • Position 2 gets value 7: ans[2] = 7
  • Position 3 gets value 9: ans[3] = 9

Final Result: [3, 5, 7, 9]

This makes sense because with limit = 2, all adjacent elements in the sorted array can swap with each other, creating one large connected component. Since all elements can reach any position through a series of swaps, we can achieve the fully sorted array, which is the lexicographically smallest possible arrangement.

Why this works:

  • Original [7, 9, 3, 5] → Can swap 7 and 5 (diff = 2), then 5 and 3 (diff = 2), then 7 and 9 (diff = 2)
  • Through these chained swaps, any element can reach any position
  • The algorithm correctly identifies this and produces the optimal result

Solution Implementation

1class Solution:
2    def lexicographicallySmallestArray(self, nums: List[int], limit: int) -> List[int]:
3        n = len(nums)
4      
5        # Create pairs of (value, original_index) and sort by value
6        sorted_with_indices = sorted(zip(nums, range(n)))
7      
8        # Initialize result array
9        result = [0] * n
10      
11        i = 0
12        while i < n:
13            # Find the end of current group where consecutive differences <= limit
14            j = i + 1
15            while j < n and sorted_with_indices[j][0] - sorted_with_indices[j - 1][0] <= limit:
16                j += 1
17          
18            # Extract and sort the original indices for current group
19            original_indices = sorted(index for _, index in sorted_with_indices[i:j])
20          
21            # Assign smallest available values to smallest available positions
22            # This ensures lexicographically smallest arrangement within the group
23            for position, (value, _) in zip(original_indices, sorted_with_indices[i:j]):
24                result[position] = value
25          
26            # Move to next group
27            i = j
28      
29        return result
30
1class Solution {
2    public int[] lexicographicallySmallestArray(int[] nums, int limit) {
3        int n = nums.length;
4      
5        // Create an array of indices to track original positions
6        Integer[] indices = new Integer[n];
7        Arrays.setAll(indices, i -> i);
8      
9        // Sort indices based on the values in nums array (ascending order)
10        Arrays.sort(indices, (i, j) -> nums[i] - nums[j]);
11      
12        // Initialize result array
13        int[] result = new int[n];
14      
15        // Process groups of elements that can be swapped with each other
16        int i = 0;
17        while (i < n) {
18            // Find the end of current group where consecutive elements differ by at most 'limit'
19            int groupEnd = i + 1;
20            while (groupEnd < n && nums[indices[groupEnd]] - nums[indices[groupEnd - 1]] <= limit) {
21                groupEnd++;
22            }
23          
24            // Extract indices of current group
25            Integer[] groupIndices = Arrays.copyOfRange(indices, i, groupEnd);
26          
27            // Sort group indices by their original position (ascending order)
28            Arrays.sort(groupIndices, (x, y) -> x - y);
29          
30            // Assign the sorted values to their lexicographically smallest positions
31            // Elements in the group get the smallest available values in order
32            for (int k = i; k < groupEnd; k++) {
33                result[groupIndices[k - i]] = nums[indices[k]];
34            }
35          
36            // Move to the next group
37            i = groupEnd;
38        }
39      
40        return result;
41    }
42}
43
1class Solution {
2public:
3    vector<int> lexicographicallySmallestArray(vector<int>& nums, int limit) {
4        int n = nums.size();
5      
6        // Create an array of indices [0, 1, 2, ..., n-1]
7        vector<int> indices(n);
8        iota(indices.begin(), indices.end(), 0);
9      
10        // Sort indices based on the values in nums (ascending order)
11        // This groups elements by their values
12        sort(indices.begin(), indices.end(), [&](int i, int j) {
13            return nums[i] < nums[j];
14        });
15      
16        // Result array to store the final answer
17        vector<int> result(n);
18      
19        // Process groups of elements that can be swapped with each other
20        for (int groupStart = 0; groupStart < n;) {
21            // Find the end of current group
22            // Elements in a group can swap if their difference <= limit
23            int groupEnd = groupStart + 1;
24            while (groupEnd < n && 
25                   nums[indices[groupEnd]] - nums[indices[groupEnd - 1]] <= limit) {
26                ++groupEnd;
27            }
28          
29            // Extract the original positions of elements in this group
30            vector<int> originalPositions(indices.begin() + groupStart, 
31                                         indices.begin() + groupEnd);
32          
33            // Sort the original positions to place smallest values first
34            sort(originalPositions.begin(), originalPositions.end());
35          
36            // Assign the sorted values to their corresponding positions
37            // The smallest value goes to the smallest position index, and so on
38            for (int k = groupStart; k < groupEnd; ++k) {
39                result[originalPositions[k - groupStart]] = nums[indices[k]];
40            }
41          
42            // Move to the next group
43            groupStart = groupEnd;
44        }
45      
46        return result;
47    }
48};
49
1/**
2 * Returns the lexicographically smallest array by swapping elements within a limit constraint.
3 * Elements can be swapped if their absolute difference is at most the given limit.
4 * 
5 * @param nums - The input array of numbers
6 * @param limit - The maximum allowed difference between elements that can be swapped
7 * @returns The lexicographically smallest array after allowed swaps
8 */
9function lexicographicallySmallestArray(nums: number[], limit: number): number[] {
10    const arrayLength: number = nums.length;
11  
12    // Create an array of indices [0, 1, 2, ..., n-1]
13    const indices: number[] = Array.from({ length: arrayLength }, (_, index) => index);
14  
15    // Sort indices based on their corresponding values in nums (ascending order)
16    indices.sort((indexA: number, indexB: number) => nums[indexA] - nums[indexB]);
17  
18    // Initialize result array with zeros
19    const result: number[] = Array(arrayLength).fill(0);
20  
21    // Process groups of elements that can be swapped with each other
22    let currentIndex: number = 0;
23    while (currentIndex < arrayLength) {
24        // Find the end of current group where consecutive sorted elements differ by at most 'limit'
25        let groupEndIndex: number = currentIndex + 1;
26        while (groupEndIndex < arrayLength && 
27               nums[indices[groupEndIndex]] - nums[indices[groupEndIndex - 1]] <= limit) {
28            groupEndIndex++;
29        }
30      
31        // Extract indices of current group and sort them by position (ascending order)
32        const sortedGroupIndices: number[] = indices
33            .slice(currentIndex, groupEndIndex)
34            .sort((a: number, b: number) => a - b);
35      
36        // Assign the sorted values to their optimal positions
37        // Place smallest available values at leftmost available positions
38        for (let k: number = currentIndex; k < groupEndIndex; k++) {
39            result[sortedGroupIndices[k - currentIndex]] = nums[indices[k]];
40        }
41      
42        // Move to the next group
43        currentIndex = groupEndIndex;
44    }
45  
46    return result;
47}
48

Time and Space Complexity

Time Complexity: O(n log n)

The time complexity is dominated by the following operations:

  • Initial sorting of the array with indices: O(n log n)
  • The while loop iterates through each element exactly once: O(n)
  • Inside the loop, for each group of elements within the limit:
    • Sorting the indices: O(k log k) where k is the size of the current group
    • Zip and assignment operations: O(k)
    • Since each element is processed exactly once across all groups, the total cost for all sorting operations inside the loop is O(n log n) in the worst case (when all elements form one group)

Overall time complexity: O(n log n) + O(n log n) = O(n log n)

Space Complexity: O(n)

The space complexity consists of:

  • arr: stores tuples of (value, index) for all n elements: O(n)
  • ans: result array of size n: O(n)
  • idx: temporary list storing indices, at most n elements: O(n)
  • Other variables (i, j, k, x): O(1)

Total space complexity: O(n)

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

Common Pitfalls

Pitfall 1: Misunderstanding Group Connectivity

The Problem: A common mistake is assuming that if element A can swap with B, and B can swap with C, then A can directly swap with C. This leads to incorrectly grouping elements based only on their ability to swap with a "representative" element rather than checking consecutive differences in the sorted array.

Incorrect Approach:

# WRONG: Trying to group based on direct swappability with first element
groups = []
for num in nums:
    placed = False
    for group in groups:
        if any(abs(num - g) <= limit for g in group):
            group.append(num)
            placed = True
            break
    if not placed:
        groups.append([num])

Why It Fails: Consider nums = [1, 5, 9] with limit = 4:

  • 1 and 5 can swap (difference = 4)
  • 5 and 9 can swap (difference = 4)
  • 1 and 9 cannot directly swap (difference = 8)

However, through transitivity, all three elements belong to the same group since 1 can reach position of 9 through intermediate swaps with 5.

Correct Solution: The provided code correctly identifies groups by checking consecutive differences in the sorted array. Elements form a connected component if all consecutive pairs in the sorted order have differences ≤ limit.

Pitfall 2: Incorrectly Assigning Values to Positions

The Problem: After identifying groups, developers might try to greedily place the smallest values at the front positions without considering which positions actually belong to the group.

Incorrect Approach:

# WRONG: Trying to place group values at earliest positions in result
result = [0] * n
position = 0
for group in groups:
    sorted_group = sorted(group)
    for value in sorted_group:
        result[position] = value
        position += 1

Why It Fails: This ignores the original positions that the group elements occupied. For example, if a group's elements originally were at indices [2, 5, 7], we must place the sorted group values back at these specific indices, not at positions [0, 1, 2].

Correct Solution: The code correctly extracts the original indices for each group (original_indices = sorted(index for _, index in sorted_with_indices[i:j])) and assigns the sorted values to these specific positions, ensuring the smallest value goes to the smallest index within the group's allowed positions.

Pitfall 3: Not Maintaining Original Index Information

The Problem: Simply sorting the array values without tracking their original positions makes it impossible to know where to place the rearranged elements in the final result.

Incorrect Approach:

# WRONG: Losing track of original positions
sorted_nums = sorted(nums)
# Now we don't know which positions these values came from!

Correct Solution: The code uses sorted(zip(nums, range(n))) to create pairs of (value, original_index), preserving the connection between each value and its original position throughout the algorithm. This enables proper reconstruction of the result array with values placed at the correct positions.

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

Which two pointer techniques do you use to check if a string is a palindrome?


Recommended Readings

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

Load More