Facebook Pixel

2724. Sort By

Problem Description

You are given an array arr and a function fn. Your task is to return a new sorted array sortedArr where the elements are sorted based on the numeric values returned by applying fn to each element.

The sorting rules are:

  • Apply the function fn to each element in the array to get a numeric value
  • Sort the array elements in ascending order based on these numeric values
  • If fn(a) returns a smaller number than fn(b), then a should come before b in the sorted array

Key constraints:

  • The function fn only returns numbers
  • The function fn will never return duplicate numbers for different elements in the same array (each element maps to a unique number)

For example, if you have an array [5, 4, 1, 2, 3] and a function fn = (x) => x, the sorted array would be [1, 2, 3, 4, 5]. If the function was fn = (x) => -x, the sorted array would be [5, 4, 3, 2, 1] because the negative values would reverse the order.

The solution uses JavaScript's built-in sort() method with a custom comparator function (a, b) => fn(a) - fn(b) to achieve the ascending order sorting based on the function's output values.

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

Intuition

The key insight is that we need to sort elements not by their own values, but by the values that the function fn produces when applied to them. This is a classic use case for custom sorting.

When we think about sorting, we typically compare two elements directly. But here, we need an intermediate step: first transform each element using fn, then compare those transformed values.

JavaScript's sort() method accepts a comparator function that determines the order of elements. The comparator takes two elements a and b and returns:

  • A negative number if a should come before b
  • A positive number if b should come before a
  • Zero if they're equal

Since we want ascending order based on fn's output, we can directly use fn(a) - fn(b) as our comparator:

  • If fn(a) < fn(b), the result is negative, placing a before b
  • If fn(a) > fn(b), the result is positive, placing b before a

This elegantly handles the transformation and comparison in a single expression. The guarantee that fn never produces duplicate values for a given array means we don't need to worry about handling ties or maintaining stable sort order for equal values.

The beauty of this approach is its simplicity - we let the built-in sort method handle all the complexity of the sorting algorithm itself, while we just provide the logic for comparing elements based on their transformed values.

Solution Approach

The implementation uses JavaScript's built-in Array.sort() method with a custom comparator function. Let's walk through how this works:

Step 1: Understanding the Sort Method

The sort() method modifies the array in-place and accepts a comparator function that defines the sorting logic. The comparator receives two elements at a time and determines their relative order.

Step 2: Creating the Comparator Function

The comparator (a, b) => fn(a) - fn(b) works as follows:

  • Takes two elements a and b from the array
  • Applies the transformation function fn to both elements
  • Returns the difference fn(a) - fn(b)

Step 3: How the Sorting Works

When sort() executes with our comparator:

  1. For any two elements being compared, it calls fn(a) and fn(b) to get their numeric values
  2. The subtraction fn(a) - fn(b) produces:
    • A negative value when fn(a) < fn(b)a is placed before b
    • A positive value when fn(a) > fn(b)b is placed before a
    • Zero when fn(a) === fn(b) → order remains unchanged (though this won't happen per problem constraints)

Example Walkthrough:

If arr = [3, 1, 2] and fn = (x) => x * x:

  • Comparing 3 and 1: fn(3) - fn(1) = 9 - 1 = 8 (positive, so 1 comes before 3)
  • Comparing 1 and 2: fn(1) - fn(2) = 1 - 4 = -3 (negative, so 1 comes before 2)
  • Comparing 3 and 2: fn(3) - fn(2) = 9 - 4 = 5 (positive, so 2 comes before 3)
  • Result: [1, 2, 3] (sorted by square values: 1, 4, 9)

The solution is efficient and concise, leveraging the language's built-in sorting capabilities while providing custom comparison logic through the function composition.

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 concrete example to understand how the solution works.

Given:

  • arr = [3, -2, 1, -4]
  • fn = (x) => x * x (squares each number)

Step 1: Understand what fn does to each element

  • fn(3) = 9
  • fn(-2) = 4
  • fn(1) = 1
  • fn(-4) = 16

Step 2: Apply the sort with comparator (a, b) => fn(a) - fn(b)

The sort method will compare pairs of elements. Let's trace through some comparisons:

First comparison: 3 and -2

  • fn(3) - fn(-2) = 9 - 4 = 5 (positive)
  • Since the result is positive, -2 should come before 3

Second comparison: -2 and 1

  • fn(-2) - fn(1) = 4 - 1 = 3 (positive)
  • Since the result is positive, 1 should come before -2

Third comparison: 1 and -4

  • fn(1) - fn(-4) = 1 - 16 = -15 (negative)
  • Since the result is negative, 1 should come before -4

Step 3: Build the final sorted order

Based on the squared values:

  • 1 has the smallest squared value (1)
  • -2 has the next smallest squared value (4)
  • 3 has the third smallest squared value (9)
  • -4 has the largest squared value (16)

Final Result: [1, -2, 3, -4]

This demonstrates how elements are sorted not by their original values, but by the values produced when fn is applied to them. The comparator fn(a) - fn(b) ensures ascending order based on the transformed values.

Solution Implementation

1def sortBy(arr, fn):
2    """
3    Sorts an array of elements based on a numeric value returned by the provided function
4  
5    Args:
6        arr: The array to be sorted
7        fn: A function that takes an element and returns a numeric value for comparison
8  
9    Returns:
10        A new sorted array in ascending order based on the function's return values
11    """
12    # Create a copy of the array to avoid modifying the original
13    # Use the key parameter in sorted() to specify the comparison function
14    # The sorted() function automatically sorts in ascending order based on fn's return values
15    return sorted(arr, key=fn)
16
1import java.util.Arrays;
2import java.util.Comparator;
3import java.util.function.Function;
4
5/**
6 * Sorts an array of elements based on a numeric value returned by the provided function
7 * @param <T> The type of elements in the array
8 * @param arr The array to be sorted
9 * @param fn A function that takes an element and returns a numeric value for comparison
10 * @return A new sorted array in ascending order based on the function's return values
11 */
12public static <T> T[] sortBy(T[] arr, Function<T, Double> fn) {
13    // Create a copy of the array to avoid modifying the original
14    T[] sortedArray = Arrays.copyOf(arr, arr.length);
15  
16    // Sort the copied array using a custom comparator
17    // The comparator compares elements by comparing the numeric values returned by fn
18    // Elements are sorted in ascending order based on these values
19    Arrays.sort(sortedArray, new Comparator<T>() {
20        @Override
21        public int compare(T a, T b) {
22            // Get the numeric values for both elements
23            Double valueA = fn.apply(a);
24            Double valueB = fn.apply(b);
25            // Compare the values (ascending order)
26            return valueA.compareTo(valueB);
27        }
28    });
29  
30    return sortedArray;
31}
32
1#include <vector>
2#include <algorithm>
3#include <functional>
4
5/**
6 * Sorts an array of elements based on a numeric value returned by the provided function
7 * @param arr - The array to be sorted
8 * @param fn - A function that takes an element and returns a numeric value for comparison
9 * @returns A new sorted array in ascending order based on the function's return values
10 */
11template<typename T>
12std::vector<T> sortBy(const std::vector<T>& arr, std::function<double(const T&)> fn) {
13    // Create a copy of the input array to avoid modifying the original
14    std::vector<T> sorted_arr = arr;
15  
16    // Sort the copied array using a custom comparator
17    // The comparator compares elements by evaluating fn on each element
18    // Returns true if fn(a) < fn(b), which places a before b in ascending order
19    std::sort(sorted_arr.begin(), sorted_arr.end(), 
20              [&fn](const T& a, const T& b) {
21                  return fn(a) < fn(b);
22              });
23  
24    // Return the sorted array
25    return sorted_arr;
26}
27
1/**
2 * Sorts an array of elements based on a numeric value returned by the provided function
3 * @param arr - The array to be sorted
4 * @param fn - A function that takes an element and returns a numeric value for comparison
5 * @returns A new sorted array in ascending order based on the function's return values
6 */
7function sortBy<T>(arr: T[], fn: (item: T) => number): T[] {
8    // Create a copy of the array and sort it using the comparison function
9    // The sort compares elements by subtracting fn(b) from fn(a)
10    // Negative result means a comes before b, positive means b comes before a
11    return arr.sort((a: T, b: T) => fn(a) - fn(b));
12}
13

Time and Space Complexity

Time Complexity: O(n log n)

The sortBy function uses JavaScript's built-in sort() method with a custom comparator function. The time complexity of the sort operation depends on the underlying sorting algorithm implementation, which in most JavaScript engines (like V8) is typically Timsort or a variation of quicksort. These algorithms have an average and worst-case time complexity of O(n log n), where n is the number of elements in the array.

Additionally, the comparator function fn is called O(n log n) times during the sorting process. If we assume fn has a time complexity of O(f), the overall time complexity becomes O(n log n × f). When fn is O(1) (constant time), the total complexity remains O(n log n).

Space Complexity: O(n)

The space complexity depends on the sorting algorithm implementation:

  • The sort() method in JavaScript typically uses O(log n) auxiliary space for the recursive call stack in algorithms like quicksort or Timsort
  • However, since sort() may create temporary arrays during the sorting process (especially in Timsort for merging), the worst-case space complexity can be O(n)
  • The function returns the sorted array in-place (modifying the original array), so no additional O(n) space is needed for the output

Therefore, the overall space complexity is O(n) in the worst case, though it could be O(log n) in practice depending on the specific implementation and input characteristics.

Common Pitfalls

1. Modifying the Original Array Instead of Creating a New One

A frequent mistake is using JavaScript's arr.sort() directly, which modifies the original array in-place rather than creating a new sorted array as required by the problem.

Incorrect approach:

function sortBy(arr, fn) {
    return arr.sort((a, b) => fn(a) - fn(b)); // Modifies original array!
}

Correct approach:

function sortBy(arr, fn) {
    return [...arr].sort((a, b) => fn(a) - fn(b)); // Creates a copy first
}
// or
function sortBy(arr, fn) {
    return arr.slice().sort((a, b) => fn(a) - fn(b)); // Also creates a copy
}

2. Caching Function Results for Performance

When fn is computationally expensive, the sort comparator calls it multiple times for the same elements during sorting (approximately O(n log n) times per element). This can lead to performance issues.

Inefficient approach:

function sortBy(arr, fn) {
    return [...arr].sort((a, b) => fn(a) - fn(b)); // fn called multiple times per element
}

Optimized approach using Schwartzian Transform:

function sortBy(arr, fn) {
    return arr
        .map(item => [item, fn(item)])  // Cache fn results
        .sort((a, b) => a[1] - b[1])     // Sort by cached values
        .map(pair => pair[0]);           // Extract original elements
}

3. Handling Non-Numeric Return Values

While the problem states fn only returns numbers, defensive programming suggests validating this assumption to prevent unexpected behavior.

Vulnerable approach:

function sortBy(arr, fn) {
    return [...arr].sort((a, b) => fn(a) - fn(b)); // Assumes fn always returns numbers
}

Defensive approach:

function sortBy(arr, fn) {
    return [...arr].sort((a, b) => {
        const valA = fn(a);
        const valB = fn(b);
        if (typeof valA !== 'number' || typeof valB !== 'number') {
            throw new Error('Function must return numeric values');
        }
        return valA - valB;
    });
}

4. Language-Specific Sort Stability Concerns

Different programming languages handle stable sorting differently. While modern JavaScript (ES2019+) guarantees stable sorting, older versions don't. Python's sorted() is always stable, but this distinction matters when migrating code between languages or dealing with legacy environments.

Cross-language consideration:

  • Python's sorted(arr, key=fn) maintains relative order for equal elements
  • Pre-ES2019 JavaScript might not preserve original order for equal fn values
  • Though the problem states fn returns unique values, this becomes important when requirements change
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What's the output of running the following function using the following tree as input?

1def serialize(root):
2    res = []
3    def dfs(root):
4        if not root:
5            res.append('x')
6            return
7        res.append(root.val)
8        dfs(root.left)
9        dfs(root.right)
10    dfs(root)
11    return ' '.join(res)
12
1import java.util.StringJoiner;
2
3public static String serialize(Node root) {
4    StringJoiner res = new StringJoiner(" ");
5    serializeDFS(root, res);
6    return res.toString();
7}
8
9private static void serializeDFS(Node root, StringJoiner result) {
10    if (root == null) {
11        result.add("x");
12        return;
13    }
14    result.add(Integer.toString(root.val));
15    serializeDFS(root.left, result);
16    serializeDFS(root.right, result);
17}
18
1function serialize(root) {
2    let res = [];
3    serialize_dfs(root, res);
4    return res.join(" ");
5}
6
7function serialize_dfs(root, res) {
8    if (!root) {
9        res.push("x");
10        return;
11    }
12    res.push(root.val);
13    serialize_dfs(root.left, res);
14    serialize_dfs(root.right, res);
15}
16

Recommended Readings

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

Load More