Problem

You are given a 0-indexed array of positive integers nums and a positive integer limit.

In one operation, you can choose any two indices i and j and swap nums[i] and nums[j] if |nums[i] - nums[j]| <= limit.

Return the lexicographically smallest array that can be obtained by performing the operation any number of times.

An array a is lexicographically smaller than an array b if in the first position where a and b differ, array a has an element that is less than the corresponding element in b. For example, the array [2,10,3] is lexicographically smaller than the array [10,2,3] because they differ at index 0 and 2 < 10.

Examples

Example 1:

Input: nums = [1,5,3,9,8], limit = 2
Output: [1,3,5,8,9]
Explanation: Apply the operation 2 times:
- Swap nums[1] with nums[2]. The array becomes [1,3,5,9,8]
- Swap nums[3] with nums[4]. The array becomes [1,3,5,8,9]
We cannot obtain a lexicographically smaller array by applying any more operations.
Note that it may be possible to get the same result by doing different operations.

Example 2:

Input: nums = [1,7,6,18,2,1], limit = 3
Output: [1,6,7,18,1,2]
Explanation: Apply the operation 3 times:
- Swap nums[1] with nums[2]. The array becomes [1,6,7,18,2,1]
- Swap nums[0] with nums[4]. The array becomes [2,6,7,18,1,1]
- Swap nums[0] with nums[5]. The array becomes [1,6,7,18,1,2]
We cannot obtain a lexicographically smaller array by applying any more operations.

Example 3:

Input: nums = [1,7,28,19,10], limit = 3
Output: [1,7,28,19,10]
Explanation: [1,7,28,19,10] is the lexicographically smallest array we can obtain because we cannot apply the operation on any two indices.

Constraints:

  • 1 <= nums.length <= 10^5
  • 1 <= nums[i] <= 10^9
  • 1 <= limit <= 10^9

Solution

Method 1 - Using Union Find Data Structure

To solve this problem, we need to ensure that for any two indices i and j, the elements at those indices can be swapped if their absolute difference is <= limit. This leads us to the realization that certain elements can create “groups” of swappable indices. Within these groups, the elements can be freely permuted to form the lexicographically smallest result.

To achieve the lexicographically smallest array with the given constraints, we can utilise a union-find (disjoint set union) data structure. The core idea is to identify groups of elements that can be swapped with each other based on the specified limit. Once these groups are identified, we can sort each group individually and place the smallest possible values back into their original positions.

Approach

  1. Sort Indices: First, create an array of indices sorted by the corresponding values in nums. This enables us to process elements in ascending order of their values.
  2. Union-Find: As we traverse through the sorted indices, union indices that are within the specified limit of each other. This ensures that we only need to check adjacent elements in the sorted list.
  3. Group and Sort: After constructing the union-find structure, group indices by their root parent and sort the elements within each group.
  4. Reconstruct the Result: Finally, place the sorted values back into their original indices to form the result array.

Code

Java
class Solution {
    public int[] lexicographicallySmallestArray(int[] nums, int limit) {
        int n = nums.length;
        UnionFind uf = new UnionFind(n);
        
        // Create an array of indices sorted by the values in nums
        Integer[] indices = new Integer[n];
        for (int i = 0; i < n; i++) {
            indices[i] = i;
        }
        
        Arrays.sort(indices, (a, b) -> Integer.compare(nums[a], nums[b]));
        
        // Union adjacent indices in the sorted order if they can be swapped
        for (int i = 0; i < n - 1; i++) {
            if (Math.abs(nums[indices[i]] - nums[indices[i + 1]]) <= limit) {
                uf.union(indices[i], indices[i + 1]);
            }
        }
        
        // Create a map to hold the indices of each component
        Map<Integer, List<Integer>> components = new HashMap<>();
        for (int i = 0; i < n; i++) {
            int root = uf.find(i);
            components.putIfAbsent(root, new ArrayList<>());
            components.get(root).add(i);
        }
        
        // Create the result array
        int[] result = new int[n];
        
        // Sort each component and place back in the result
        for (List<Integer> indicesList : components.values()) {
            List<Integer> values = new ArrayList<>();
            for (int index : indicesList) {
                values.add(nums[index]);
            }
            Collections.sort(values);
            Collections.sort(indicesList);
            for (int i = 0; i < indicesList.size(); i++) {
                result[indicesList.get(i)] = values.get(i);
            }
        }
        
        return result;
    }
    
    // Union-Find class
    class UnionFind {
        private int[] parent;
        
        public UnionFind(int size) {
            parent = new int[size];
            for (int i = 0; i < size; i++) {
                parent[i] = i;
            }
        }
        
        public int find(int x) {
            if (parent[x] != x) {
                parent[x] = find(parent[x]); // Path compression
            }
            return parent[x];
        }
        
        public void union(int x, int y) {
            int rootX = find(x);
            int rootY = find(y);
            if (rootX != rootY) {
                parent[rootY] = rootX; // Union
            }
        }
    }
}
Python
class Solution:
    def findLexSmallestArray(self, nums: List[int], limit: int) -> List[int]:
        n = len(nums)
        parent = list(range(n))
        
        def find(x: int) -> int:
            if parent[x] != x:
                parent[x] = find(parent[x])
            return parent[x]
        
        def union(x: int, y: int) -> None:
            root_x, root_y = find(x), find(y)
            if root_x != root_y:
                parent[root_y] = root_x
        
        for i in range(n):
            for j in range(i + 1, n):
                if abs(nums[i] - nums[j]) <= limit:
                    union(i, j)
        
        components = defaultdict(list)
        for i in range(n):
            root = find(i)
            components[root].append(i)
        
        res = [0] * n
        for indices in components.values():
            values = sorted(nums[i] for i in indices)
            for idx, val in zip(sorted(indices), values):
                res[idx] = val
        
        return res

Complexity

  • ⏰ Time complexity: O(n log n)
    • The complexity for the union-find operations (with path compression and union by rank) is O(n * α(n)), where α(n) is the inverse Ackermann function, which is nearly constant.
    • Sorting the elements will have a time complexity of O(n log n).
  • 🧺 Space complexity: O(n) due to the storage required for the union-find structure and temporary arrays for sorting.