Swift Sort and Custom Comparators: The Coding Interview Reference

sort()mutates in place (requiresvar), whilesorted()returns a new array and works onletconstants- Never use
<=or>=in a Swift custom comparator: it breaks strict weak ordering, causing a debug crash or silently wrong output in release - The tuple trick
{ ($0.a, $0.b) < ($1.a, $1.b) }handles multi-key sorting in one expression with no nested conditionals - Swift sort has been stable since Swift 5.0, so sequential sort passes chain correctly for multi-key ordering
- Conform to
Comparableby implementing onlystatic func <: Swift derives>,<=, and>=through the protocol extension - Default string comparison is case-sensitive: uppercase sorts before lowercase; use
localizedCaseInsensitiveComparewhen you need case-insensitive ordering
You write the sort. It looks correct. You run it in debug mode and get a precondition failure. You switch to release mode and it doesn't crash anymore, just silently produces garbage output. You spend forty minutes staring at a closure that is one character away from working. That character is =.
This covers the two sort methods, every custom comparator pattern you'll need, the multi-key tuple trick, and the exact rule you will almost certainly violate on your first attempt. For a broader view of Swift's interview-relevant standard library, start with the Swift for Coding Interviews overview.
sort() vs sorted(): Pick the Right One
Two methods. One mutates. One doesn't.
var nums = [3, 1, 4, 1, 5, 9] // Mutates in place (array must be `var`) nums.sort() // [1, 1, 3, 4, 5, 9] // Returns a new array (works on `let` too) let sorted = nums.sorted() // [1, 1, 3, 4, 5, 9]
The most common mistake is calling sort() on a let constant. Swift won't compile it. If you can't change the declaration, use sorted(). If you don't need the original array, use sort() and skip the allocation.
Both run in O(n log n). Both are stable since Swift 5.0.
Writing a Custom Comparator
The by: parameter takes a closure that accepts two elements and returns true if the first should come before the second.
// Descending order nums.sort(by: { $0 > $1 }) // Same thing, operator shorthand nums.sort(by: >) // Ascending, explicit names nums.sort(by: { a, b in a < b })
When the closure is a single operator, pass it directly. sort(by: <) and sort(by: >) are idiomatic Swift and you will feel like a genius writing them in an interview.
For a struct, write the closure against the property you care about:
struct Task { let name: String let priority: Int } var tasks = [ Task(name: "Deploy", priority: 2), Task(name: "Review PR", priority: 1), Task(name: "Fix bug", priority: 3), ] tasks.sort(by: { $0.priority < $1.priority }) // ascending tasks.sort(by: { $0.priority > $1.priority }) // descending
The Rule You Cannot Break: Strict Weak Ordering
Here is the trap that ends people.
Swift's sort requires your comparator to be a strict weak ordering. Three conditions:
- Irreflexive:
f(a, a)is alwaysfalse - Transitive: if
f(a, b)andf(b, c), thenf(a, c) - Transitive incomparability: if
aandbare equivalent, andbandcare, thenaandcmust be too
The bug people hit is using <= instead of <:
// WRONG: precondition failure in debug, silent corruption in release tasks.sort(by: { $0.priority <= $1.priority }) // CORRECT tasks.sort(by: { $0.priority < $1.priority })
Using <= breaks irreflexivity because a <= a is always true. You are telling Timsort that element a comes before itself. Debug builds catch this immediately with a precondition failure. Release builds do not catch it. They silently produce wrong output. You only find out when a user reports that their list is out of order and you have no crash log to look at.
This is the programming equivalent of pulling the pin out of a grenade, realizing your mistake, and carefully putting the grenade back in your pocket. The debug build explodes immediately. The release build waits until the worst moment.
Same trap on the descending side:
// WRONG tasks.sort(by: { $0.priority >= $1.priority }) // CORRECT tasks.sort(by: { $0.priority > $1.priority })
The fix is always the same: strict < or >, never <= or >=. This rule applies in every language. The C++ sort and custom comparator guide covers the same trap in std::sort, where the violation produces undefined behavior rather than a clean crash, which is even more fun.
Multi-Key Sorting: The Tuple Trick
This is where Swift earns its reputation for expressiveness.
Swift tuples are Comparable as long as their elements are, and comparison is lexicographic. That means you can express a multi-criteria sort in one line:
// Sort by priority ascending, then by name alphabetically tasks.sort(by: { ($0.priority, $0.name) < ($1.priority, $1.name) })
Swift compares the first element. If equal, it moves to the second. That's the whole thing.
To mix ascending and descending on different keys, swap which side provides which field:
// Primary: priority descending. Secondary: name ascending. tasks.sort(by: { ($1.priority, $0.name) < ($0.priority, $1.name) })
For the priority key, writing $1.priority < $0.priority means higher priority comes first. For the name key, $0.name < $1.name means alphabetical order. Both expressed in one comparison, zero nested if statements.
The tuple approach eliminates the pyramid of conditionals you'd otherwise build with three or more sort keys. The pyramid being the thing that starts as three lines and ends as a twenty-line function that nobody wants to touch.
Conforming a Type to Comparable
If you sort the same type in more than one place, conforming to Comparable is cleaner than copying a closure everywhere. Implement < and ==, and Swift derives >, <=, and >= for free.
struct Task: Comparable { let name: String let priority: Int static func < (lhs: Task, rhs: Task) -> Bool { lhs.priority < rhs.priority } static func == (lhs: Task, rhs: Task) -> Bool { lhs.priority == rhs.priority && lhs.name == rhs.name } } var tasks: [Task] = [...] tasks.sort() // uses <, ascending by priority
You only need to implement <. Swift synthesizes the rest through the protocol extension. The one requirement: == must be consistent with <. If a == b, then neither a < b nor b < a should be true. Violating this is another path to corrupted output with no crash to guide you.
For simple structs where every stored property is Comparable, Swift synthesizes both protocols automatically, sorting lexicographically by declaration order:
struct Point: Comparable { let x: Int let y: Int // Swift synthesizes < and == (compares x first, then y) }
String Sorting Gotchas
Strings are Comparable in Swift, and sorting them looks innocent:
let words = ["banana", "Apple", "cherry"] print(words.sorted()) // ["Apple", "banana", "cherry"]
That output surprises most people. Swift's default string comparison is case-sensitive, and uppercase letters sort before lowercase. "A" is ASCII 65. "b" is ASCII 98. So "Apple" comes first. Your users will notice.
For case-insensitive sorting:
let sorted = words.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
For locale-aware sorting (anything that ends up on a screen):
let sorted = words.sorted { $0.localizedCompare($1) == .orderedAscending }
For interview inputs, which are ASCII, the default < is fine. Just know the rule before an interviewer asks why "Zebra" sorted before "apple."
Sort Stability: It's Guaranteed
Swift's sort() has been stable since Swift 5.0. The algorithm is a modified Timsort, combining merge sort for large runs with insertion sort for small ones. Both are stable, so the merged result is stable.
Stable means equal elements preserve their original relative order. This matters when you sort by one key and want a secondary sort expressed as a prior sort pass:
// Sort by name first tasks.sort(by: { $0.name < $1.name }) // Then sort by priority. Ties keep name order from the first sort. tasks.sort(by: { $0.priority < $1.priority })
For a full treatment of why this matters in multi-pass sorting, see Stable Sort.
The Only Reference You Need
| What you want | Code |
|---|---|
| Sort array in place, ascending | arr.sort() |
| Sort array in place, descending | arr.sort(by: >) |
| New sorted array | arr.sorted() |
| Sort by property, ascending | arr.sort { $0.x < $1.x } |
| Sort by property, descending | arr.sort { $0.x > $1.x } |
| Multi-key sort | arr.sort { ($0.a, $0.b) < ($1.a, $1.b) } |
| Multi-key mixed direction | arr.sort { ($1.a, $0.b) < ($0.a, $1.b) } |
| Case-insensitive string sort | arr.sort { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending } |
Conform type for sort() with no args | Implement Comparable with static func < |
Traps Checklist
Before you submit any sort in an interview, run through this list:
<= or >= in the comparator? Replace with < or >. This is how you get a precondition failure at the worst possible time.
Calling sort() on a let? Switch to sorted() or change the declaration to var. The compiler will catch this, but it's embarrassing to debug during an interview.
Comparing floating-point values? Watch for NaN. NaN < NaN is false and NaN > NaN is false, so two NaN values are considered incomparable and can land anywhere in the output.
Assuming case-insensitive string order? Use localizedCaseInsensitiveCompare if you need it.
Returning true for equal elements in a custom Comparable? That breaks the requirement that a == b implies neither a < b nor b < a. Use a tiebreaker or return false.
Getting It Right Under Pressure
Reading your comparator out loud is the fastest debug technique. For { $0.priority < $1.priority }, say: "left priority comes before right priority when left is smaller, so smaller values appear first." If that's not what you want, flip the operator.
For multi-key tuples, read element by element. "Compare first by priority ascending, then by name ascending." Then check whether you've swapped anything for descending. The mental model that actually holds under pressure: the comparator returns true when the left argument should appear earlier in the final array. Not "should come first compared to," not "is less than." Earlier in the output. Say that out loud and you will catch your own bugs.
Narrating your comparator out loud matters more than memorizing the syntax. The syntax is a three-second lookup. Reasoning correctly about ordering under time pressure is a trained skill, not a recalled fact. That gap between knowing the API and explaining it clearly to someone who's watching is exactly what SpaceComplexity trains, through voice-based mock interviews where you explain your comparator to an interviewer who will absolutely ask why.
Further Reading
- Swift sorted(by:) documentation: official reference with complexity guarantees
- Timsort on Wikipedia: the algorithm behind Swift's sort since 5.0
- Stable Sort in Swift (Sarunw): history of the transition from introsort
- Sorting Swift collections (Swift by Sundell): broader patterns and key path sorting
- Swift Forums: Is sort() stable in Swift 5?: official confirmation thread