Facebook Pixel

1331. Rank Transform of an Array

Problem Description

You are given an array of integers arr. Your task is to replace each element in the array with its rank according to specific rules.

The rank represents the relative size of each element compared to other elements in the array. The ranking rules are:

  1. Rank starts from 1: The smallest element gets rank 1, the next smallest gets rank 2, and so on.

  2. Larger elements get larger ranks: If element A is greater than element B, then A's rank must be greater than B's rank.

  3. Equal elements get equal ranks: If two or more elements have the same value, they must receive the same rank.

  4. Ranks should be consecutive: The ranks should use the smallest possible integers. For example, if you have values [10, 20, 20, 30], the ranks would be [1, 2, 2, 3] not [1, 2, 2, 4].

For example:

  • Input: arr = [40, 10, 20, 30]

  • Output: [4, 1, 2, 3]

  • Explanation: 10 is the smallest (rank 1), 20 is second smallest (rank 2), 30 is third (rank 3), and 40 is the largest (rank 4).

  • Input: arr = [100, 100, 100]

  • Output: [1, 1, 1]

  • Explanation: All elements are equal, so they all get rank 1.

The solution uses discretization by creating a sorted array of unique values from the original array. Then for each element in the original array, it finds its position in this sorted unique array using binary search (bisect_right), which directly gives the rank.

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

Intuition

The key insight is recognizing that ranks are essentially the position of each element when all unique values are sorted in ascending order.

Think about it this way: if we line up all the distinct values from smallest to largest, the rank of any element is simply its position in this lineup. For example, with values [40, 10, 20, 30], the unique sorted values are [10, 20, 30, 40]. The element 10 is at position 1, 20 is at position 2, and so on.

But what about duplicate values? Since equal elements must have the same rank, we only need to consider unique values when determining ranks. This naturally handles the duplicate case - if we have [20, 10, 20, 30], we create a sorted unique list [10, 20, 30], and both occurrences of 20 will map to the same position (rank 2).

This leads us to a two-step approach:

  1. Extract all unique values and sort them - this gives us the "rank mapping"
  2. For each element in the original array, find where it sits in this sorted unique list

The beauty of using bisect_right is that it returns the insertion point that would maintain sorted order, which is exactly the count of elements less than or equal to the target value. This count is precisely the rank we need! For instance, if bisect_right([10, 20, 30], 20) returns 2, it means there are 2 elements ≀ 20, making 20's rank equal to 2.

This approach elegantly satisfies all requirements: ranks start from 1 (since bisect_right returns indices starting from 0 for non-existent smaller elements, and we get 1 for the smallest element), larger elements get larger ranks (guaranteed by the sorted order), and equal elements get equal ranks (they map to the same position in the sorted unique array).

Learn more about Sorting patterns.

Solution Approach

The solution implements the discretization technique to transform array values into ranks. Here's the step-by-step implementation:

Step 1: Create a sorted unique array

t = sorted(set(arr))
  • set(arr) removes all duplicate values from the original array
  • sorted() arranges these unique values in ascending order
  • This creates our "rank mapping" array t where the position of each value determines its rank

For example, if arr = [40, 10, 20, 30, 20]:

  • set(arr) gives us {10, 20, 30, 40}
  • sorted(set(arr)) gives us [10, 20, 30, 40]

Step 2: Map each element to its rank using binary search

return [bisect_right(t, x) for x in arr]
  • For each element x in the original array
  • bisect_right(t, x) performs binary search to find the rightmost insertion position of x in the sorted array t
  • This position equals the number of unique values less than or equal to x, which is exactly the rank

The bisect_right function returns:

  • For x = 10: position 1 (since 10 is at index 0, and bisect_right returns the position after the last occurrence)
  • For x = 20: position 2
  • For x = 30: position 3
  • For x = 40: position 4

Time Complexity: O(n log n) where n is the length of the array

  • Creating the set: O(n)
  • Sorting unique elements: O(m log m) where m ≀ n is the number of unique elements
  • Binary search for each element: O(n log m)
  • Overall: O(n log n) dominated by sorting

Space Complexity: O(m) where m is the number of unique elements in the array for storing the sorted unique array t

The elegance of this approach lies in how bisect_right naturally handles all the ranking rules - it automatically assigns consecutive ranks starting from 1, maintains order relationships, and gives equal elements the same rank since they map to the same position in the sorted unique array.

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 the solution with the array arr = [30, 10, 30, 20].

Step 1: Create sorted unique array

  • Start with: arr = [30, 10, 30, 20]
  • Extract unique values: set(arr) = {10, 20, 30}
  • Sort them: t = [10, 20, 30]

Now we have our rank mapping where:

  • Position 1 β†’ value 10 (smallest)
  • Position 2 β†’ value 20 (middle)
  • Position 3 β†’ value 30 (largest)

Step 2: Find rank for each original element

Process each element in the original array [30, 10, 30, 20]:

  1. First element: 30

    • Use bisect_right(t, 30) where t = [10, 20, 30]
    • Binary search finds 30 at index 2
    • bisect_right returns the position after the last occurrence: 3
    • Rank of 30 is 3
  2. Second element: 10

    • Use bisect_right(t, 10) where t = [10, 20, 30]
    • Binary search finds 10 at index 0
    • bisect_right returns the position after the last occurrence: 1
    • Rank of 10 is 1
  3. Third element: 30

    • Use bisect_right(t, 30) where t = [10, 20, 30]
    • Same as before, returns 3
    • Rank of 30 is 3 (same as the first 30, satisfying the equal elements rule)
  4. Fourth element: 20

    • Use bisect_right(t, 20) where t = [10, 20, 30]
    • Binary search finds 20 at index 1
    • bisect_right returns the position after the last occurrence: 2
    • Rank of 20 is 2

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

This correctly assigns:

  • Rank 1 to the smallest value (10)
  • Rank 2 to the middle value (20)
  • Rank 3 to the largest value (30)
  • Both occurrences of 30 get the same rank (3)

Solution Implementation

1from typing import List
2from bisect import bisect_right
3
4class Solution:
5    def arrayRankTransform(self, arr: List[int]) -> List[int]:
6        # Create a sorted list of unique elements from the array
7        # This represents all distinct values in ascending order
8        unique_sorted = sorted(set(arr))
9      
10        # For each element in the original array, find its rank
11        # bisect_right returns the insertion position, which equals the rank
12        # since we're using 1-based ranking (position 0 means rank 1)
13        return [bisect_right(unique_sorted, x) for x in arr]
14
1class Solution {
2    public int[] arrayRankTransform(int[] arr) {
3        int arrayLength = arr.length;
4      
5        // Create a copy of the original array for sorting
6        int[] sortedArray = arr.clone();
7        Arrays.sort(sortedArray);
8      
9        // Remove duplicates from sorted array to get unique values
10        // uniqueCount will track the number of unique elements
11        int uniqueCount = 0;
12        for (int i = 0; i < arrayLength; ++i) {
13            // Keep first occurrence or when current element differs from previous
14            if (i == 0 || sortedArray[i] != sortedArray[i - 1]) {
15                sortedArray[uniqueCount++] = sortedArray[i];
16            }
17        }
18      
19        // Create result array to store ranks
20        int[] result = new int[arrayLength];
21      
22        // For each element in original array, find its rank
23        for (int i = 0; i < arrayLength; ++i) {
24            // Binary search returns index of element in unique sorted array
25            // Add 1 because ranks start from 1, not 0
26            result[i] = Arrays.binarySearch(sortedArray, 0, uniqueCount, arr[i]) + 1;
27        }
28      
29        return result;
30    }
31}
32
1class Solution {
2public:
3    vector<int> arrayRankTransform(vector<int>& arr) {
4        // Create a copy of the original array for sorting
5        vector<int> sortedUnique = arr;
6      
7        // Sort the copy to prepare for ranking
8        sort(sortedUnique.begin(), sortedUnique.end());
9      
10        // Remove duplicate elements to get unique sorted values
11        // unique() returns iterator to the new end after removing duplicates
12        sortedUnique.erase(unique(sortedUnique.begin(), sortedUnique.end()), 
13                          sortedUnique.end());
14      
15        // Store the ranks for each element in the original array
16        vector<int> ranks;
17      
18        // For each element in the original array, find its rank
19        for (int element : arr) {
20            // Find the position of element in sorted unique array
21            // Note: Using lower_bound would give 0-based index, adding 1 would give 1-based rank
22            // Current code uses upper_bound which gives the position after the element
23            int rank = upper_bound(sortedUnique.begin(), sortedUnique.end(), element) 
24                       - sortedUnique.begin();
25            ranks.push_back(rank);
26        }
27      
28        return ranks;
29    }
30};
31
1/**
2 * Transforms array elements to their rank (1-indexed position after sorting)
3 * @param arr - Input array of numbers
4 * @returns Array where each element is replaced by its rank
5 */
6function arrayRankTransform(arr: number[]): number[] {
7    // Create a sorted copy of the input array
8    const sortedArray: number[] = [...arr].sort((a, b) => a - b);
9  
10    // Remove duplicates from sorted array and track unique count
11    let uniqueCount: number = 0;
12    for (let i = 0; i < sortedArray.length; ++i) {
13        // Keep only first occurrence of each unique value
14        if (i === 0 || sortedArray[i] !== sortedArray[i - 1]) {
15            sortedArray[uniqueCount++] = sortedArray[i];
16        }
17    }
18  
19    /**
20     * Binary search to find the position of a value in sorted array
21     * @param sortedArr - Sorted array to search in
22     * @param rightBound - Right boundary for search (exclusive)
23     * @param target - Value to search for
24     * @returns Index position of target + 1 (1-indexed rank)
25     */
26    const binarySearch = (sortedArr: number[], rightBound: number, target: number): number => {
27        let left: number = 0;
28        let right: number = rightBound;
29      
30        // Binary search for first element greater than target
31        while (left < right) {
32            const mid: number = (left + right) >> 1;
33            if (sortedArr[mid] > target) {
34                right = mid;
35            } else {
36                left = mid + 1;
37            }
38        }
39        return left;
40    };
41  
42    // Build result array by finding rank of each original element
43    const result: number[] = [];
44    for (const value of arr) {
45        // Find rank (1-indexed position) of current value
46        result.push(binarySearch(sortedArray, uniqueCount, value));
47    }
48  
49    return result;
50}
51

Time and Space Complexity

Time Complexity: O(n Γ— log n)

The time complexity breaks down as follows:

  • set(arr): O(n) to create a set from the array
  • sorted(set(arr)): O(m Γ— log m) where m is the number of unique elements. In the worst case, m = n, so this is O(n Γ— log n)
  • List comprehension with bisect_right: For each of the n elements in arr, we perform a binary search using bisect_right on the sorted array t of size m. Each binary search takes O(log m) time. Total: O(n Γ— log m), which is O(n Γ— log n) in the worst case

The dominant operation is either the sorting step or the binary search step, both of which are O(n Γ— log n) in the worst case.

Space Complexity: O(n)

The space complexity consists of:

  • set(arr): O(m) space where m ≀ n
  • sorted(set(arr)): Creates a new list t of size m, requiring O(m) space
  • Output list from the list comprehension: O(n) space

In the worst case where all elements are unique, m = n, giving us O(n) space complexity.

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

Common Pitfalls

Pitfall 1: Confusion Between bisect_left and bisect_right

A common mistake is using bisect_left instead of bisect_right, which would produce incorrect results.

Why this happens:

  • bisect_left(t, x) returns the leftmost position where x should be inserted
  • bisect_right(t, x) returns the rightmost position where x should be inserted

Example demonstrating the issue:

arr = [20, 10, 20, 30]
unique_sorted = [10, 20, 30]  # sorted unique values

# Using bisect_left (WRONG):
# bisect_left(unique_sorted, 10) = 0  β†’ rank would be 0 (incorrect!)
# bisect_left(unique_sorted, 20) = 1  β†’ rank would be 1
# bisect_left(unique_sorted, 30) = 2  β†’ rank would be 2

# Using bisect_right (CORRECT):
# bisect_right(unique_sorted, 10) = 1  β†’ rank is 1 βœ“
# bisect_right(unique_sorted, 20) = 2  β†’ rank is 2 βœ“
# bisect_right(unique_sorted, 30) = 3  β†’ rank is 3 βœ“

Solution: Always use bisect_right for ranking problems since it naturally provides 1-based indexing. The rightmost position after an element equals the count of elements less than or equal to it.

Pitfall 2: Attempting Manual Rank Assignment with Duplicates

Some might try to manually assign ranks while iterating through sorted elements, leading to complex logic for handling duplicates:

Incorrect approach:

def arrayRankTransform(arr):
    if not arr:
        return []
  
    sorted_arr = sorted(enumerate(arr), key=lambda x: x[1])
    ranks = [0] * len(arr)
    current_rank = 1
  
    # This gets complicated with duplicates
    for i in range(len(sorted_arr)):
        if i > 0 and sorted_arr[i][1] != sorted_arr[i-1][1]:
            current_rank += 1  # Wrong! Should increment by unique count
        ranks[sorted_arr[i][0]] = current_rank
  
    return ranks

Problems with this approach:

  • Requires careful tracking of when to increment ranks
  • Easy to make off-by-one errors
  • More complex code that's harder to debug

Solution: Use the discretization approach with sorted(set(arr)) and bisect_right. This automatically handles all edge cases and duplicate values without special logic.

Pitfall 3: Not Handling Empty Arrays

The code might fail or behave unexpectedly with empty input arrays.

Test case:

arr = []
unique_sorted = sorted(set([]))  # Returns []
result = [bisect_right([], x) for x in []]  # Returns []

While the current implementation handles this correctly, it's important to be aware that:

  • sorted(set([])) returns an empty list
  • List comprehension over an empty list returns an empty list
  • bisect_right is never called, avoiding any potential issues

Solution: The current implementation naturally handles empty arrays, but for clarity and defensive programming, you might want to add an explicit check:

def arrayRankTransform(self, arr: List[int]) -> List[int]:
    if not arr:
        return []
  
    unique_sorted = sorted(set(arr))
    return [bisect_right(unique_sorted, x) for x in arr]
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following uses divide and conquer strategy?


Recommended Readings

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

Load More