Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Coding Interview Patterns  Module 7 – Sorting as a tool

Coordinate compression and ranks

Coordinate compression maps large or negative values to dense ranks, so arrays, difference arrays and Fenwick trees fit: ties, 1-based ranks and the gap bug.

  • Beginner
  • 25 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0, quickjs 0.32.0, Java 25, Clang 21.0.0 and Go 1.27.1
  • By MySmartCoPilot
Free intro, Pro: 5 of 25 minutes free
Free: The problem: rank the contest scores. With a pass: the other 13 sections and 3 interview answers, about 20 minutes.

What you will learn

  • Map large or negative values to dense ranks with one sort and a lookup
  • Use ranks to index arrays, difference arrays and Fenwick trees
  • Handle ties consistently and choose ranks from 0 or from 1
  • Avoid the gap bug when compressed points must measure lengths

Before you start

What you’ll learn · Before you start

What you will learn

  • Map large or negative values to dense ranks with one sort and a lookup
  • Use ranks to index arrays, difference arrays and Fenwick trees
  • Handle ties consistently and choose ranks from 0 or from 1
  • Avoid the gap bug when compressed points must measure lengths

Before you start

On this page

Coordinate compression replaces each value by its rank among the distinct values and keeps their order: the smallest value becomes rank 0 or 1, the next distinct value the next rank, and so on. Ranks let an array, a difference array, a Fenwick tree or a table indexed by value work even when the values reach 10⁹, are negative or are spread far apart, because n values have at most n distinct ranks. Coordinate compression costs one sort, O(n log n), plus one lookup per value. This lesson’s problem is the rank transform itself, replacing every score by its dense rank, and the uses of the ranks come after it.

The problem: rank the contest scores

The problem

A coding contest’s final scores, after penalties, are whole numbers from −10⁹ to 10⁹, scores[0..n − 1], in the order of the contestants’ ids. Replace every score by its rank among the distinct scores: the lowest score has rank 1, equal scores share a rank, and the ranks have no gaps. Return the ranks in the original order. Limits: 0 ≤ n ≤ 10⁶.

  1. [40, 10, 40, 25] gives [3, 1, 3, 2]: the distinct scores are 10, 25 and 40.
  2. [-5, 7, -5, 0, 1000000000] gives [1, 3, 1, 2, 4].
  3. [] gives [].
The scores −5, 7, −5, 0 and 10⁹ map to the sorted distinct scores −5, 0, 7, 10⁹, ranked 1 to 4; in input order 1, 3, 1, 2, 4.Scores as given-57-5010⁹-5rank 10rank 27rank 310⁹rank 4Above: the distinct scores, sorted, with their ranksranks in input order: 1, 3, 1, 2, 4

From scores to dense ranks

Text description of the diagram

The top row holds the scores as given: −5, 7, −5, 0 and 10⁹ (one billion).

The second row holds the distinct scores in sorted order, each with its rank underneath: −5 is rank 1, 0 is rank 2, 7 is rank 3 and 10⁹ is rank 4. A line joins every score of the top row to its copy in the second row; both −5s join the same box.

The line at the bottom reads the ranks back in input order: 1, 3, 1, 2, 4. The huge score 10⁹ simply becomes 4, so an array of four slots can stand in for a range of two billion values.

The brute force, and the array that does not fit

For every score, count the distinct scores below it and add 1. That is n × d comparisons for d distinct scores. The other obvious idea, an array with one slot per possible score, does not fit at all:

Counting smaller scores, and an array indexed by score Python · brute/rank_brute.py
# Dense ranks by brute force: a score's rank is 1 plus the number of distinct scores below it. Then what an array
# indexed by the scores themselves would need.
import random


def ranks_brute(scores):
    """Returns the ranks and the comparisons made: one per pair of a score and a distinct score."""
    distinct = list(set(scores))
    comparisons = 0
    ranks = []
    for s in scores:
        below = 0
        for d in distinct:
            comparisons += 1
            if d < s:
                below += 1
        ranks.append(below + 1)
    return ranks, comparisons


for scores in ([40, 10, 40, 25], [-5, 7, -5, 0, 1_000_000_000], []):
    print(scores, "->", ranks_brute(scores)[0])

rng = random.Random(2)
for n in (1_000, 2_000):
    _, comparisons = ranks_brute([rng.randint(-10**9, 10**9) for _ in range(n)])
    print(f"n = {n:,} distinct scores: {comparisons:,} comparisons")
print(f"n = 1,000,000 distinct scores: {10**12:,} comparisons")
slots = 2 * 10**9 + 1
print(f"an array with one slot per possible score: {slots:,} slots, {slots * 4 / 10**9:.1f} GB as 32-bit counters")

Output

[40, 10, 40, 25] -> [3, 1, 3, 2]
[-5, 7, -5, 0, 1000000000] -> [1, 3, 1, 2, 4]
[] -> []
n = 1,000 distinct scores: 1,000,000 comparisons
n = 2,000 distinct scores: 4,000,000 comparisons
n = 1,000,000 distinct scores: 1,000,000,000,000 comparisons
an array with one slot per possible score: 2,000,000,001 slots, 8.0 GB as 32-bit counters

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 rank_brute.py

The count made 4,000,000 comparisons for 2,000 different scores and would make 10¹² for a million, far beyond a time limit. An array indexed by the score itself needs 2,000,000,001 slots, 8 GB as 32-bit counters, even for a list of five scores.

What to notice

Only the order of the scores matters, never their size, and a list of n scores has at most n distinct values. Put in order once, the distinct values can serve as the index: a score’s rank is its position among them, whatever its magnitude.

Compress with one sort and one lookup Pro: opens with a Pro, Premium or Ultimate pass

In this part:

  • Why the ranks keep every comparison
  • A dry run on the second example
  • The code in Python, JavaScript, Java, C++ and Go
    • Three slips, caught by a stress test
  • Ranks as array indices: the busiest moment of 100,000 bookings
  • The gap bug: when ranks must measure lengths
  • Ranks for a Fenwick tree: lower scores to the right
  • Edge cases and common bugs
  • Variants, with answers
  • Follow-up questions, answered
  • Exercise and practice set
    • Replace every score by its dense rank
  • Check yourself
  • Interview questions
    • What is the difference between dense, competition and ordinal ranks when values tie?
    • You need, for each of 10⁶ transactions in time order, how many earlier transactions had a smaller amount, and amounts range from −10¹² to 10¹². A teammate proposes a Fenwick tree indexed by amount. What goes wrong, and what do you build instead?
    • A service merges 10⁵ closed ranges of seat numbers, each up to 10⁹, and reports how many seats are covered. It compresses the range ends and marks ranks, and the reported count is wrong on some inputs. Explain the bug with an example and fix it.
    • A monitoring job receives latency values one at a time and must report, after each, how many earlier values were lower. The values are not known in advance, so you cannot sort them first. How do you keep the answers in O(log n) each?
  • Key takeaways

1 diagram · 6 runnable examples (Build and run all three, C++, Go, Java, JavaScript, Python) · 1 exercise with sample tests · 3 interview answers · about 23 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.