Swift Sort and Custom Comparators: The Coding Interview Reference

May 29, 20269 min read
dsaalgorithmsinterview-prepswift
Swift Sort and Custom Comparators: The Coding Interview Reference
TL;DR
  • sort() mutates in place (requires var), while sorted() returns a new array and works on let constants
  • 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 Comparable by implementing only static func <: Swift derives >, <=, and >= through the protocol extension
  • Default string comparison is case-sensitive: uppercase sorts before lowercase; use localizedCaseInsensitiveCompare when 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:

  1. Irreflexive: f(a, a) is always false
  2. Transitive: if f(a, b) and f(b, c), then f(a, c)
  3. Transitive incomparability: if a and b are equivalent, and b and c are, then a and c must 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 wantCode
Sort array in place, ascendingarr.sort()
Sort array in place, descendingarr.sort(by: >)
New sorted arrayarr.sorted()
Sort by property, ascendingarr.sort { $0.x < $1.x }
Sort by property, descendingarr.sort { $0.x > $1.x }
Multi-key sortarr.sort { ($0.a, $0.b) < ($1.a, $1.b) }
Multi-key mixed directionarr.sort { ($1.a, $0.b) < ($0.a, $1.b) }
Case-insensitive string sortarr.sort { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
Conform type for sort() with no argsImplement 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