Зачем нужны сортировка и поиск
Список результатов теста хочется показать по убыванию баллов. Каталог товаров — от дешёвых к дорогим. В списке заказов нужно быстро найти первый просроченный. Всё это одна и та же задача с двух сторон: расставить элементы коллекции в нужном порядке или найти среди них конкретный элемент, не перебирая всё вручную циклом. У массивов и словарей для этого уже есть готовые методы — их и разберём.
sorted() и sort()
Метод sorted() возвращает новый массив с элементами по возрастанию, а исходный оставляет как есть.
let numbers = [5, 3, 8, 1]
let ascending = numbers.sorted()
print(ascending)
print(numbers)
ascending — это [1, 3, 5, 8], а numbers по-прежнему [5, 3, 8, 1]: sorted() ничего не меняет в исходном массиве, а строит рядом новый. Именно поэтому numbers можно объявить через let — метод не пытается его изменить.
У sorted() есть парный метод sort() — без приставки ed. Он не возвращает ничего нового, а переставляет элементы прямо в существующем массиве. Раз массив меняется, он обязан быть объявлен через var:
var scores = [70, 40, 95]
scores.sort()
print(scores)
После этой строки scores — уже [40, 70, 95], отдельной переменной под результат заводить не пришлось. Если бы scores был объявлен через let, компилятор отказался бы собирать программу: sort() меняет получателя, а константы менять нельзя.
Comparable простыми словами
Чтобы sorted() вообще мог расставить элементы по порядку, тип элементов должен уметь отвечать на вопрос «кто из двух меньше». Это умение называется протоколом Comparable: если тип ему соответствует, для его значений работают <, > и подобные операторы, а значит и сортировка без уточнений. У Int, Double и String это уже есть «из коробки» — поэтому [3, 1, 2].sorted() и ["б", "а"].sorted() работают без вопросов. Для своих типов такое умение придётся объявлять самостоятельно, но это отдельная тема, до которой мы ещё дойдём.
Своё правило сортировки: sorted(by:)
Если стандартного порядка мало — например, нужно по убыванию, а не по возрастанию, — на помощь приходит sorted(by:). Он принимает замыкание с двумя элементами и должен вернуть true, если первый должен идти раньше второго:
let prices = [250, 40, 999, 15]
let descending = prices.sorted(by: { first, second in first > second })
print(descending)
first > second означает «первый должен встать раньше, если он больше второго» — то есть по убыванию. Результат — [999, 250, 40, 15]. Такое правило пишут и короче, через сокращённую запись параметров замыкания:
let descending2 = prices.sorted(by: { $0 > $1 })
print(descending2)
Здесь $0 — первый из сравниваемых элементов, $1 — второй, а сама запись читается точно так же: «первый больше второго — значит, он раньше». То же самое работает и у sort(by:) — только результат окажется в исходном (обязательно var) массиве, а не в новом.
Сортировка строк и ловушка с регистром
Строки сравниваются посимвольно по числовому коду символов, а не «по алфавиту, как в словаре». У латиницы заглавные буквы стоят в таблице символов раньше строчных, поэтому обычный sorted() без уточнений расставит заглавную букву перед любой строчной, даже если по алфавиту она должна идти позже:
let names = ["banana", "Apple", "cherry"]
print(names.sorted())
Результат — ["Apple", "banana", "cherry"]: "Apple" оказалась первой не потому, что она раньше по алфавиту, а потому, что заглавная A «меньше» строчной b как символ. Если регистр не должен влиять на порядок, элементы приводят к одному регистру прямо в правиле сравнения:
let sortedIgnoringCase = names.sorted { $0.lowercased() < $1.lowercased() }
print(sortedIgnoringCase)
Теперь сравниваются уже полностью строчные копии строк, и результат — привычный алфавитный порядок: ["Apple", "banana", "cherry"] с учётом того, что "apple" действительно раньше остальных по алфавиту.
Сортировка по нескольким критериям
Иногда одного правила мало: например, отсортировать имена сначала по длине, а среди одинаковых по длине — по алфавиту. Тогда в замыкании проверяют первый критерий и, только если он не различает элементы, переходят ко второму:
let words = ["fig", "date", "kiwi", "banana"]
let byLengthThenAlpha = words.sorted { a, b in
if a.count != b.count {
return a.count < b.count
}
return a < b
}
print(byLengthThenAlpha)
Если длины различаются, порядок решает именно длина. Если длины совпали (как у "date" и "kiwi", по четыре символа), в дело идёт вторая проверка — обычное сравнение строк. Результат — ["fig", "date", "kiwi", "banana"].
Поиск первого и последнего подходящего элемента
first(where:) возвращает первый элемент, для которого замыкание вернуло true, а last(where:) — последний. Раз подходящего элемента может и не найтись, оба возвращают Optional:
let temperatures = [18, 22, 15, 30, 12]
let firstHot = temperatures.first(where: { $0 > 25 })
print(firstHot)
firstHot — это Optional(30): единственное число больше 25. Если бы такого числа не было, результатом стал бы nil, и его, как любой опционал, нужно разворачивать — например, через ??, чтобы подставить значение по умолчанию, если ничего не нашлось.
Поиск позиции: firstIndex
firstIndex(of:) ищет позицию конкретного значения, а firstIndex(where:) — позицию первого элемента, подходящего под условие. Оба тоже возвращают Optional, потому что элемента может не быть вовсе:
let letters = ["a", "b", "c", "d"]
let position = letters.firstIndex(of: "c")
print(position)
position — Optional(2), потому что "c" стоит третьим по счёту, а индексы начинаются с нуля.
contains, allSatisfy и границы коллекции
contains проверяет, есть ли в коллекции конкретное значение, а contains(where:) — есть ли хотя бы один элемент, подходящий под условие; оба возвращают простой Bool, без опционала — ответ либо есть, либо нет, независимого от найденного значения:
let numbers2 = [4, 8, 15, 16]
print(numbers2.contains(15))
print(numbers2.contains(where: { $0 > 100 }))
Первая строка выведет true — 15 есть в массиве. Вторая — false: чисел больше ста здесь нет. У allSatisfy обратная логика — он проверяет, что условию подходят вообще все элементы: numbers2.allSatisfy { $0 > 0 } вернёт true, потому что все числа положительные.
min(), max() и свои правила min(by:), max(by:)
min() и max() возвращают наименьший и наибольший элемент — тоже как Optional, потому что у пустой коллекции их не существует:
let values = [7, 2, 9, 4]
print(values.min())
print(values.max())
Здесь min() даст Optional(2), а max() — Optional(9). Когда нужно сравнивать не «в лоб», а по своему правилу — например, по длине строки, — используют min(by:) и max(by:) с тем же замыканием из двух аргументов, что и у sorted(by:):
let animals = ["cat", "elephant", "dog"]
let longest = animals.max(by: { $0.count < $1.count })
print(longest)
max(by:) с правилом «первый короче второго» находит элемент, который ни для одного другого не оказался короче — то есть самый длинный. Результат — Optional("elephant").
Ловушка со словарём: sorted(by:) возвращает не словарь
У словаря нет понятия «порядок» — пары ключ-значение хранятся так, как удобно самой структуре, а не так, как их добавили. Когда вы вызываете sorted(by:) у словаря, результатом становится не отсортированный словарь (такого в принципе не существует), а обычный массив кортежей (key, value), уже расставленных по нужному правилу:
let scores = ["Anna": 90, "Boris": 95, "Karim": 80]
let sortedPairs = scores.sorted { $0.value > $1.value }
print(sortedPairs)
sortedPairs имеет тип [(key: String, value: Int)] — это массив, а не словарь, и обращаться к его элементам нужно как к парам: sortedPairs[0].key, sortedPairs[0].value. Если из результата нужны только имена, их дополнительно достают через map: sortedPairs.map { $0.key }.
Частые ошибки
- Вызывать
sort()на массиве, объявленном черезlet— компилятор откажется собирать код, потому чтоsort()меняет получателя на месте. - Ожидать, что
sorted()без параметров расставит строки «по алфавиту, как в словаре» — на деле заглавные буквы окажутся раньше строчных. - Забывать, что
first(where:),firstIndex(of:),min()иmax()возвращаютOptional— и пытаться использовать результат так, будто он точно есть. - Думать, что
sorted(by:)у словаря вернёт словарь — на самом деле это всегда массив кортежей(key, value). - Путать
sorted()иsort()в описании кода: первый ничего не меняет и возвращает новое, второй меняет исходную коллекцию и не возвращает ничего полезного.
Резюме
sorted()возвращает новый отсортированный массив, исходный не трогает; работает для типов, соответствующихComparable— уInt,Double,Stringэто уже есть.sort()сортирует массив на месте и требуетvar.sorted(by:)иsort(by:)принимают своё правило сравнения — замыкание вида{ $0 > $1 }; несколько критериев проверяют по очереди внутри одного замыкания.- Сортировка строк по умолчанию учитывает регистр символов — заглавные буквы идут раньше строчных.
first(where:),last(where:),firstIndex(of:),firstIndex(where:),min(),max()возвращаютOptional, потому что подходящего элемента может не найтись.contains,contains(where:)иallSatisfyвозвращают обычныйBool.min(by:)иmax(by:)ищут крайний элемент по своему правилу, какsorted(by:).sorted(by:)у словаря возвращает массив кортежей(key, value), а не словарь.
Проверьте себя
3 вопроса
Сортировка по длине и алфавиту
Напишите функцию sortByLengthThenAlphabetically, которая принимает массив строк и возвращает новый массив, отсортированный по длине строки по возрастанию. Если у двух строк длина одинаковая, они должны идти в обычном алфавитном порядке между собой.
Используйте sorted(by:) со своим правилом сравнения: сначала сравните длины, а если они равны — сами строки.
Первое длинное слово
Напишите функцию firstWordLongerThan, которая принимает массив строк words и число length, а возвращает первое слово, длина которого строго больше length. Если такого слова нет, функция должна вернуть строку "not found".
Используйте first(where:) — он вернёт первое подходящее слово или nil, если такого нет, а значение по умолчанию для случая nil удобно подставить оператором ??.
Лучшие результаты
Напишите функцию topScorers, которая принимает словарь scores вида [String: Int] (имя — результат) и число n, а возвращает массив из n имён с наибольшими результатами, отсортированный по убыванию результата. Если два участника набрали поровну, между собой они должны идти в алфавитном порядке имён. Если n больше числа участников, верните имена всех участников. Если n меньше или равно нулю, верните пустой массив.
Вспомните, что sorted(by:) у словаря возвращает массив кортежей (key, value), а не словарь — из него можно взять нужное число элементов через prefix и достать имена через map.