With over a decade in software engineering, you quickly realize that sorting isn’t just a hurdle for technical interviews—it is a fundamental system design decision. Choosing the wrong sorting algorithm can lead to memory spikes, UI frame drops, and battery drain, especially on mobile devices.
Most modern languages (including Swift) abstract sorting behind a simple .sort() method. However, understanding the underlying mechanisms is critical when building custom data pipelines, handling massive data sets, or optimizing for specific input distributions.
Here is a pragmatic guide to the core sorting mechanisms, their time complexities, and exactly when to deploy them in production.
The Time Complexity Cheat Sheet
Before diving into the “why,” here is the fundamental “what.”
| Algorithm | Best Case | Average Case | Worst Case | Space Complexity | Stable? |
|---|---|---|---|---|---|
| Insertion Sort | O(N) | O(N²) | O(N²) | O(1) | Yes |
| Merge Sort | O(N log N) | O(N log N) | O(N log N) | O(N) | Yes |
| Quick Sort | O(N log N) | O(N log N) | O(N²) | O(log N) | No |
| Heap Sort | O(N log N) | O(N log N) | O(N log N) | O(1) | No |
| TimSort | O(N) | O(N log N) | O(N log N) | O(N) | Yes |
1. Quick Sort: The General Purpose Workhorse
Quick Sort is a divide-and-conquer algorithm that picks a “pivot” and partitions the array into elements smaller and larger than the pivot.
- When to use it: When you need a fast, cache-friendly, general-purpose in-memory sort and don’t care about stability (maintaining the relative order of equal elements).
- Why to use it: Despite its O(N²) worst-case, an optimized Quick Sort (like Introsort) with randomized pivot selection is practically the fastest sorting algorithm for arrays due to spatial locality—it plays very nicely with CPU caches.
- The Catch: It is unstable. If you sort an array of
Employeeobjects byDepartment, and then sort bySalary, theDepartmentgrouping will be destroyed.
2. Merge Sort: The Stable Giant
Merge Sort divides the array into halves, recursively sorts them, and then merges the sorted halves back together.
- When to use it: When you are sorting Linked Lists, when you require stability, or when the data set is too large to fit into RAM (External Sorting).
- Why to use it: It guarantees O(N log N) time regardless of the input. Because it accesses data sequentially, it is perfect for Linked Lists (which lack random access) and for merging chunks of data stored on disk.
- The Catch: It requires O(N) auxiliary space. If you are memory-constrained on an iOS device, allocating a second massive array just to sort the first one can trigger a memory warning.
3. Insertion Sort: The Micro-Optimizer
Insertion Sort builds the final sorted array one item at a time, picking an element and inserting it into its correct position among the previously sorted elements.
- When to use it: When the dataset is very small (usually < 50 elements) or when the data is nearly sorted.
- Why to use it: If you have a live
UITableViewdata source that is already sorted, and a user adds one new item, Insertion Sort will place it in O(N) time. It avoids the overhead of recursive calls seen in Quick/Merge sort. - The Catch: It is catastrophic for large, randomized datasets (O(N²)).
4. Heap Sort: The Memory Miser
Heap Sort converts the array into a binary max-heap, repeatedly extracting the maximum element to build the sorted array at the end of the heap.
- When to use it: When you have strict memory limitations but still need a guaranteed O(N log N) worst-case time complexity.
- Why to use it: It sorts in-place (O(1) space) and doesn’t suffer from Quick Sort’s O(N²) worst-case. It is heavily used in embedded systems or core OS kernels where memory allocation is expensive or restricted.
- The Catch: It is incredibly cache-unfriendly. It jumps around the array constantly, causing CPU cache misses, making it practically slower than Quick Sort for most applications.
5. TimSort: The Modern Standard
TimSort is a hybrid algorithm derived from Merge Sort and Insertion Sort. It looks for subsets of the data that are already ordered (“runs”) and uses that to its advantage.
- When to use it: Almost everywhere. It is the default sorting algorithm in Swift (as of Swift 5), Python, and Java.
- Why to use it: Real-world data is rarely purely random; it often contains partially sorted sequences. TimSort capitalizes on this, delivering O(N) performance on nearly-sorted data while maintaining a stable O(N log N) worst-case and stability.
Senior Takeaway: What does Swift use?
When you call myArray.sorted() in Swift, you aren’t just getting one algorithm.
Historically, Apple used IntroSort (a hybrid that starts with QuickSort and switches to HeapSort if the recursion goes too deep, and InsertionSort for small partitions). More recently, Swift transitioned to a modified TimSort, ensuring that your UI data remains stable and benefits from the partially-sorted nature of real-world user data.
1. Quick Sort (In-Place)
This implementation uses the Lomuto partition scheme. By passing the array as inout, we avoid the massive memory overhead of creating new arrays during every recursive call.
extension Array where Element: Comparable {
mutating func quickSort(low: Int, high: Int) {
if low < high {
let pivotIndex = partition(low: low, high: high)
quickSort(low: low, high: pivotIndex - 1)
quickSort(low: pivotIndex + 1, high: high)
}
}
private mutating func partition(low: Int, high: Int) -> Int {
let pivot = self[high]
var i = low
for j in low..<high {
if self[j] <= pivot {
self.swapAt(i, j)
i += 1
}
}
self.swapAt(i, high)
return i
}
}
// Usage:
// var numbers = [10, 7, 8, 9, 1, 5]
// numbers.quickSort(low: 0, high: numbers.count - 1)
2. Merge Sort (Out-of-Place)
Merge Sort inherently requires extra space, so this returns a new sorted array rather than mutating the original.
func mergeSort<T: Comparable>(_ array: [T]) -> [T] {
guard array.count > 1 else { return array }
let middleIndex = array.count / 2
let leftArray = mergeSort(Array(array[0..<middleIndex]))
let rightArray = mergeSort(Array(array[middleIndex..<array.count]))
return merge(left: leftArray, right: rightArray)
}
private func merge<T: Comparable>(left: [T], right: [T]) -> [T] {
var leftIndex = 0
var rightIndex = 0
var orderedArray: [T] = []
orderedArray.reserveCapacity(left.count + right.count) // Optimization
while leftIndex < left.count && rightIndex < right.count {
if left[leftIndex] < right[rightIndex] {
orderedArray.append(left[leftIndex])
leftIndex += 1
} else if left[leftIndex] > right[rightIndex] {
orderedArray.append(right[rightIndex])
rightIndex += 1
} else {
orderedArray.append(left[leftIndex])
leftIndex += 1
orderedArray.append(right[rightIndex])
rightIndex += 1
}
}
orderedArray.append(contentsOf: left[leftIndex...])
orderedArray.append(contentsOf: right[rightIndex...])
return orderedArray
}
3. Insertion Sort (In-Place)
This is brilliant for small, nearly sorted datasets. It iterates through the array, pulling elements backwards into their correct sorted position.
extension Array where Element: Comparable {
mutating func insertionSort() {
guard self.count > 1 else { return }
for i in 1..<self.count {
var y = i
let temp = self[y]
while y > 0 && temp < self[y - 1] {
self[y] = self[y - 1] // Shift right
y -= 1
}
self[y] = temp
}
}
}
4. Heap Sort (In-Place)
Heap sort is complex to read but guarantees O(N log N) with O(1) space. It relies on building a Max-Heap structure out of the flat array.
extension Array where Element: Comparable {
mutating func heapSort() {
let n = self.count
// Build max heap
for i in stride(from: n / 2 - 1, through: 0, by: -1) {
heapify(n: n, i: i)
}
// Extract elements from heap one by one
for i in stride(from: n - 1, through: 0, by: -1) {
self.swapAt(0, i) // Move current root to end
heapify(n: i, i: 0) // Call max heapify on the reduced heap
}
}
private mutating func heapify(n: Int, i: Int) {
var largest = i
let left = 2 * i + 1
let right = 2 * i + 2
if left < n && self[left] > self[largest] {
largest = left
}
if right < n && self[right] > self[largest] {
largest = right
}
if largest != i {
self.swapAt(i, largest)
heapify(n: n, i: largest)
}
}
}
A Note on TimSort
You do not need to write out TimSort for the article. Because it is a highly complex hybrid of Merge and Insertion sort, writing it from scratch takes hundreds of lines of code. It is best to conclude the article by showing exactly how Apple implements it under the hood when a developer simply calls:
// This natively triggers Swift's highly optimized, stable TimSort variant
let sortedArray = myArray.sorted()