Sort the Jumbled Numbers
MedPrompt
You are given a 0-indexed integer array mapping which represents the mapping rule of a shuffled decimal system. mapping[i] = j means digit i should be mapped to digit j in this system.
The mapped value of an integer is the new integer obtained by replacing each occurrence of digit i in the integer with mapping[i] for all 0 <= i <= 9.
You are also given another integer array nums. Return the array nums sorted in non-decreasing order based on the mapped values of its elements.
Notes:
- Elements with the same mapped values should appear in the same relative order as in the input.
- The elements of
numsshould only be sorted based on their mapped values and not be replaced by them.
Example 1:
Input: mapping = [8,9,4,0,2,1,3,5,7,6], nums = [991,338,38]
Output: [338,38,991]
Explanation:
Map the number 991 as follows:
1. mapping[9] = 6, so all occurrences of the digit 9 will become 6.
2. mapping[1] = 9, so all occurrences of the digit 1 will become 9.
Therefore, the mapped value of 991 is 669.
338 maps to 007, or 7 after removing the leading zeros.
38 maps to 07, which is also 7 after removing leading zeros.
Since 338 and 38 share the same mapped value, they should remain in the same relative order, so 338 comes before 38.
Thus, the sorted array is [338,38,991].Example 2:
Input: mapping = [0,1,2,3,4,5,6,7,8,9], nums = [789,456,123]
Output: [123,456,789]
Explanation: 789 maps to 789, 456 maps to 456, and 123 maps to 123. Thus, the sorted array is [123,456,789].
Constraints:
mapping.length == 100 <= mapping[i] <= 9- All the values of
mapping[i]are unique. 1 <= nums.length <= 3 * 1040 <= nums[i] < 109
Approaches
3 approaches with complexity analysis and trade-offs.
This approach involves using a standard sorting algorithm with a custom comparator. The comparator calculates the mapped value for two numbers on the fly during each comparison. Because comparison-based sorting algorithms like Timsort (used by Collections.sort) compare elements multiple times, the mapped value for a single number will be computed repeatedly, leading to poor performance.
Algorithm
- Convert the input
int[] numsto aList<Integer>to leverage Java's stableCollections.sort. - Define a helper function
getMappedValue(num, mapping)that:- Converts
numto its string representation. - Iterates through each character (digit) of the string.
- Replaces the digit with its mapped value from the
mappingarray. - Builds a new string with the mapped digits.
- Parses the new string back to a number. It's safer to parse to a
longto avoid potential overflow, though anintmight suffice given the constraints.
- Converts
- Sort the list using
Collections.sortwith a custom comparator. - The comparator takes two numbers,
aandb, callsgetMappedValuefor both, and compares the results. - Copy the elements from the sorted list back to an
int[]array. - Return the result array.
Walkthrough
The core of this method is a custom comparison logic that we provide to a sorting function. First, we need a helper function, let's call it getMappedValue, which takes an integer, converts it to a string, replaces each digit character according to the mapping array, and then parses the resulting string back to an integer. For example, if num = 991 and mapping[9]=6, mapping[1]=9, getMappedValue(991) would convert 991 to "991", map it to "669", and return the integer 669.
Since standard sorts on primitive arrays (like int[]) in Java are not guaranteed to be stable, we first convert the nums array into a List<Integer>. We then use Collections.sort() on this list, providing a lambda expression as the comparator. This comparator (a, b) -> ... will call getMappedValue(a) and getMappedValue(b) and compare the results. Collections.sort() is a stable sort, which satisfies the problem's requirement for elements with equal mapped values.
Finally, the sorted list is converted back into an int[] array. The main drawback is the performance hit from re-calculating mapped values. A number might be part of O(log N) comparisons, leading to O(log N) calculations of its mapped value.
class Solution { public int[] sortJumbled(int[] mapping, int[] nums) { List<Integer> numsList = new ArrayList<>(); for (int num : nums) { numsList.add(num); } // Sort using a custom comparator that calculates mapped values on the fly Collections.sort(numsList, (a, b) -> { long mappedA = getMappedValue(a, mapping); long mappedB = getMappedValue(b, mapping); return Long.compare(mappedA, mappedB); }); int[] result = new int[nums.length]; for (int i = 0; i < nums.length; i++) { result[i] = numsList.get(i); } return result; } private long getMappedValue(int n, int[] mapping) { char[] s = Integer.toString(n).toCharArray(); for (int i = 0; i < s.length; i++) { s[i] = (char) (mapping[s[i] - '0'] + '0'); } return Long.parseLong(new String(s)); }}Complexity
Time
O(N * log(N) * D), where `N` is the number of elements in `nums` and `D` is the maximum number of digits in a number from `nums`. The sort takes `O(N * log(N))` comparisons. Each comparison involves two calls to `getMappedValue`, which takes `O(D)` time.
Space
O(N + D). `O(N)` is required to store the list of numbers. `O(D)` is used for the string builder within the `getMappedValue` function. The sort itself (Timsort) can take up to `O(N)` space in the worst case.
Trade-offs
Pros
Relatively simple to understand and implement using standard library functions.
Code is concise.
Cons
Inefficient due to redundant computations of mapped values. The mapped value for a single number is recalculated every time it's involved in a comparison during the sort.
This approach improves upon the first one by avoiding redundant calculations. We first compute the mapped value for every number in nums just once and store it. Then, we sort the original numbers based on these pre-computed mapped values using a standard comparison-based sort.
Algorithm
- Create a 2D array
pairedNumsof sizeN x 2to store[mapped_value, original_number]. - Iterate through the input
numsarray fromi = 0toN-1. - For each
nums[i], calculate its mapped value using a helper function. - Store the mapped value and the original number as a pair:
pairedNums[i] = {mappedValue, nums[i]}. - Sort the
pairedNumsarray based on the first column (the mapped values) using a stable comparison sort likeArrays.sortwith a lambda comparator. - Initialize a new result array.
- Iterate through the sorted
pairedNumsarray and populate the result array with the second column (the original numbers). - Return the result array.
Walkthrough
The key idea is to decouple the calculation of mapped values from the sorting comparison logic. We create a data structure to hold pairs of information: the original number and its corresponding mapped value. A 2D array int[][] pairedNums of size N x 2 is a good choice, where N is the length of nums.
We iterate through the nums array once. For each number nums[i], we calculate its mapped value and store the pair {mappedValue, originalNumber} in our pairedNums array. For example, pairedNums[i] = {getMappedValue(nums[i]), nums[i]}.
After populating the pairedNums array, we sort it based on the first element of each pair (the mapped value). Arrays.sort with a custom comparator (a, b) -> Integer.compare(a[0], b[0]) works perfectly. Since Arrays.sort for objects (and 2D arrays are arrays of objects) is stable in Java, if two numbers have the same mapped value, their original relative order is preserved because we populated pairedNums in the original order.
Finally, we create the result array by extracting the second element (the original number) from each pair in the now-sorted pairedNums array.
class Solution { public int[] sortJumbled(int[] mapping, int[] nums) { int n = nums.length; int[][] pairedNums = new int[n][2]; for (int i = 0; i < n; i++) { int originalNum = nums[i]; int mappedNum = getMappedValue(originalNum, mapping); pairedNums[i][0] = mappedNum; pairedNums[i][1] = originalNum; } // Sort the pairs based on the mapped value Arrays.sort(pairedNums, (a, b) -> Integer.compare(a[0], b[0])); int[] result = new int[n]; for (int i = 0; i < n; i++) { result[i] = pairedNums[i][1]; } return result; } private int getMappedValue(int n, int[] mapping) { char[] s = Integer.toString(n).toCharArray(); for (int i = 0; i < s.length; i++) { s[i] = (char) (mapping[s[i] - '0'] + '0'); } return Integer.parseInt(new String(s)); }}Complexity
Time
O(N * log(N) + N * D). `N` is the number of elements in `nums`, and `D` is the max number of digits. The first term is for sorting, and the second is for computing all mapped values. Since `D` is a small constant (<= 10), the complexity is effectively `O(N * log(N))`.
Space
O(N). We need `O(N)` space for the `pairedNums` array. The sorting algorithm might also use `O(log N)` or `O(N)` auxiliary space.
Trade-offs
Pros
Much more efficient than the first approach as it avoids recomputing mapped values.
Still relatively easy to implement using built-in sort functions.
Cons
Requires extra space (
O(N)) to store the pairs of mapped and original values.Not as asymptotically fast as a linear-time sorting algorithm like Radix Sort.
This is the most optimal approach in terms of time complexity. After computing the mapped values and pairing them with their original numbers, we use Radix Sort instead of a comparison-based sort. Radix Sort is a non-comparison integer sorting algorithm that can sort numbers in linear time. Since Radix Sort is inherently stable, it naturally handles the requirement to preserve the relative order for elements with the same mapped value.
Algorithm
- Create a data structure (e.g., a 2D array or a list of custom objects) to store pairs of
(mapped_value, original_number). - Iterate through
nums, calculate the mapped value for each number, and populate the list of pairs. - Implement Radix Sort to sort this list of pairs based on the
mapped_value.- Find the maximum mapped value to determine the number of passes.
- Loop from the least significant digit to the most significant digit (
exp = 1, 10, 100, ...). - In each loop, use a stable sorting algorithm like Counting Sort to sort the pairs based on the current digit of their
mapped_value.
- After the Radix Sort is complete, the list of pairs is sorted.
- Create a result array and populate it with the
original_numberfrom the sorted pairs. - Return the result array.
Walkthrough
This approach also starts by pre-computing mapped values, similar to the second approach. We create pairs of (mapped_value, original_number). The key difference is the sorting algorithm used.
Instead of Arrays.sort, which has a time complexity of O(N log N), we implement Radix Sort. Radix sort works by sorting numbers digit by digit, from the least significant digit to the most significant. To ensure the overall sort is correct, the sorting algorithm used for each digit must be stable. Counting Sort is a perfect candidate for this stable, linear-time subroutine.
We find the maximum mapped value to determine how many digits (passes) we need. For each digit place (1s, 10s, 100s, etc.), we perform a Counting Sort on our array of pairs. The "key" for the counting sort is the digit at the current place for the mapped_value of each pair.
After all passes are complete, the array of pairs will be sorted according to the mapped_value. We then extract the original_number from each pair to construct the final result.
class Solution { public int[] sortJumbled(int[] mapping, int[] nums) { int n = nums.length; int[][] pairs = new int[n][2]; // [mapped_value, original_number] for (int i = 0; i < n; i++) { pairs[i][0] = getMappedValue(nums[i], mapping); pairs[i][1] = nums[i]; } radixSort(pairs); int[] result = new int[n]; for (int i = 0; i < n; i++) { result[i] = pairs[i][1]; } return result; } private int getMappedValue(int n, int[] mapping) { char[] s = Integer.toString(n).toCharArray(); for (int i = 0; i < s.length; i++) { s[i] = (char) (mapping[s[i] - '0'] + '0'); } return Integer.parseInt(new String(s)); } private void radixSort(int[][] pairs) { int maxVal = 0; for (int[] pair : pairs) { maxVal = Math.max(maxVal, pair[0]); } long exp = 1; while (maxVal / exp > 0) { countingSort(pairs, exp); exp *= 10; } } private void countingSort(int[][] pairs, long exp) { int n = pairs.length; int[][] output = new int[n][2]; int[] count = new int[10]; // For digits 0-9 for (int i = 0; i < n; i++) { int digit = (int) ((pairs[i][0] / exp) % 10); count[digit]++; } for (int i = 1; i < 10; i++) { count[i] += count[i - 1]; } for (int i = n - 1; i >= 0; i--) { int digit = (int) ((pairs[i][0] / exp) % 10); int index = count[digit] - 1; output[index][0] = pairs[i][0]; output[index][1] = pairs[i][1]; count[digit]--; } System.arraycopy(output, 0, pairs, 0, n); }}Complexity
Time
O(N * D + D_max * (N + B)). `N` is the number of elements, `D` is the average number of digits in `nums`, `D_max` is the number of digits in the maximum mapped value (a constant, at most 10), and `B` is the base (10). Since `D`, `D_max`, and `B` are small constants, the overall complexity simplifies to `O(N)`.
Space
O(N + B). `O(N)` is needed to store the pairs. The Counting Sort subroutine requires `O(N + B)` space for the output array and the count array, where `B` is the base (10). This simplifies to `O(N)`.
Trade-offs
Pros
Asymptotically the fastest approach with linear time complexity.
Highly efficient for large inputs.
Cons
More complex to implement correctly compared to using a built-in sorting function.
The constant factors might make it slower than a highly optimized library
sortfor smaller input sizes, despite better asymptotic complexity.
Solutions
Solution
public class Solution { public int[] SortJumbled(int[] mapping, int[] nums) { Func < int, int > f = (int x) => { if (x == 0) { return mapping[0]; } int y = 0; int k = 1; int num = x; while (num != 0) { int v = mapping[num % 10]; y = k * v + y; k *= 10; num /= 10; } return y; }; int n = nums.Length; List < (int, int) > arr = new List < (int, int) > (); for (int i = 0; i < n; ++i) { arr.Add((f(nums[i]), i)); } arr.Sort(); int[] ans = new int[n]; for (int i = 0; i < n; ++i) { ans[i] = nums[arr[i].Item2]; } return ans; }}class Solution {public int[] sortJumbled(int[] mapping, int[] nums) { int n = nums.length; int[][] arr = new int[n][2]; for (int i = 0; i < n; ++i) { int x = nums[i]; int y = x == 0 ? mapping[0] : 0; int k = 1; for (; x > 0; x /= 10) { y += k * mapping[x % 10]; k *= 10; } arr[i] = new int[]{y, i}; } Arrays.sort(arr, (a, b)->a[0] == b[0] ? a[1] - b[1] : a[0] - b[0]); int[] ans = new int[n]; for (int i = 0; i < n; ++i) { ans[i] = nums[arr[i][1]]; } return ans; }}/** * @param {number[]} mapping * @param {number[]} nums * @return {number[]} */ var sortJumbled = function (mapping, nums) { const n = nums.length; const f = (x) => { if (x === 0) { return mapping[0]; } let y = 0; for (let k = 1; x; x = (x / 10) | 0) { const v = mapping[x % 10]; y += v * k; k *= 10; } return y; }; const arr = nums.map((x, i) => [f(x), i]); arr.sort((a, b) => (a[0] === b[0] ? a[1] - b[1] : a[0] - b[0])); return arr.map((x) => nums[x[1]]); };class Solution {public: vector<int> sortJumbled(vector<int> &mapping, vector<int> &nums) { int n = nums.size(); vector<pair<int, int>> arr(n); for (int i = 0; i < n; ++i) { int x = nums[i]; int y = x == 0 ? mapping[0] : 0; int k = 1; for (; x; x /= 10) { y += k * mapping[x % 10]; k *= 10; } arr[i] = {y, i}; } sort(arr.begin(), arr.end()); vector<int> ans; for (auto &[_, i] : arr) { ans.push_back(nums[i]); } return ans; }};class Solution: def sortJumbled(self, mapping: List[int], nums: List[int]) -> List[int]: arr = [] for i, x in enumerate(nums): y = mapping[0] if x == 0 else 0 k = 1 while x: x, v = divmod(x, 10) y = mapping[v] * k + y k *= 10 arr . append((y, i)) arr . sort() return [nums[i] for _, i in arr]Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.