Sorting with Custom Comparators
Almost every greedy on this phase begins with a sort. The algorithm is usually three lines; the sort key is the insight. “Sort by end time” is interval scheduling. “Sort by how much cheaper A is than B” is the solution to Two City Scheduling.
Python gives you two ways to express an ordering, and knowing which to reach for matters:
key=key=— map each element to a value that sorts naturally. Fast, idiomatic, and correct for almost everything.cmp_to_keycmp_to_key— define a pairwise comparison. Necessary only when the ordering genuinely cannot be expressed per-element.
This page covers both, plus the traps in mixed-direction sorting.
What you’ll learn
- Tuple keys for multi-level sorting, and negation for descending fields.
- Why negating a string is impossible, and what to do instead.
functools.cmp_to_keyfunctools.cmp_to_key— and how to recognise the rare case that needs it.- Why sort stability is a feature you can design around.
- Three real LeetCode problems solved in the browser: 179, 406, 1029.
The cue
Tool 1 — tuple keys
Tuples compare lexicographically, so a tuple key sorts by the first field, then breaks ties with the second, and so on.
people = [("alice", 30), ("bob", 25), ("carol", 30)]
print(sorted(people, key=lambda p: p[1])) # by age ascending
print(sorted(people, key=lambda p: -p[1])) # by age descending
print(sorted(people, key=lambda p: (-p[1], p[0]))) # age desc, then name ascpeople = [("alice", 30), ("bob", 25), ("carol", 30)]
print(sorted(people, key=lambda p: p[1])) # by age ascending
print(sorted(people, key=lambda p: -p[1])) # by age descending
print(sorted(people, key=lambda p: (-p[1], p[0]))) # age desc, then name ascThat last line is the pattern worth memorising: negate the fields you want descending, leave the rest ascending. It handles mixed directions in a single pass with no comparator function.
Tool 2 — cmp_to_keycmp_to_key for pairwise rules
Some orderings cannot be expressed as a per-element key at all. LC 179 is the
canonical example: to build the largest number from [3, 30, 34, 5, 9][3, 30, 34, 5, 9], you
need "3""3" before "30""30" because "330" > "303""330" > "303" — a fact about the pair,
not about either string alone.
from functools import cmp_to_key
def compare(a, b):
"""Return negative if a should come first, positive if b should."""
if a + b > b + a:
return -1 # a first
if a + b < b + a:
return 1 # b first
return 0 # equal
nums = ["3", "30", "34", "5", "9"]
print(sorted(nums, key=cmp_to_key(compare))) # ['9', '5', '34', '3', '30']from functools import cmp_to_key
def compare(a, b):
"""Return negative if a should come first, positive if b should."""
if a + b > b + a:
return -1 # a first
if a + b < b + a:
return 1 # b first
return 0 # equal
nums = ["3", "30", "34", "5", "9"]
print(sorted(nums, key=cmp_to_key(compare))) # ['9', '5', '34', '3', '30']The convention is C’s: negative means “first argument comes first”, positive means the second does, zero means equal. Getting the sign backwards silently reverses your sort.
| Speed | When to use | |
|---|---|---|
key=key= | Fast — computes nn keys, then sorts | Almost always |
cmp_to_keycmp_to_key | Slower — wraps every element, calls Python per comparison | Only when the rule is inherently pairwise |
The variant map
| Variant | The ordering | Canonical problem |
|---|---|---|
| Pairwise concatenation | a + ba + b vs b + ab + a via cmp_to_keycmp_to_key | 179 Largest Number |
| Mixed directions | Tuple key (-height, k)(-height, k) | 406 · 692 |
| Derived quantity | Sort by cost_a - cost_bcost_a - cost_b | 1029 Two City Scheduling |
| Descending string field | Two stable sorts, secondary first | 692 Top K Frequent Words |
| Category then content | Tuple key with a category flag first | 937 Reorder Data in Log Files |
| Sort to enable a greedy | Sort by end / by start | Interval scheduling |
Practice — real LeetCode problems
LC 179 — Largest Number · Medium
Problem. Given a list of non-negative integers, arrange them so that they form the largest possible number. Return it as a string.
Constraints. 1 <= len(nums) <= 1001 <= len(nums) <= 100, 0 <= nums[i] <= 10^90 <= nums[i] <= 10^9.
Examples. [10,2][10,2] gives "210""210" · [3,30,34,5,9][3,30,34,5,9] gives "9534330""9534330" ·
[0,0][0,0] gives "0""0"
Editorial — approach, complexity, follow-ups
The ordering is pairwise: aa should precede bb exactly when the
concatenation a + ba + b is larger than b + ab + a. No per-element score captures
this, because whether "3""3" beats "30""30" depends on "30""30".
Time comparisons, each on string length, so . Space .
Two details:
- The all-zeros guard.
[0,0][0,0]sorts to["0","0"]["0","0"]and joins to"000""000"— wait, to"00""00"— either way not"0""0". Since the comparator puts the largest first, if the leading character is"0""0"then every element is zero, so the answer is"0""0". Checkingresult[0] == "0"result[0] == "0"is sufficient and cheaper than stripping. - Transitivity. This comparator is a valid total order (the relation
a+b > b+aa+b > b+ais transitive on digit strings), which is what makes it safe to hand to a sort. A non-transitive comparator produces undefined results, and it is a fair thing to be asked about.
[432, 43, 43][432, 43, 43] giving "4343432""4343432" is the case that catches naive descending
sorts: plain string-descending order would put "432""432" first and produce
"4324343""4324343", which is smaller.
Follow-ups you should expect: “Can you avoid cmp_to_keycmp_to_key?” — yes, the
key=lambda s: s * 10key=lambda s: s * 10 trick, valid because values are bounded to 10 digits;
say why it works and why the comparator is safer. “Prove the comparator is
transitive” — a legitimate deeper question. “Smallest number instead?” — flip
the comparison, and mind leading zeros differently.
LC 406 — Queue Reconstruction by Height · Medium
Problem. Each person is [h, k][h, k]: height hh, and kk = the number of
people in front of them with height greater than or equal to hh.
Reconstruct and return the queue.
Constraints. 1 <= len(people) <= 20001 <= len(people) <= 2000, 0 <= h <= 10^60 <= h <= 10^6,
0 <= k < len(people)0 <= k < len(people). A valid queue is always possible.
Examples. [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]][[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]] gives
[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]][[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
Editorial — approach, complexity, follow-ups
Process people tallest first. When you insert someone, everyone already
in the list is at least as tall, so among those people the person’s kk is
exactly their target index — insert them there.
And inserting a shorter person later never invalidates an earlier
placement, because kk counts only people of height >= h>= h. A shorter person
does not count towards anyone taller. That monotonicity is why the greedy
works.
Time — list.insertlist.insert shifts elements, nn times. Space
. This is accepted at n <= 2000n <= 2000; a Fenwick tree or balanced BST that
finds the “k-th empty slot” reduces it to , which is the
optimisation to name if pushed.
The tuple key (-p[0], p[1])(-p[0], p[1]) is the crux:
-p[0]-p[0]gives descending height.p[1]p[1]gives ascendingkkwithin equal heights. This part is essential, not cosmetic: among people of the same height, the one with smallerkkmust be inserted first, or they would land behind their same-height peer and both counts would be wrong. Try[[7,0],[7,1]][[7,0],[7,1]]with thekkorder reversed and the output is[[7,1],[7,0]][[7,1],[7,0]], which is invalid.
Follow-ups you should expect: “Why tallest first?” — so that everyone
already placed counts towards kk, making kk an exact index. “Why is kk
ascending within a height?” — as above; a great test of whether you reasoned
or memorised. “Reduce the ?” — Fenwick tree over empty slots, or a
segment tree, for . “What if kk counted strictly taller
people?” — the same-height tie-break changes, and you must re-derive it.
LC 1029 — Two City Scheduling · Medium
Problem. 2n2n candidates must be flown to interviews. costs[i] = [aCost, bCost]costs[i] = [aCost, bCost] gives the cost of flying candidate ii to city A or city B.
Exactly nn must go to each city. Return the minimum total cost.
Constraints. 2 <= len(costs) <= 1002 <= len(costs) <= 100, len(costs)len(costs) is even,
1 <= aCost, bCost <= 10001 <= aCost, bCost <= 1000.
Examples. [[10,20],[30,200],[400,50],[30,20]][[10,20],[30,200],[400,50],[30,20]] gives 110110 ·
[[259,770],[448,54],[926,667],[184,139],[840,118],[577,469]][[259,770],[448,54],[926,667],[184,139],[840,118],[577,469]] gives 18591859
Editorial — approach, complexity, follow-ups
Send everyone to city B as a baseline, costing sum(bCost)sum(bCost). Moving person
ii to A instead changes the total by aCost[i] - bCost[i]aCost[i] - bCost[i]. You must move
exactly nn people, so choose the nn with the smallest (most negative)
differences.
Sorting by c[0] - c[1]c[0] - c[1] puts exactly those first, so the answer is the A
costs of the first half plus the B costs of the second.
Time . Space beyond the sort.
The lesson is the derived key: aCost - bCostaCost - bCost appears nowhere in the
input, and no ordering of aCostaCost or bCostbCost alone solves the problem.
Sorting by aCostaCost ascending fails — someone cheap for A might be even
cheaper for B, and the exact-split constraint is what makes only the
difference meaningful.
An exchange argument confirms optimality: if an optimal assignment sends pp
to B and qq to A where diff(p) < diff(q)diff(p) < diff(q), swapping them changes the total
by diff(p) - diff(q) < 0diff(p) - diff(q) < 0, an improvement. So no optimal solution can have
such an inversion, which is exactly what sorting by difference eliminates.
Follow-ups you should expect: “Why not sort by aCostaCost?” — the
counterexample above. “Prove it” — the exchange argument. “What if the split
were kk and 2n - k2n - k?” — same sort, split at kk. “What if there were three
cities?” — greedy breaks; it becomes min-cost flow or DP over counts, a
genuinely harder problem worth flagging.
LeetCode problem set
| # | Problem | Difficulty | The twist |
|---|---|---|---|
| 179 | Largest Number | Medium | Pairwise a+ba+b vs b+ab+a; needs cmp_to_keycmp_to_key |
| 406 | Queue Reconstruction by Height | Medium | Tuple key (-h, k)(-h, k), then insert at index kk |
| 1029 | Two City Scheduling | Medium | Sort by the derived aCost - bCostaCost - bCost |
| 937 | Reorder Data in Log Files | Medium | Category first, then content, then identifier — and digit logs keep original order (stability) |
| 692 | Top K Frequent Words | Medium | Count descending and word ascending — two stable sorts, or a (-count, word)(-count, word) key |
Interview follow-ups
| They ask | What they’re checking | The answer |
|---|---|---|
”keykey or cmp_to_keycmp_to_key?” | Judgement | keykey unless the rule is inherently pairwise; cmp_to_keycmp_to_key wraps every element and calls back into Python, so it is slower |
| “Sort descending on a string field?” | Language fluency | You cannot negate a string: use two stable sorts (secondary first), or a comparator |
| “Is Python’s sort stable?” | Useful detail | Yes, guaranteed — and you can design keys that rely on it (LC 937) |
| “Why does the tie-break direction matter in 406?” | Whether you reasoned it out | Among equal heights, smaller kk must be inserted first, or same-height people land in the wrong order |
| “Prove your ordering is optimal” | Rigour | An exchange argument: show that any inversion relative to your order can be swapped for an improvement |
| “Is your comparator a valid total order?” | Depth | It must be transitive, or the sort’s behaviour is undefined — worth checking for cmp_to_keycmp_to_key rules |
| “Can you avoid the inserts in 406?” | Optimisation | A Fenwick tree or segment tree over empty slots gives |
Edge-case checklist
- All zeros (LC 179) — must return
"0""0", not"00""00". - Single element — every sort is trivially correct; check the wrapper code still works.
- Ties on the primary key — the case that reveals whether your secondary direction is right.
- Equal heights in LC 406 —
[[7,0],[7,1]][[7,0],[7,1]]; the tie-break test. - Negative derived keys (LC 1029) —
aCost - bCostaCost - bCostis often negative; do not assume a sign. - Descending string fields — reach for two stable sorts, never
-string-string. - Duplicate elements — fine, but make sure your comparator returns
00for genuinely equal items rather than an arbitrary sign. - Comparator sign convention — negative means the first argument comes first; reversing it silently inverts the whole sort.
Recap
- In most greedy problems the algorithm is trivial and the sort key is the insight.
- Tuple keys handle multi-level ordering; negate the fields you want
descending:
(-height, k)(-height, k). - You cannot negate a string. Use two stable sorts, secondary key first.
cmp_to_keycmp_to_keyis for genuinely pairwise rules like LC 179’sa+ba+bvsb+ab+a. Negative means the first argument comes first, and the relation must be transitive.- Python’s sort is stable, and that is a feature you can design keys around (LC 937).
- The best keys are often derived quantities the input never states —
aCost - bCostaCost - bCostin LC 1029 is the whole solution. - Whatever ordering you choose, be ready to justify it with an exchange argument.
Next: the linked-list patterns — fast/slow pointers, in-place reversal, and dummy-head rewiring.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
