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.
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:
- Elements in different groups cannot exchange positions
- Within a group, any permutation is achievable through swaps
- 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:
-
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 inarr
is a tuple(value, original_index)
. -
Initialize result array: Create
ans = [0] * n
to store the final lexicographically smallest array. -
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 pointerj
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
- Start with pointer
-
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
- Extract the original indices from the group:
-
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
- Use
-
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 EvaluatorExample 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!
- Check if
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)
wherek
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)
- Sorting the indices:
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 alln
elements:O(n)
ans
: result array of sizen
:O(n)
idx
: temporary list storing indices, at mostn
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
and5
can swap (difference = 4)5
and9
can swap (difference = 4)1
and9
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.
Which two pointer techniques do you use to check if a string is a palindrome?
Recommended Readings
Union Find Disjoint Set Union Data Structure Introduction Prerequisite Depth First Search Review problems dfs_intro Once we have a strong grasp of recursion and Depth First Search we can now introduce Disjoint Set Union DSU This data structure is motivated by the following problem Suppose we have sets of elements
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!