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

Custom orders and comparator pitfalls

Sort with keys and comparators in Python, JavaScript, Java, C++ and Go: build the largest number from joined values, and spot comparators that break sorts.

  • Intermediate
  • 35 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 core, Pro depth: 11 of 35 minutes free
Free: The problem: the largest banner number · The key idea: let both joins decide · Keys and comparators in five languages · Exercise and self-check. With a pass: 3 in-depth parts and 3 interview answers, about 24 minutes.

What you will learn

  • Write comparators and key functions for multi-key and non-obvious orders
  • Build the largest number from joined values with the concatenation order
  • Prove that the concatenation order is consistent and gives the largest number
  • Recognise comparators that break sorts in JavaScript, Python, Java, C++ and Go
  • Choose between a key and a comparator in Python from measured costs

Before you start

What you’ll learn · Before you start

What you will learn

  • Write comparators and key functions for multi-key and non-obvious orders
  • Build the largest number from joined values with the concatenation order
  • Prove that the concatenation order is consistent and gives the largest number
  • Recognise comparators that break sorts in JavaScript, Python, Java, C++ and Go
  • Choose between a key and a comparator in Python from measured costs

Before you start

On this page

Custom orders are sort rules that you supply yourself, as a key or as a comparator. You need one whenever a problem’s order is neither numeric nor alphabetical. A key turns each item into a value that the built-in order already sorts correctly; a comparator looks at two items and says which goes first. Comparator pitfalls are the ways such a rule breaks a sort without any error, such as returning a boolean, never admitting a tie, or using <= where a strict order is needed. This lesson solves a problem whose order exists only as a comparison of two items, the largest number that a list of numbers spells side by side, then names each language’s comparator pitfalls.

The problem: the largest banner number

The problem

A relay team’s jersey numbers are painted side by side on one banner, with no spaces. The coach wants the order that makes the banner read as the largest possible number. Given the jersey numbers jerseys[0..n − 1], return that number as a string, using every jersey exactly once; a banner of zeros reads as "0". Limits: 1 ≤ n ≤ 100,000 and 0 ≤ jerseys[i] ≤ 10⁹, so a banner can run to a million digits.

  1. [5, 56, 9, 504, 50] gives "956550504": 9 leads, then 56, 5, 50 and 504.
  2. [8, 89, 898] gives "898988". All three start with 8, and the shortest one goes last.
  3. [0, 0, 0] gives "0", not "000".

The brute force: every order, and two shortcuts that fail

Try every order, join the numbers and keep the largest banner. Every banner of a team has the same length, so comparing banners as text compares them as numbers. The brute force is plainly correct, which makes it the referee for faster code, but a team of n jerseys has n! orders:

Every order, then two shortcuts checked against it Python · brute/largest_brute.py
# The largest banner number by brute force: try every order of the jersey numbers and keep the largest result.
# Then two shortcuts that look right, checked against it on random teams.
import math
import random
from itertools import permutations


def as_number(text):
    """A banner of only zeros reads as 0, not 000."""
    return text.lstrip("0") or "0"


def largest_by_brute_force(jerseys):
    """Every order of the jerseys, n! of them; returns the largest banner and how many orders were tried."""
    tried = 0
    best = ""
    for order in permutations(jerseys):
        tried += 1
        banner = "".join(str(j) for j in order)
        # All banners have the same length, so comparing them as text compares them as numbers.
        if banner > best:
            best = banner
    return as_number(best), tried


def largest_value_first(jerseys):
    return as_number("".join(str(j) for j in sorted(jerseys, reverse=True)))


def largest_text_first(jerseys):
    return as_number("".join(sorted((str(j) for j in jerseys), reverse=True)))


for team in ([5, 56, 9, 504, 50], [0, 0, 0], [8, 89, 898]):
    banner, tried = largest_by_brute_force(team)
    print(f"{team} -> {banner} ({tried} orders tried)")

rng = random.Random(7)
teams = []
for _ in range(2_000):
    size = rng.randint(2, 5)
    # Numbers built from a few digits, so that shared prefixes such as 8, 89 and 898 are common.
    teams.append([int("".join(rng.choice("0589") for _ in range(rng.randint(1, 3)))) for _ in range(size)])

for name, shortcut in (("largest value first", largest_value_first), ("largest text first", largest_text_first)):
    wrong = [t for t in teams if shortcut(t) != largest_by_brute_force(t)[0]]
    first = min(wrong, key=len)
    print(f"{name}: wrong on {len(wrong):,} of {len(teams):,} teams, e.g. {first} gives {shortcut(first)}, "
          f"not {largest_by_brute_force(first)[0]}")

for n in (10, 15, 20):
    print(f"{n} jerseys: {math.factorial(n):,} orders")
digits = math.floor(math.lgamma(100_001) / math.log(10)) + 1
print(f"100,000 jerseys: the number of orders alone has {digits:,} digits")

Output

[5, 56, 9, 504, 50] -> 956550504 (120 orders tried)
[0, 0, 0] -> 0 (6 orders tried)
[8, 89, 898] -> 898988 (6 orders tried)
largest value first: wrong on 1,185 of 2,000 teams, e.g. [80, 598] gives 59880, not 80598
largest text first: wrong on 554 of 2,000 teams, e.g. [590, 59] gives 59059, not 59590
10 jerseys: 3,628,800 orders
15 jerseys: 1,307,674,368,000 orders
20 jerseys: 2,432,902,008,176,640,000 orders
100,000 jerseys: the number of orders alone has 456,574 digits

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

Ten jerseys already make 3,628,800 orders, and for 100,000 jerseys the number of orders has 456,574 digits. Two quick rules look as if they could replace the search, and the run checks both on 2,000 random teams. Largest value first was wrong on 1,185 of them, for example on [80, 598], where the leading 8 should beat 598. Largest text first was wrong on 554, for example on [590, 59], where the shorter 59 should lead. Neither rule looks at what decides a banner: the digits that come first once two numbers are joined.

The key idea: let both joins decide

Take two jerseys, a and b, as text. On the banner one of them comes first, and nothing else on the banner depends on that choice, so compare the two joins: if a + b is the larger number, a goes first. For 5 and 504 the joins are 5504 and 5045, so 5 leads; for 59 and 590 they are 59590 and 59059, so 59 leads. Sort the team with that comparison, join it, and the banner is the largest one. Finally, turn a banner that starts with 0 into "0". Only the number 0 starts with that digit and the comparison puts it last, so a leading zero means that every jersey is 0.

Two jerseys, 5 and 504, are joined both ways; 5504 beats 5045, so 5 goes first. The team sorted this way gives 956550504.Two jerseys:a = 5, b = 504a then b5 504 = 5504b then a504 5 = 50455504 > 5045,so 5 goes firstThe whole team in that order:9 · 56 · 5 · 50 · 504Banner: 956550504largerthe same test for every pairjoin

One comparison, decided by both concatenations

Text description of the diagram

The chart reads from top to bottom.

  1. Two jerseys are compared: a = 5 and b = 504.
  2. They are joined both ways. a then b gives 5504; b then a gives 5045.
  3. 5504 is larger than 5045, so 5 goes before 504 on the banner.
  4. Every pair of the team is ordered by the same test, which sorts the team as 9, 56, 5, 50, 504.
  5. Joining them gives the banner 956550504, the largest of all 120 orders.

Two facts make this safe: the comparison must be a consistent order, or no sort can be trusted with it, and the order it produces must beat every other order, not just look right locally. Both take a few lines to prove, and interviewers often ask for the second. The cost is one sort, about n log₂ n comparisons: roughly 1.7 million for 100,000 jerseys, each joining two texts of at most 10 digits.

A worked example

The sorted order of the first example, checked pair by pair:

Neighbours a + b b + a Goes first
9 and 56 956 569 9
56 and 5 565 556 56
5 and 50 550 505 5
50 and 504 50504 50450 50

Every pair of neighbours passes, and the banner, 956550504, is the one the brute force found.

Keys and comparators in five languages

Every language accepts custom orders, but each expects its own form:

  • Python: sorted() and list.sort() take a key only; functools.cmp_to_key turns a comparator into a key, as long as its result is below zero, zero or above zero.
  • JavaScript: Array.prototype.sort() takes a comparator whose result must be a number: below zero puts the first item first, above zero the second.
  • Java: a Comparator returns a negative, zero or positive int; Comparator.comparing builds one from a key.
  • C++: std::sort takes a predicate that is true only when its first argument goes strictly before the second.
  • Go: slices.SortFunc takes a function that returns a negative, zero or positive int.

Comparator pitfalls follow from those contracts, and each of them can pass a small test:

  • A boolean instead of a number: in JavaScript true counts as 1 and false as 0, so (a, b) => a > b never says “a first”.
  • No answer of zero: a comparator that never reports a tie breaks its contract whenever two items are equal.
  • <= where a strict order is needed: C++ requires comp(a, a) to be false.
  • Subtracting to compare: b - a can overflow a Java int, as the Java toolkit lesson shows.

Exercise and self-check

Write the banner yourself first. The sample tests include a stress test against every order of 300 small teams.

Exercise · Medium · Python

Arrange the jersey numbers into the largest banner

The relay team's jersey numbers are written side by side on a banner, with no spaces. Write largest_banner(jerseys) in banner.py: it returns, as a string, the largest number the banner can show, using every jersey exactly once. A banner of zeros reads as "0". There can be up to 100,000 jerseys, each a whole number from 0 to 10⁹, and the list passed in must not change.

largest_banner([5, 56, 9, 504, 50])   # "956550504"
largest_banner([8, 89, 898])          # "898988"
largest_banner([0, 0, 0])             # "0"

The sample tests check the examples, a single jersey, numbers that share a prefix such as 12 and 121, zeros mixed with other numbers, a stress test against every order of 300 random small teams (it prints the first team where your banner differs), and 20,000 jerseys within a fixed number of executed lines.

Starter code · banner.py

def largest_banner(jerseys):
    """The largest number, as a string, made by writing every jersey number side by side once."""
    # Replace this line with your code.
    return ""
The sample tests · test_banner.py
import random
import sys
from itertools import permutations

from banner import largest_banner

LINE_LIMIT = 4_000_000


def trusted(jerseys):
    """Every order of a small team, the largest banner kept."""
    best = max("".join(map(str, order)) for order in permutations(jerseys))
    return best.lstrip("0") or "0"


class TooSlow(Exception):
    pass


def run_limited(jerseys):
    """Call largest_banner, stopped after LINE_LIMIT executed lines (a fair time limit)."""
    executed = 0

    def count_lines(frame, event, arg):
        nonlocal executed
        if event == "line":
            executed += 1
            if executed > LINE_LIMIT:
                raise TooSlow
        return count_lines

    sys.settrace(count_lines)
    try:
        return largest_banner(jerseys)
    except TooSlow:
        raise AssertionError(f"too slow: more than {LINE_LIMIT:,} lines ran; aim for one sort") from None
    finally:
        sys.settrace(None)


def test_examples():
    """the examples from the prompt"""
    assert largest_banner([5, 56, 9, 504, 50]) == "956550504"
    assert largest_banner([8, 89, 898]) == "898988"
    assert largest_banner([0, 0, 0]) == "0"


def test_single_jersey():
    """one jersey is the whole banner"""
    assert largest_banner([7]) == "7"
    assert largest_banner([1_000_000_000]) == "1000000000"
    assert largest_banner([0]) == "0"


def test_shared_prefixes():
    """12 and 121, 3 and 30: the shorter number is not always first"""
    assert largest_banner([121, 12]) == "12121"
    assert largest_banner([12, 121]) == "12121"
    assert largest_banner([30, 3, 34]) == "34330"
    assert largest_banner([10, 2]) == "210"


def test_zeros_with_other_numbers():
    """zeros go last, and only an all-zero banner collapses to 0"""
    assert largest_banner([0, 0, 1]) == "100"
    assert largest_banner([0, 10, 100]) == "101000"


def test_input_not_changed():
    """the list passed in keeps its order"""
    team = [3, 30, 34, 5, 9]
    largest_banner(team)
    assert team == [3, 30, 34, 5, 9]


def test_stress_against_every_order():
    """300 random teams of up to 6 jerseys, built from few digits so that prefixes repeat"""
    rng = random.Random(42)
    for _ in range(300):
        team = [int("".join(rng.choice("0189") for _ in range(rng.randint(1, 3)))) for _ in range(rng.randint(1, 6))]
        got, want = largest_banner(list(team)), trusted(team)
        assert got == want, f"jerseys={team}: got {got!r}, expected {want!r}"


def test_large_team_is_fast():
    """20,000 jerseys within a fixed number of executed lines"""
    rng = random.Random(7)
    team = [rng.randint(0, 10**9) for _ in range(20_000)]
    banner = run_limited(team)
    assert len(banner) == sum(len(str(j)) for j in team)
    assert sorted(banner) == sorted("".join(map(str, team)))
A hint

Look at two jerseys only, a and b, as text. One of them has to come first, and the rest of the banner does not change which choice is better: compare the banner pieces a + b and b + a. functools.cmp_to_key turns such a comparison into a sort key. What should the function return for [0, 0]?

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Python Online Compiler Paste largest_brute.py and add teams of your own, such as numbers that share a long prefix.

Check yourself

4 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 4 With the concatenation test, which of the jerseys 34 and 3 goes first on the banner?

    Choose one answer.

    Show the answer to question 1

    Answer: 34, because 343 is larger than 334

    Join them both ways: 34 then 3 gives 343, and 3 then 34 gives 334. 343 is the larger banner piece, so 34 goes first. Length alone decides nothing: with 30 and 3 the shorter number goes first, because 330 beats 303.

  2. Question 2 of 4 Why does sorting the jerseys from the largest value to the smallest fail on [80, 598]?

    Choose one answer.

    Show the answer to question 2

    Answer: It puts 598 first and gives 59880, but 80598 is larger

    The banner's first digit matters most, and the value order looks at magnitude instead. 598 is the larger value, but its first digit, 5, is smaller than 8, so 80 should lead: 80598 beats 59880.

  3. Question 3 of 4 What should the banner be for the jerseys [0, 0, 0]?

    Choose one answer.

    Show the answer to question 3

    Answer: "0"

    A banner of zeros reads as the number 0, so the answer is the single digit "0". Joining the sorted texts gives "000", which is why the solution checks the first character after joining.

  4. Question 4 of 4 How many orders does the brute force try for a team of 8 jerseys?

    Type a number.

    Show the answer to question 4

    Answer: 40320

    Every order of 8 different jerseys is a permutation, and there are 8! = 40,320 of them. At 20 jerseys the count passes 2.4 × 10¹⁸, which is why trying orders cannot scale.

The comparison fits in one line in every language, and each language words it differently. Here are the five versions with their recorded runs, the two proofs that make the order safe, and a stress test that catches two plausible slips.

  • The code in Python, JavaScript, Java, C++ and Go
  • Why the order is consistent: each jersey is a repeating decimal
  • Why sorting gives the largest banner: an exchange argument
  • A stress test, and two slips it catches

3 runnable examples (Build and run all three, C++, Go, Java, JavaScript, Python) · about 3 minutes

Opening this part with your 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).

Comparator pitfalls reproduced in four languages Pro: opens with a Pro, Premium or Ultimate pass

Each sort contract has a typical way to break, and none of them fails loudly on a small test. Four of them are reproduced here with recorded runs, followed by the checks to make before trusting any comparator.

In this part:

  • Booleans in JavaScript and Python: the sort moves nothing
  • A comparator that never answers zero, in Java
  • >= in C++: undefined behaviour, then a named cause
  • The checks to make before you trust a comparator

2 runnable examples (Build and run both, C++, Java, JavaScript, Python) · about 3 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).

Keys or comparators: multi-key orders and measured costs Pro: opens with a Pro, Premium or Ultimate pass

Most custom orders in practice have several keys, such as score from high to low and then name from A to Z, and each language has an idiom for them. This part compares the idioms, measures what a comparator costs in Python, and ends with edge cases, variants, follow-up questions and a practice set.

In this part:

  • Several keys: a tuple, two stable sorts or a comparator
  • Measured: a comparator against two keys on 100,000 jerseys
  • Edge cases and common bugs
  • Variants, with answers
  • Follow-up questions, answered
  • Practice set

2 runnable examples (JavaScript, Python) · about 4 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).

Interview questions

  1. Fresher to mid · Warm-up

    What must a sort comparator return in JavaScript, Java and Go, and how does Python accept one?

    A comparator receives two items and returns a number whose sign is the answer: negative when the first item goes first, zero when they tie, positive when the second goes first. JavaScript converts the result with ToNumber, so it must be a number: a boolean becomes 1 or 0 and never says "first". Java's Comparator returns an int with the same signs, and Go's slices.SortFunc takes a function that returns an int. C++ differs: std::sort takes a predicate that is true only when the first item goes strictly before the second. Python's sorted() and list.sort() accept only a key function, and functools.cmp_to_key wraps a comparator into one. In every language the comparator must agree with itself: a tie one way is a tie the other way, and the order is transitive.

  2. Mid to senior

    You build the largest number from a list of non-negative integers by sorting them as strings, with a before b when a + b > b + a. The interviewer asks you to prove that this comparison is a valid sort order and that the result beats every other arrangement. How do you argue it?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Fresher to mid

    A JavaScript service sorts prices with list.sort((a, b) => a > b). Unit tests on five short inputs pass, but users see unsorted price lists in production. Explain what happens, fix it, and design a test that would have caught it.

    Model answer, Pro. First step and follow-ups

  4. Mid to senior

    In Python, sort a million log records by severity (critical, error, warning, info, in that order), then newest first, then by service name. A colleague's comparator with cmp_to_key is too slow. What do you write instead, and how much faster should it be?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A custom order reaches a sort as a key, which maps each item to a value, or as a comparator of two items.
  • The largest banner comes from one comparison: a goes first when a + b is the larger number.
  • A comparator must agree with itself: test it on many ties before you trust it.

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.