Topics › Heaps › Medium Problems
Replace Elements by Their Rank
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
Given an integer array arr, replace every element by its rank and return the resulting array. Ranks follow three rules:
- The smallest value has rank
1, and a larger value has a larger rank. - Equal values have the same rank.
- Ranks are as small as possible, so there are no gaps: if the distinct values are
d1 < d2 < ... < dm, thendihas ranki. (This is “dense” ranking, so two tied values at rank 1 are followed by rank 2, not rank 3.)
Constraints: 0 ≤ n ≤ 10^5, values fit in an int, negatives and duplicates allowed. The input array is left unchanged; a new array is returned.
Example 1
arr = [20, 15, 26, 2, 98, 6]
answer = [4, 3, 5, 1, 6, 2] // sorted: 2, 6, 15, 20, 26, 98 get ranks 1..6
Example 2
arr = [-5, 0, -5, 7]
answer = [1, 2, 1, 3] // both -5s share rank 1, and 0 gets rank 2 (no gap)
Approach 1: Brute force, count the distinct smaller values
Intuition. Rule 3 gives a direct formula: the rank of x is one more than the number of distinct values smaller than x. For each element, scan the whole array, put every smaller value into a set (so repeated values count once) and read off the set’s size.
Algorithm.
- For each index
i: - Create an empty set. For each
xinarr, ifx < arr[i], addxto the set. - Set
rank[i] = set.size() + 1.
static int[] bruteForce(int[] arr) {
int n = arr.length;
int[] rank = new int[n];
for (int i = 0; i < n; i++) {
Set<Integer> smaller = new HashSet<>();
for (int x : arr) {
if (x < arr[i]) smaller.add(x);
}
rank[i] = smaller.size() + 1;
}
return rank;
}
Complexity. Time O(n^2): n full scans with O(1) average set operations. Space O(n) for the set in the worst case.
Every element repeats the same question, “how many distinct values are below me?”, from scratch. If the values were laid out in sorted order, all the answers could be read off in one sweep.
Approach 2: Sort a copy and map values to ranks
Intuition. Sort a copy of the array. Walking the sorted copy from left to right, the distinct values appear in increasing order, so the first new value gets rank 1, the next new value rank 2, and so on; repeats of a value already seen are skipped. Store the ranks in a hash map from value to rank, then translate the original array through the map. The size of the map at the moment a new value is inserted is exactly the number of distinct values below it, which is precisely the brute-force formula.
Algorithm.
- Copy
arrand sort the copy. - For each
xin the sorted copy: ifxis not yet inrankOf, storerankOf[x] = rankOf.size() + 1. - Build the answer with
rank[i] = rankOf[arr[i]].
static int[] sortAndMap(int[] arr) {
int[] sorted = arr.clone();
Arrays.sort(sorted);
Map<Integer, Integer> rankOf = new HashMap<>();
for (int x : sorted) {
if (!rankOf.containsKey(x)) rankOf.put(x, rankOf.size() + 1);
}
int[] rank = new int[arr.length];
for (int i = 0; i < arr.length; i++) rank[i] = rankOf.get(arr[i]);
return rank;
}
Complexity. Time O(n log n) for the sort; the two passes are O(n) with average O(1) map operations. Space O(n) for the copy and the map.
Approach 3: Min-heap of value and index pairs
Intuition. This row sits in the heap section because a min-heap is another way to visit the values in increasing order, one at a time. Push a pair {value, index} for every element; popping then yields the elements from smallest to largest, and the stored index tells us where to write the rank. Keep a running rank counter and the previously popped value: when the popped value differs from the previous one, we have reached a new distinct value and the counter goes up by one. Equal values pop consecutively, so they all receive the same counter value. No map is needed, because the index travels with the value.
Algorithm.
- Push
{arr[i], i}for everyiinto a min-heap ordered by value. - Set
current = 0andprevto a sentinel that equals noint(alongholdingLong.MIN_VALUE). - While the heap is not empty: pop
{v, i}. Ifv != prev, incrementcurrentand setprev = v. Writerank[i] = current.
static int[] minHeap(int[] arr) {
PriorityQueue<int[]> pq = new PriorityQueue<>((x, y) -> Integer.compare(x[0], y[0]));
for (int i = 0; i < arr.length; i++) pq.offer(new int[]{arr[i], i});
int[] rank = new int[arr.length];
int current = 0;
long prev = Long.MIN_VALUE; // equal to no int, so the first pop opens rank 1
while (!pq.isEmpty()) {
int[] top = pq.poll();
if (top[0] != prev) {
current++;
prev = top[0];
}
rank[top[1]] = current;
}
return rank;
}
Dry run on Example 1, arr = [20, 15, 26, 2, 98, 6]:
| popped {value, index} | value changed? | current | rank array |
|---|---|---|---|
| {2, 3} | yes | 1 | [_, _, _, 1, _, _] |
| {6, 5} | yes | 2 | [_, _, _, 1, _, 2] |
| {15, 1} | yes | 3 | [_, 3, _, 1, _, 2] |
| {20, 0} | yes | 4 | [4, 3, _, 1, _, 2] |
| {26, 2} | yes | 5 | [4, 3, 5, 1, _, 2] |
| {98, 4} | yes | 6 | [4, 3, 5, 1, 6, 2] |
Complexity. Time O(n log n): n pushes and n pops on a heap of size up to n. Space O(n) for the heap of pairs.
Approaches 2 and 3 have the same bound. Sorting is usually faster in practice because Arrays.sort on primitives avoids allocating one small array per element; the heap version is worth knowing because the same “pop in order, carry the index along” pattern solves many problems where elements arrive over time.
Edge cases and pitfalls
- Dense, not competition, ranking.
[100, 100, 100]becomes[1, 1, 1], and in[-5, 0, -5, 7]the value0gets rank 2, not 3. Using the sorted index plus one gives competition ranking and is wrong here. - Duplicates must share a rank. In Approach 2, insert a value into the map only the first time it is seen; overwriting it later would give later duplicates a larger rank.
- The sentinel. Starting
prevat0or atInteger.MIN_VALUEbreaks when the smallest value is exactly that number, because the first pop would not open rank 1. Alongsentinel outside theintrange cannot collide; the tests includeInteger.MIN_VALUE. - Empty array. All three methods return an empty array without special cases.
- Do not sort the input itself. The original order is needed to place the ranks, which is why Approach 2 sorts a clone; the tests check that the input is unchanged.
- Comparator. Order pairs with
Integer.compare(x[0], y[0])rather thanx[0] - y[0], which overflows for values of opposite sign.
Related problems
- Kth smallest element in an array [use priority queue] also visits values in increasing order with a heap.
- K-th Largest element in an array asks for a single rank instead of all of them.
- Top K Frequent Elements combines a hash map of counts with a heap.
- Basic Hashing covers the value-to-information map used in Approach 2.
- Heaps (Theory Video) explains heaps of pairs with a custom comparator.