sort() and sorted()
Swift arrays have two ways to sort, and the difference matters:
sort()sorts the array in place. It changes the array it is called on and returns nothing, so the array must be declared withvar.sorted()leaves the array alone and returns a new sorted array. It works on aletconstant, and on function parameters, which are constants in Swift.
var numbers = [11, 3, 7, 5, 2]
numbers.sort()
print(numbers) // [2, 3, 5, 7, 11]
let original = [11, 3, 7, 5, 2]
let ascending = original.sorted()
print(original) // [11, 3, 7, 5, 2]
print(ascending) // [2, 3, 5, 7, 11]
Without a comparison, both sort ascending. This works for any element type that can be compared with <, such as Int, Double and String.
Descending order with by:
Both methods have a form that takes a function, sort(by:) and sorted(by:). The function receives two elements and returns true when the first one must come before the second. The operator > is itself such a function, so it can be passed as it is:
var numbers = [11, 3, 7, 5, 2]
numbers.sort(by: >)
print(numbers) // [11, 7, 5, 3, 2]
This works the same way for every element type, including strings. No separate reverse step is needed.
Custom orderings with closures
For any other order, pass a closure. These three forms do the same thing, from the longest to the shortest:
func compare(_ a: Int, _ b: Int) -> Bool {
return a < b
}
// a closure with typed parameters
let typed = { (a: Int, b: Int) -> Bool in
return a < b
}
// a closure with inferred types, passed straight to sorted(by:)
let result = [3, 23, 12].sorted(by: { a, b in a < b })
print(result) // [3, 12, 23]
A closure written after the call's parentheses is a trailing closure. When it has a single expression, the return can be dropped, and the parameters can be left unnamed and called $0 and $1 instead:
var words = ["banana", "pie", "Washington", "book"]
words.sort { $0.count < $1.count }
print(words) // ["pie", "book", "banana", "Washington"]
Here $0 and $1 are the two strings being compared. If the closure returns true, the first goes before the second; if it returns false, it doesn't.
Sorting pairs
A closure with several statements needs an explicit return. This is how you sort an array of two-element arrays, using the second value when the first values are equal:
var pairs = [[1, 5], [12, 5], [10, 1], [1, 3]]
pairs.sort {
if $0[0] == $1[0] {
return $0[1] < $1[1]
}
return $0[0] < $1[0]
}
print(pairs) // [[1, 3], [1, 5], [10, 1], [12, 5]]
Gotchas
sort()on a constant doesn't compile. Callingsort()on aletarray, or on a function parameter, givescannot use mutating member on immutable value: 'numbers' is a 'let' constant. Usesorted(), or copy the parameter first withvar nums = nums.sort()returns nothing. Writinglet result = numbers.sort()doesn't give you the array. Swift warns that the constant has type(), an empty value. Usesorted()when you want a result.- Strings compare by character value. Every uppercase letter comes before every lowercase letter, so
["Banana", "apple", "Cherry"].sorted()gives["Banana", "Cherry", "apple"]. To ignore case, compare lowercased versions in a closure. - The closure decides "comes first". It should say whether the first argument goes before the second. Using
<=, which is true for equal elements, breaks that rule and can give unreliable results. Use a strict comparison.
The lesson's exercise
The exercise has three parts, one for each kind of sort.
- Numbers, descending. Declare
numbersas[2, 1, 3, 10]withvarand callnumbers.sort(by: >). It becomes[10, 3, 2, 1]. - Pairs, first value ascending, second value descending. Declare
coordinatesas[3, 2],[2, 3]and[3, 5]. In the closure, when$0[0] == $1[0]return$0[1] > $1[1]; otherwise return$0[0] < $1[0]. The result is[[2, 3], [3, 5], [3, 2]]: the pair starting with 2 comes first, and of the two pairs starting with 3, the one with the bigger second value comes first. - Names, ignoring case. Declare
namesas["Mircea", "john", "Andy"]and sort with{ $0.lowercased() < $1.lowercased() }. The result is["Andy", "john", "Mircea"]. A plainsort()would put"john"last.
var numbers = [2, 1, 3, 10]
numbers.sort(by: >)
var coordinates = [
[3, 2],
[2, 3],
[3, 5]
]
coordinates.sort {
if $0[0] == $1[0] {
return $0[1] > $1[1]
}
return $0[0] < $1[0]
}
var names = ["Mircea", "john", "Andy"]
names.sort { $0.lowercased() < $1.lowercased() }
Cost
Sorting n elements takes O(n log n) comparisons, and each comparison costs time too. Comparing two numbers is constant time, so sorting numbers is O(n log n). Comparing two strings takes time proportional to their length, so sorting strings of average length L costs O(n log n * L). sorted() also builds a second array of n elements, so it uses O(n) extra memory.
Key takeaways
sort()changes avararray in place;sorted()returns a new array and works on constants.by: >sorts in descending order for numbers and strings alike.- A sorting closure takes two elements and returns
truewhen the first goes first.$0and$1are the shorthand names for them. - To sort by several keys, compare the first key and fall back to the next one only when the first keys are equal.
