← back

Sorting Techniques

Custom Comparator Sort

Define a comparator that determines the final order. For largest number, compare str(a)+str(b) vs str(b)+str(a). Reduces a complex ordering problem to a pairwise comparison.

Custom Comparator Sorting

Python's built-in sorted() and .sort() methods order elements in ascending order by default: integers numerically, strings lexicographically. That works perfectly for most problems, but a surprising number of interview questions require a completely different ordering. The moment you catch yourself thinking "I need to sort these, but not in the normal way," a custom comparator is probably your tool.

The classic trigger problem is Largest Number (LC 179): given a list of integers, arrange them to form the largest possible number. With [3, 30, 34, 5, 9], standard numeric sort gives [3, 5, 9, 30, 34], which concatenates to "3593034", nowhere near optimal. The correct answer is "9534330".

The Key Insight: Compare Concatenations

The ordering rule for largest number is: number a should come before number b if the string a+b is lexicographically greater than b+a. For example, comparing "9" and "34": "934" > "349", so 9 goes first. Comparing "3" and "30": "330" > "303", so 3 goes before 30.

This comparison relation is transitive, meaning it defines a valid total order and can be used as a sort key. The tricky part is that Python 3 removed the old-style cmp argument from sorted(). Instead, you convert a two-argument comparator into a key function using functools.cmp_to_key.

A comparator function takes two arguments a and b and must return:

  • a negative number if a should come first
  • a positive number if b should come first
  • zero if they are equal

Walking Through the Example

Starting with [3, 30, 34, 5, 9], convert everything to strings: ["3", "30", "34", "5", "9"].

The comparator checks a+b versus b+a. A few representative comparisons:

  • "9" vs "5": "95" > "59", so 9 comes first.
  • "5" vs "34": "534" > "345", so 5 comes first.
  • "3" vs "30": "330" > "303", so 3 comes first.
  • "30" vs "34": "3034" < "3430", so 34 comes first.

After sorting: ["9", "5", "34", "3", "30"]. Concatenated: "9534330". One edge case: if every number is zero, the result is all zeros, so return "0" rather than "000...".

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from functools import cmp_to_key

def largest_number(nums: list[int]) -> str:
    strs = list(map(str, nums))

    def compare(a, b):
        if a + b > b + a:
            return -1   # a should come first
        elif a + b < b + a:
            return 1    # b should come first
        return 0

    strs.sort(key=cmp_to_key(compare))
    result = "".join(strs)
    return "0" if result[0] == "0" else result

The lambda version you'll often see in competitive code condenses this to a one-liner: key=cmp_to_key(lambda a, b: (1 if a+b < b+a else -1 if a+b > b+a else 0)). Both are correct; the explicit function is easier to read under pressure.

Multi-Key Sorting with Tuples

When you need to sort by multiple criteria, say by priority descending and then by name ascending, Python's tuple comparison gives you multi-key sorting for free. Because tuples are compared element by element, returning a tuple from a key function lets you express mixed ascending/descending orderings cleanly.

1
2
3
4
5
tasks = [("task_a", 2), ("task_b", 1), ("task_c", 2)]

# Sort by priority descending, then by name ascending
tasks.sort(key=lambda t: (-t[1], t[0]))
# Result: [("task_a", 2), ("task_c", 2), ("task_b", 1)]

Negating a numeric field flips its sort direction without needing a custom comparator at all. This tuple-key approach is almost always preferable to cmp_to_key when a simple key expression can express the ordering.

Interval Sorting for Greedy Problems

Interval scheduling problems often live or die on how you sort before applying the greedy pass. The two most common orderings are:

Sort by end time (earliest deadline first) is used in the classic activity selection / non-overlapping intervals problem. Sorting intervals by their end time lets the greedy algorithm always pick the interval that leaves the most room for future choices.

Sort by start time is used when merging overlapping intervals. After sorting by start, a single left-to-right sweep can merge intervals because any overlap must involve the previous interval's end and the current interval's start.

1
2
3
4
5
# Sort intervals by end time for greedy scheduling
intervals.sort(key=lambda x: x[1])

# Sort intervals by start time for merging
intervals.sort(key=lambda x: x[0])

These are so common that recognizing which ordering a greedy interval problem needs is itself a key skill.

Queue Reconstruction by Height

LeetCode 406. Queue Reconstruction by Height is a beautiful example of how sorting order determines algorithm correctness. You are given a list of people described by [height, k], where k is the number of people in front who are taller or equal in height. Reconstruct the queue.

The insight: sort people by height descending, then by k ascending. Then insert each person into the result list at index k. Because taller people are placed first, when you insert a shorter person at position k, all previously placed (taller) people are already in the list and will correctly be "in front" of them. Shorter people inserted later never affect the k-value of taller people already placed.

1
2
3
4
5
6
7
def reconstructQueue(people):
    # Sort: tallest first; among same height, smaller k first
    people.sort(key=lambda p: (-p[0], p[1]))
    queue = []
    for person in people:
        queue.insert(person[1], person)
    return queue

Walkthrough: [[7,0], [4,4], [7,1], [5,0], [6,1], [5,2]]

After sorting by (-height, k): [[7,0], [7,1], [6,1], [5,0], [5,2], [4,4]]

Insert step by step:

  • Insert [7,0] at index 0: [[7,0]]
  • Insert [7,1] at index 1: [[7,0], [7,1]]
  • Insert [6,1] at index 1: [[7,0], [6,1], [7,1]]
  • Insert [5,0] at index 0: [[5,0], [7,0], [6,1], [7,1]]
  • Insert [5,2] at index 2: [[5,0], [7,0], [5,2], [6,1], [7,1]]
  • Insert [4,4] at index 4: [[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]]

Verify the final queue [[5,0], [7,0], [5,2], [6,1], [4,4], [7,1]] by counting people in front who are taller or equal in height. [5,0]: nobody in front. [7,0]: nobody in front. [5,2]: [5,0] (equal) and [7,0] (taller), that's 2. [6,1]: [7,0] (taller), that's 1. [4,4]: [5,0], [7,0], [5,2], [6,1] are all at least 4, that's 4. [7,1]: [7,0] (equal), that's 1. All k-values check out.

The sorting order is the entire algorithm. Without the right comparator, the greedy insertion strategy breaks down completely.

Complexity

Both sorted() and .sort() use Timsort, so the time complexity is O(nlogn)O(n \log n) regardless of how the comparator is defined. Each comparison call is O(1)O(1) for numeric keys and O(k)O(k) for string concatenation comparisons, where k is the average number of digits. Space complexity is O(n)O(n) for the auxiliary array produced by sorted(), or O(logn)O(\log n) for the in-place .sort() (stack space for Timsort's merge passes).

When the comparison itself is expensive, for instance comparing long strings, the total cost becomes O(nlognk)O(n \log n · k). In most interview problems, k is small enough that this doesn't matter, but it's worth noting if strings can be arbitrarily long.

Recognizing this pattern

You're probably looking at custom comparator sort when:

  • The natural sort order does not produce the desired result, so you need a non-obvious key combining two values.
  • The greedy strategy depends entirely on the ordering, like queue reconstruction or schedule assembly.
  • The comparison rule is asymmetric or non-numeric, such as concatenation order between strings.
  • Phrasing mentions "reconstruct the queue", "largest number you can form", or "order by custom rule".

Common templates:

  • Pairwise concatenation rule: sort by a + b vs b + a comparison. Example: Largest Number.
  • Tall-first insertion: sort by (-height, k) then insert at index k. Example: Queue Reconstruction by Height.
  • Sort by deadline or end time: greedy scheduling by interval finish or due date. Example: Course Schedule III.
  • Stable secondary key: sort by primary key, break ties with a meaningful secondary like input order. Example: Sort the People.

Practice Problems (95)

179 Largest Number
324 Wiggle Sort II
406 Queue Reconstruction by Height
506 Relative Ranks
539 Minimum Time Difference
561 Array Partition
628 Maximum Product of Three Numbers
791 Custom Sort String
870 Advantage Shuffle
899 Orderly Queue
937 Reorder Data in Log Files
954 Array of Doubled Pairs
1005 Maximize Sum Of Array After K Negations
1029 Two City Scheduling
1051 Height Checker
1090 Largest Values From Labels
1122 Relative Sort Array
1200 Minimum Absolute Difference
1329 Sort the Matrix Diagonally
1331 Rank Transform of an Array
1333 Filter Restaurants by Vegan-Friendly, Price and Distance
1356 Sort Integers by The Number of 1 Bits
1366 Rank Teams by Votes
1403 Minimum Subsequence in Non-Increasing Order
1433 Check If a String Can Break Another String
1451 Rearrange Words in a Sentence
1465 Maximum Area of a Piece of Cake After Horizontal and Vertical Cuts
1471 The k Strongest Values in an Array
1502 Can Make Arithmetic Progression From Sequence
1509 Minimum Difference Between Largest and Smallest Value in Three Moves
1536 Minimum Swaps to Arrange a Binary Grid
1561 Maximum Number of Coins You Can Get
1604 Alert Using Same Key-Card Three or More Times in a One Hour Period
1605 Find Valid Matrix Given Row and Column Sums
1619 Mean of Array After Removing Some Elements
1630 Arithmetic Subarrays
1636 Sort Array by Increasing Frequency
1637 Widest Vertical Area Between Two Points Containing No Points
1657 Determine if Two Strings Are Close
1665 Minimum Initial Energy to Finish Tasks
1686 Stone Game VI
1710 Maximum Units on a Truck
1727 Largest Submatrix With Rearrangements
1840 Maximum Building Height
1846 Maximum Element After Decreasing and Rearranging
1847 Closest Room
1887 Reduction Operations to Make the Array Elements Equal
1913 Maximum Product Difference Between Two Pairs
1968 Array With Elements Not Equal to Average of Neighbors
1982 Find Array Given Subset Sums
1996 The Number of Weak Characters in the Game
2033 Minimum Operations to Make a Uni-Value Grid
2054 Two Best Non-Overlapping Events
2126 Destroying Asteroids
2136 Earliest Possible Day of Full Bloom
2144 Minimum Cost of Buying Candies With Discount
2164 Sort Even and Odd Indices Independently
2165 Smallest Value of the Rearranged Number
2171 Removing Minimum Number of Magic Beans
2191 Sort the Jumbled Numbers
2195 Append K Integers With Minimal Sum
2197 Replace Non-Coprime Numbers in Array
2231 Largest Number After Digit Swaps by Parity
2274 Maximum Consecutive Floors Without Special Floors
2279 Maximum Bags With Full Capacity of Rocks
2294 Partition Array Such That Maximum Difference Is K
2332 The Latest Time to Catch a Bus
2418 Sort the People
2449 Minimum Number of Operations to Make Arrays Similar
2500 Delete Greatest Value in Each Row
2545 Sort the Students by Their Kth Score
2551 Put Marbles in Bags
2567 Minimum Score by Changing Two Elements
2578 Split With Minimum Sum
2587 Rearrange Array to Maximize Prefix Score
2785 Sort Vowels in a String
2931 Maximum Spending After Buying Items
2966 Divide Array Into Arrays With Max Difference
3025 Find the Number of Ways to Place People I
3027 Find the Number of Ways to Place People II
3273 Minimum Amount of Damage Dealt to Bob
3301 Maximize the Total Height of Unique Towers
3397 Maximum Number of Distinct Elements After Operations
3424 Minimum Cost to Make Arrays Identical
3446 Sort Matrix by Diagonals
3457 Eat Pizzas!
3551 Minimum Swaps to Sort by Digit Sum
3727 Maximum Alternating Sum of Squares
3732 Maximum Product of Three Elements After One Replacement
3745 Maximize Expression of Three Elements
3759 Count Elements With at Least K Greater Values
3769 Sort Integers by Binary Reflection
3774 Absolute Difference Between Maximum and Minimum K Elements
3776 Minimum Moves to Balance Circular Array
3780 Maximum Sum of Three Numbers Divisible by Three