Swift
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Optionals
-
Section 4: Object-Oriented and Value Types
-
Section 5: Memory Management
-
Section 6: Generics and Error Handling
-
Section 7: Concurrency
-
Section 8: Working with Collections
-
Section 9: Codable and Data Handling
-
Section 10: Protocol-Oriented Programming
-
Section 11: Testing and Tooling
-
Section 12: Practical Projects
-
Section 13: Interview Practice
-
Section 14: More Practice Exercises
-
Section 15: More Standard Library
-
Section 16: Advanced Concurrency
-
Section 17: Foundation Framework Deep Dive
-
Section 18: URLSession and Networking Deep Dive
-
Section 19: Combine Framework
-
Section 20: SwiftUI Fundamentals for Swift Developers
-
Section 21: Server-Side Swift with Vapor
-
Section 22: Swift Package Manager Deep Dive
-
Section 23: Swift Concurrency Deep Dive
-
Section 24: More Language Features
-
Section 25: Error Handling Deep Dive
-
Section 26: Testing Deep Dive
-
Section 27: Data Structures and Algorithms in Swift
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Swift Macros (Swift 5.9+)
-
Section 31: Property Wrappers Ecosystem
-
Section 32: Swift Interop Deep Dive
-
Section 33: iOS App Architecture Patterns
-
Section 34: Performance and Debugging
-
Section 35: App Distribution and CI/CD
-
Section 36: More Practical Projects
-
Section 37: SwiftData and Persistence
-
Section 38: More Design Patterns
-
Section 39: More Review and Practice
-
Section 40: More Foundation Deep Dive
-
Section 41: Advanced Collections in Swift
-
Section 42: Advanced Generics Practice
-
Section 43: UIKit for Legacy and Hybrid Apps
-
Section 44: watchOS and visionOS Development Basics
-
Section 45: More Networking Patterns
-
Section 46: More Testing Practice
-
Section 47: Accessibility in Swift Apps
-
Section 48: Localization
-
Section 49: More Practical Projects Round 2
-
Section 50: Swift Charts Framework
-
Section 51: More Interview and Algorithm Practice
-
Section 52: Final Practice and Mastery
-
Section 53: Swift Compiler and Build System
-
Section 54: More Concurrency Practice
-
Section 55: App Store Guidelines and Review
-
Section 56: More Design and Architecture
124: Implementing Common Sorting Algorithms
I once worked with a developer who had implemented a nested loop to sort a list of a few hundred financial transactions in a fintech app. On his high-end MacBook Pro using the simulator, it felt instantaneous. But when the app hit the hands of a user on a five-year-old iPhone with a few thousand transactions? The UI thread just hung. He hadn't realized he'd accidentally written a Bubble Sort, and the $O(n^2)$ complexity had finally caught up with him. While Swift's built-in .sort() is incredibly optimized (it uses a hybrid algorithm called Timsort), there are times—whether it's a technical interview, a specialized memory constraint, or building a sorting visualizer—where you need to implement these yourself.
The Naive Approach and Why It Fails
If you're just starting to think about sorting, you'll likely land on Bubble Sort. It's intuitive: you walk through the list, compare adjacent elements, and swap them if they're in the wrong order. You keep doing this until the largest element "bubbles" up to the end. It's simple to write, but it's devastatingly slow for large datasets.
func bubbleSort<T: Comparable>(_ array: inout [T]) {
let n = array.count
for i in 0<n {
for j in 0<(n - i - 1) {
if array[j] > array[j + 1] {
array.swapAt(j, j + 1)
}
}
}
}
I usually tell people to treat Bubble Sort as a cautionary tale. The nested loops mean that if you double your input size, the work doesn't double—it quadruples. In a professional environment, if I see this in a PR, I'm going to ask why it's there unless we're dealing with a list of maybe five items where simplicity outweighs performance.
Efficiency Through Divide and Conquer
To get real performance, we have to stop looking at the list as one big block and start breaking it down. Quick Sort is the gold standard here. It picks a "pivot" element and partitions the rest of the array into two groups: those smaller than the pivot and those larger. Then, it recursively sorts those subgroups. I prefer the "Hoare partition scheme" for its efficiency, but for clarity, let's look at a more readable Swift implementation using generics.
func quickSort<T: Comparable>(_ array: [T]) -> [T] {
guard array.count > 1 else { return array }
let pivot = array[array.count / 2]
let less = array.filter { $0 < pivot }
let equal = array.filter { $0 == pivot }
let greater = array.filter { $0 > pivot }
return quickSort(less) + equal + quickSort(greater)
}
Now, the version above is elegant and very "Swifty," but it allocates a lot of new arrays. In a high-performance system, you'd do this "in-place" using indices to avoid hitting the heap constantly. Still, the logic remains: by splitting the problem in half repeatedly, we move from $O(n^2)$ to $O(n \log n)$, which is a massive leap in speed.
Predictability and Stability with Merge Sort
Quick Sort is fast, but it has a weakness: if you pick a bad pivot on an already sorted list, it can still degrade to $O(n^2)$. If you need a guarantee that the sort will always perform consistently regardless of the input order, Merge Sort is your best bet. It's also "stable," meaning if two elements are equal, they keep their original relative order—something that's crucial if you're sorting a list of users by "Last Name," and then sorting that resulting list by "City."
func mergeSort<T: Comparable>(_ array: [T]) -> [T] {
guard array.count > 1 else { return array }
let middle = array.count / 2
let left = mergeSort(Array(array[0...middle-1]))
let right = mergeSort(Array(array[middle...]))
return merge(left, right)
}
func merge<T: Comparable>(_ left: [T], _ right: [T]) -> [T] {
var leftIndex = 0
var rightIndex = 0
var orderedArray: [T] = []
while leftIndex < left.count && rightIndex < right.count {
if left[leftIndex] < right[rightIndex] {
orderedArray.append(left[leftIndex])
leftIndex += 1
} else {
orderedArray.append(right[rightIndex])
rightIndex += 1
}
}
return orderedArray + Array(left[leftIndex...]) + Array(right[rightIndex...])
}
You'll notice Merge Sort uses more memory than Quick Sort because it creates these temporary arrays during the merge phase. It's a classic engineering trade-off: you're spending memory to buy yourself a guarantee of stability and consistent time complexity.
📋 Practical Task
Implementing a High-Score Leaderboard Sorter
You are building a game leaderboard. You have a Player struct with a name (String) and a score (Int). The requirements are as follows:
- Implement a Quick Sort function that sorts the players in descending order (highest score first).
- Implement a Merge Sort function that sorts the players in ascending order (lowest score first).
- Ensure both functions are generic enough to work with any
Playerarray.
Starter Code:
struct Player { let name: String let score: Int } var players = [ Player(name: "Alice", score: 450), Player(name: "Bob", score: 1200), Player(name: "Charlie", score: 800), Player(name: "Diana", score: 1200), Player(name: "Eve", score: 300) ]Goal: Write the two sorting functions and test them. When using Merge Sort on the
playersarray, verify that "Bob" and "Diana" (who both have 1200 points) maintain their relative order from the original array.
There are no comments for now.