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.
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.
[5, 56, 9, 504, 50]gives"956550504": 9 leads, then 56, 5, 50 and 504.[8, 89, 898]gives"898988". All three start with 8, and the shortest one goes last.[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:
# 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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
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.
One comparison, decided by both concatenations
Text description of the diagram
The chart reads from top to bottom.
- Two jerseys are compared: a = 5 and b = 504.
- They are joined both ways. a then b gives 5504; b then a gives 5045.
- 5504 is larger than 5045, so 5 goes before 504 on the banner.
- Every pair of the team is ordered by the same test, which sorts the team as 9, 56, 5, 50, 504.
- 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()andlist.sort()take a key only;functools.cmp_to_keyturns 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
Comparatorreturns a negative, zero or positiveint;Comparator.comparingbuilds one from a key. - C++:
std::sorttakes a predicate that is true only when its first argument goes strictly before the second. - Go:
slices.SortFunctakes a function that returns a negative, zero or positiveint.
Comparator pitfalls follow from those contracts, and each of them can pass a small test:
- A boolean instead of a number: in JavaScript
truecounts as 1 andfalseas 0, so(a, b) => a > bnever 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++ requirescomp(a, a)to be false.- Subtracting to compare:
b - acan overflow a Javaint, 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]?
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
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.
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.
The banner order in five languages, with its proof Pro: opens with a Pro, Premium or Ultimate pass
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.
In this part:
- 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…
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).
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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
-
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.
-
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?
-
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.
-
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?
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:
agoes first whena + bis the larger number. - A comparator must agree with itself: test it on many ties before you trust it.
References
- functools.cmp_to_key (Python Software Foundation)
- Sorting Techniques (Python Software Foundation)
- Built-in functions: sorted() (Python Software Foundation)
- ECMAScript Language Specification: Array.prototype.sort, SortIndexedProperties and CompareArrayElements (Ecma International (TC39))
- Java SE 25 API: Comparator (Oracle)
- Java SE 25 API: List.sort (Oracle)
- C++ named requirements: Compare (cppreference.com)
- std::sort (cppreference.com)
- Hardening modes (libc++ documentation) (The LLVM Project)
- Go standard library: package slices (SortFunc) (The Go Authors)
- Go standard library: package cmp (Compare, Or) (The Go Authors)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress