Sorting & Comparators
Learn built-in sort APIs, custom comparators, sort stability, and sort-then-scan patterns that simplify problems by imposing order on data.
Learn & Reference
Understanding the Pattern
Sorting & Comparators
Sorting is one of the most powerful preprocessing steps in programming. Many problems that seem complex become straightforward once the data is in order. Understanding how to use built-in sort APIs, write custom comparators, and apply sort-then-scan patterns is essential for both interviews and real-world code.
Built-in Sort APIs
Every language provides a highly optimized sort function. These built-in sorts use hybrid algorithms (Timsort in Python/Java/JavaScript, Introsort in Go/C++) that adapt to the structure of your data. They are almost always faster than anything you would write by hand.
Key facts about built-in sorts:
- Python:
sorted()returns a new list;list.sort()sorts in-place. Both use Timsort. - JavaScript/TypeScript:
Array.sort()sorts in-place and converts elements to strings by default — always pass a comparator for numbers. - Java:
Arrays.sort()uses dual-pivot quicksort for primitives and Timsort for objects.Collections.sort()uses Timsort. - Go:
sort.Slice()andslices.SortFunc()use pattern-defeating quicksort (pdqsort).
Custom Comparators
A comparator tells the sort function how to order two elements. This is how you sort by custom criteria — by length, by a specific field, in reverse order, or by multiple keys.
Comparator contract: Given elements a and b:
- Return negative if
ashould come beforeb - Return zero if
aandbare equal - Return positive if
ashould come afterb
Key functions (Python) vs comparators (other languages):
- Python's
key=parameter is often simpler — you provide a function that extracts a sort key, and Python compares keys naturally - Other languages use explicit comparator functions that compare two elements directly
Multi-key sorting
Sort by primary key first, then by secondary key for ties. This is extremely common in interviews:
- Sort intervals by start time, then by end time
- Sort people by age, then alphabetically by name
- Sort files by size descending, then by name ascending
Sort Stability
A stable sort preserves the relative order of elements that compare as equal. This matters when:
- You sort by one key, then sort again by another key — stability preserves the first sort for ties
- You need deterministic output order in interviews
Stable: Python (Timsort), Java (Timsort for objects), JavaScript (Timsort in V8)
Not guaranteed stable: Java (primitives use quicksort), Go (use slices.SortStableFunc for stability)
Sort-Then-Scan Patterns
The most common interview technique with sorting: sort the data first, then solve the problem with a single linear scan.
Pattern: Sort + one pass = O(n log n) total time
Examples:
- Find duplicates: sort, then check adjacent elements
- Merge intervals: sort by start, then merge overlapping pairs
- Two sum (sorted): sort, then use two pointers
- Meeting rooms: sort by start time, scan for conflicts
- Largest number: sort with custom comparator, concatenate
Lambda / Anonymous Comparators
Modern languages support inline comparator definitions. This keeps sort logic close to where it is used:
- Python:
key=lambda x: x[1]orcmp_to_key(lambda a, b: ...) - JavaScript/TypeScript:
arr.sort((a, b) => a - b) - Java:
Arrays.sort(arr, (a, b) -> a[0] - b[0])orComparator.comparing(...) - Go:
slices.SortFunc(s, func(a, b T) int { ... })
Common Sort Algorithms (Conceptual)
You rarely implement these from scratch, but understanding them helps explain complexity and behavior:
- Insertion sort — O(n²) worst, O(n) on nearly-sorted data. Stable. Used inside Timsort for small subarrays.
- Merge sort — O(n log n) always. Stable. O(n) extra space. Timsort is derived from merge sort.
- Quicksort — O(n log n) average, O(n²) worst. Not stable. O(log n) stack space. Fast in practice due to cache efficiency.
- Counting/Radix sort — O(n + k) or O(n * d). Not comparison-based. Useful when values have a small range.
Common Mistakes
- JavaScript default sort is lexicographic:
[10, 2, 1].sort()gives[1, 10, 2]— always use(a, b) => a - bfor numbers - Comparator overflow with subtraction:
a - bcan overflow for large integers in Java/Go — useInteger.compare(a, b)or explicit conditionals instead - Forgetting stability requirements: if you need stable sort in Go, use
slices.SortStableFuncexplicitly - Sorting when a partial sort suffices: if you only need the k-th smallest element, use a heap or quickselect (O(n)) instead of full sort (O(n log n))
- Mutating the input array unexpectedly: in-place sorts modify the original array — copy first if you need to preserve the original order
- Not handling equal elements in comparators: a valid comparator must be consistent — if
compare(a, b) < 0, thencompare(b, a) > 0; violating this causes undefined behavior - Sorting objects without a clear key: always define exactly what field(s) you are sorting by before writing the comparator
When to Use
Template
1# --- Basic Sort ---2nums = [3, 1, 4, 1, 5]3nums.sort() # in-place ascending4sorted_nums = sorted(nums) # returns new sorted list56# --- Sort with Custom Key ---7words = ["banana", "apple", "cherry"]8words.sort(key=len) # by length9words.sort(key=lambda w: w[-1]) # by last character1011# --- Sort-Then-Scan Pattern ---12def solve(nums):13nums.sort() # step 1: sort14result = []15for i in range(len(nums)): # step 2: linear scan16# process sorted data17pass18return result1920# --- Descending Sort ---21nums.sort(reverse=True)2223# --- Multi-key Sort ---24pairs = [(1, 'b'), (2, 'a'), (1, 'a')]25pairs.sort(key=lambda x: (x[0], x[1])) # [(1, 'a'), (1, 'b'), (2, 'a')]
Syntax Reference
1# === Built-in Sort ===2nums = [3, 1, 4, 1, 5]3sorted_nums = sorted(nums) # new list: [1, 1, 3, 4, 5]4nums.sort() # in-place sort56# === Reverse Sort ===7sorted(nums, reverse=True) # [5, 4, 3, 1, 1]8nums.sort(reverse=True)910# === Key Function ===11sorted(words, key=len) # sort by length12sorted(words, key=str.lower) # case-insensitive13sorted(pairs, key=lambda x: x[1]) # sort by second element1415# === Multi-key Sort ===16sorted(people, key=lambda x: (x.age, x.name))17# age ascending, then name ascending for ties1819# === Custom Comparator (rare in Python) ===20from functools import cmp_to_key21def compare(a, b):22# return negative if a < b, 0 if equal, positive if a > b23return a - b24sorted(nums, key=cmp_to_key(compare))2526# === Stable Sort ===27# Python sort is always stable (Timsort)28# Sort by secondary key first, then primary key29data.sort(key=lambda x: x.name) # secondary30data.sort(key=lambda x: x.age) # primary (stability preserves name order)
Common Recipes
1# --- Sort-Then-Scan: Find Duplicates ---2def find_duplicates(nums):3nums.sort()4result = []5for i in range(1, len(nums)):6if nums[i] == nums[i - 1]:7result.append(nums[i])8return result910# --- Merge Overlapping Intervals ---11def merge_intervals(intervals):12intervals.sort(key=lambda x: x[0])13merged = [intervals[0]]14for start, end in intervals[1:]:15if start <= merged[-1][1]:16merged[-1][1] = max(merged[-1][1], end)17else:18merged.append([start, end])19return merged2021# --- Sort by Multiple Keys ---22def sort_students(students):23# Sort by grade descending, then by name ascending24return sorted(students, key=lambda s: (-s['grade'], s['name']))2526# --- Custom String Sort (Largest Number) ---27from functools import cmp_to_key28def largest_number(nums):29strs = list(map(str, nums))30strs.sort(key=cmp_to_key(lambda a, b: -1 if a+b > b+a else 1))31return ''.join(strs).lstrip('0') or '0'3233# --- Sort + Two Pointers: Minimum Difference ---34def min_diff(nums):35nums.sort()36return min(nums[i+1] - nums[i] for i in range(len(nums) - 1))
Complexity
| Algorithm | Best | Average | Worst | Space | Stable? |
|---|---|---|---|---|---|
| Timsort (Python/JS/Java objects) | O(n) | O(n log n) | O(n log n) | O(n) | Yes |
| Introsort (C++/Go pdqsort) | O(n log n) | O(n log n) | O(n log n) | O(log n) | No |
| Dual-pivot Quicksort (Java primitives) | O(n log n) | O(n log n) | O(n²) | O(log n) | No |
| Merge sort | O(n log n) | O(n log n) | O(n log n) | O(n) | Yes |
| Quicksort | O(n log n) | O(n log n) | O(n²) | O(log n) | No |
| Insertion sort | O(n) | O(n²) | O(n²) | O(1) | Yes |
| Counting sort | O(n + k) | O(n + k) | O(n + k) | O(k) | Yes |
| Radix sort | O(n × d) | O(n × d) | O(n × d) | O(n + k) | Yes |
Watch Out
- ✗JavaScript default sort is lexicographic: `[10, 2, 1].sort()` gives `[1, 10, 2]` — always use `(a, b) => a - b` for numbers
- ✗Comparator overflow with subtraction: `a - b` can overflow for large integers in Java/Go — use `Integer.compare(a, b)` or explicit conditionals instead
- ✗Forgetting stability requirements: if you need stable sort in Go, use `slices.SortStableFunc` explicitly
- ✗Sorting when a partial sort suffices: if you only need the k-th smallest element, use a heap or quickselect (O(n)) instead of full sort (O(n log n))
- ✗Mutating the input array unexpectedly: in-place sorts modify the original array — copy first if you need to preserve the original order
- ✗Not handling equal elements in comparators: a valid comparator must be consistent — if `compare(a, b) < 0`, then `compare(b, a) > 0`; violating this causes undefined behavior
- ✗Sorting objects without a clear key: always define exactly what field(s) you are sorting by before writing the comparator