Функции высшего порядка для работы с коллекциями

Swift предоставляет ряд функций для работы с коллекциями, такими как Array, Dictionary и Set, которые помогают управлять, изменять и обрабатывать данные в них. На собеседованиях часто уделяется внимание коллекциям, причем как структуре коллекции, так и определенным функциям над ней. В данной статье поговорим про функции, которые позволяют упростить манипуляцию и обработку данных в коллекциях. Рассмотрим как работает каждая функция на конкретном примере.

Преобразование элементов: map, flatMap и compactMap

map(_:)

map(_:) — является одной из стандартных функций для работы с коллекциями в Swift. Она принимает замыкание (closure), которое применяется к каждому элементу коллекции и возвращает новую коллекцию с измененными элементами.

Сложность: O(n)

Рассмотрим пример:

let numbers = [1, 2, 3, 4]
var result = [Int]()
 
for number in numbers {
    result.append(number * number)
}
print(result) // [1, 4, 9, 16]

Перепишем наш пример с использованием функции map:

let numbers = [1, 2, 3, 4]
let result = numbers.map({ (number: Int) in
    return number*number
})
print(result) // [1, 4, 9, 16]

Попробуем сократить наше замыкание, используя разные правила. Например, можно убрать return если замыкание состоит из одного оператора return. Также, swift знает, что параметр этого замыкания должен быть Int, поэтому мы можем удалить его:

let numbers = [1, 2, 3, 4]
let result = numbers.map{ number in number*number }
print(result) // [1, 4, 9, 16]

Swift имеет сокращённый синтаксис, который позволяет нам писать код ещё короче! Вместо того, чтобы писать number, мы можем позволить Swift предоставлять автоматические имена для параметров замыкания. Они обозначаются знаком доллара, а затем числом, начинающимся с 0:

let numbers = [1, 2, 3, 4]
let result = numbers.map{ $0*$0 }
print(result) // [1, 4, 9, 16]

Замыкания в остальных функциях сокращены аналогичным образом.

Если говорить про саму функцию map, то расширение для этой функции могло бы выглядеть следующим образом:

extension Array {
    func map<T>(_ transform: (Element) -> T) -> [T] {
        var result: [T] = []
        result.reserveCapacity(count)
        for x in self {
            result.append(transform(x))
        }
        return result
    }
}

flatMap(_:)

flatMap(_:) — функция также является одной из стандартных функций для работы с коллекциями в Swift. Она также принимает замыкание (closure), которое применяется к каждому элементу коллекции, но отличается от map тем, что возвращает новую коллекцию, состоящую из элементов вложенных коллекций, полученных из каждого элемента исходной коллекции. Другими словами используя функцию flatMap можно получить одноуровневую коллекцию или, например, сокращение вложенных опционалов.

Сложность: O( n + m )

Рассмотрим пример с функцией map:

let number: String? = "2"
let result = number.map { Int($0) }
print(result) // Optional(Optional(2))

В результате мы получили вложенный Optional, вложенный в Optional. Но почему так?

Во-первых, строка number опциональна. Во-вторых, функция map при преобразовании не может быть уверена, что в параметре number находится число, вместо этого ведь может быть и слово. Отсюда и получаем вложенный Optional.

Но используя flatMap эту проблему можно исправить:

let number: String? = "2"
let result = number.flatMap { Int($0) }
print(result) // Optional(2)

Или можем рассмотреть другой пример преобразования массива коллекций в единую коллекцию:

let numbers = [[1], [2, 2], [3, 3, 3], [4, 4, 4, 4]]
let flatMapped = numbers.flatMap { $0 }
print(flatMapped) // [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]

compactMap(_:)

compactMap(_:) — эта функция также является одной из стандартных функций для работы с коллекциями в Swift. Она принимает замыкание (closure), которое применяется к каждому элементу коллекции и возвращает новую коллекцию с измененными элементами, но отличается от map и flatMap тем, что он удаляет из коллекции неопределенные значения (nil).

Сложность: O( n )

Например, можно использовать compactMap(_:) для удаления неопределенных значений из массива строк:

let mixedArray = ["1", "2", nil, "3"]
let cleanedArray = mixedArray.compactMap { $0 }
print(cleanedArray) // ["1", "2", "3"]

Функция compactMap так же как и map и flatMap не изменяет исходную коллекцию, она возвращает новую коллекцию с измененными элементами.

Фильтрация элементов: filter и allSatisfy

filter(_:)

filter(_:) — функция, принимающая замыкание (closure). Применяется к каждому элементу коллекции и возвращает новую коллекцию с элементами, которые удовлетворяют условию, заданному в замыкании.

Сложность: O( n )

Например, можно использовать filter, чтобы получить массив только четных чисел:

let numbers = [1, 2, 3, 4, 5, 6]
let evenNumbers = numbers.filter { $0 % 2 == 0 }
print(evenNumbers) // [2, 4, 6]

allSatisfy(_:)

allSatisfy(_:) — функция, принимающая замыкание (closure). Позволяет проверить, удовлетворяют ли все элементы определённому условию.

Сложность: O( n )

Например, если у вас есть массив чисел и вы хотите убедиться, что все они больше нуля, вы можете использовать allSatisfy.

let numbers = [1, 2, 3, 4, 5]
let allGreaterThanZero = numbers.allSatisfy { $0 > 0 }

if allGreaterThanZero {
    print("Все числа больше нуля")
} else {
    print("Есть числа, меньшие или равные нулю")
}

Этот метод проверяет каждый элемент в коллекции с помощью переданного условия и возвращает true, если все элементы соответствуют этому условию. Если хотя бы один элемент не удовлетворяет условию, то метод вернет false.

Есть один очень интересный пример, который тоже хотелось бы вам показать. Как вы думаете, что выведется на экран?

var arr = [String]()
let allvalueThanInt = arr.allSatisfy { $0 is Int }

if allvalueThanInt {
    print("Все значения типа Int")
} else {
    print("Не все значения типа Int")
}

Несмотря на то, что массив типа String и вообще он пустой, на экран распечатается «Все значения типа Int». Почему так? Потому что все элементы (пустой) последовательности удовлетворяют предикату — пустая истина.

Комбинирование элементов: reduce

reduce(_:_:) — функция, принимающая начальное значение и замыкание, которое определяет, как каждый элемент коллекции будет комбинироваться с начальным значением. Функция комбинирует все элементы коллекции в одно значение на основе определенной логики.

Сложность: O( n )

Например, у нас есть массив чисел, нужно найти сумму этих чисел:

let numbers = [1, 2, 3, 4, 5]
let sum = numbers.reduce(0) { result, number in
    return result + number
}
print("Сумма чисел: \(sum)") // Сумма чисел: 15

В этом примере reduce начинает с начального значения 0 и затем применяет замыкание ко всем элементам массива. Замыкание принимает текущее значение (result) и текущий элемент массива (number), затем возвращает результат комбинации этих значений. Этот результат становится новым значением результата для следующей итерации.

reduce, как и другие функции высокого порядка можно использовать с сокращенным синтаксисом:

С использованием сокращенного имени аргумента:

let numbers = [1, 2, 3, 4, 5]
let sum = numbers.reduce(0) { $0 + $1 }
print("Сумма чисел: \(sum)") // Сумма чисел: 15

Использование оператора + как замыкания:

let numbers = [1, 2, 3, 4, 5]
let sum = numbers.reduce(0, +)
print("Сумма чисел: \(sum)") // Сумма чисел: 15

Обход каждого элемента: forEach

forEach(_:) — функция, которая позволяет выполнять операцию для каждого элемента коллекции.

Сложность: O( n )

Функция проходится по всем элементам коллекции и применяет к каждому элементу переданное замыкание.

Для примера возьмем массив с числами и выведем каждый элемент:

let numbers = [1, 2, 3, 4, 5]
numbers.forEach { number in
    print(number)
}
// 1
// 2
// 3
// 4
// 5

Стоит помнить, что forEach не позволяет изменять исходную коллекцию, в отличие от, например, map или filter.

var numbers = [1, 2, 3, 4, 5]
numbers.forEach { number in
    numbers.append(number * 2) // Не сработает должным образом!
}
print(numbers) // [1, 2, 3, 4, 5, 2, 4, 6, 8, 10]

В этом примере forEach пытается умножить каждый элемент массива на 2 и добавить его в этот же массив. Однако это не сработает должным образом, потому что forEach работает с копией массива на момент начала итерации и не учитывает изменения, происходящие внутри замыкания.

Упорядочивание элементов: sort, sorted, lexicographicallyPrecedes и partition

sort()

sort() — функция, которая используется для сортировки элементов в коллекции. Она позволяет упорядочить элементы массива или другой коллекции в определенном порядке на основе предоставленного замыкания или с использованием стандартной сортировки.

Сложность: O(n log n)

Простой пример использования sort для сортировки массива чисел по возрастанию:

var numbers = [7, 8, 2, 1, 5]
numbers.sort()
print(numbers) // [1, 2, 5, 7, 8]

Также можно использовать замыкание для определения порядка сортировки. Например, чтобы отсортировать массив строк по их длине:

var strings = ["Раз", "Два", "Три", "Четыре", "Пять", "Шесть"]
strings.sort { $0.count < $1.count }
print(strings) // "["Раз", "Два", "Три", "Пять", "Шесть", "Четыре"]

Важно отметить, что функция sort изменяет саму коллекцию. Если вам не нужно менять исходную коллекцию, можно использовать функцию sorted.

sorted()

sorted() — функкция сортировки, но в отличие от sort(by:), возвращает новую отсортированную коллекцию, оставляя исходную без изменений.

Сложность: O(n log n)

let numbers = [5, 2, 8, 1, 7]
let sortedNumbers = numbers.sorted()
print(sortedNumbers) // [1, 2, 5, 7, 8]
print(numbers) // [5, 2, 8, 1, 7]

Это дает возможность сохранить исходную коллекцию неизменной и получить новую, отсортированную для дальнейшего использования.

lexicographicallyPrecedes(_:)

lexicographicallyPrecedes(_:) — функция, которая используется для сравнения двух коллекций и определения, предшествует ли одна коллекция другой в лексикографическом (словарном) порядке.

Сложность: O(m), где m — меньший размер коллекции.

Это означает, что он сравнивает элементы коллекций поочередно, начиная с первого, и определяет, находится ли первая коллекция перед второй в словарном порядке.

Сравним два массива чисел:

let numbers1 = [1, 2, 3, 4, 5]
let numbers2 = [1, 2, 4, 4, 5]

if numbers1.lexicographicallyPrecedes(numbers2) {
    print("numbers1 предшествует numbers2")
} else {
    print("numbers1 не предшествует numbers2")
}
// numbers1 предшествует numbers2

Этот метод сравнивает элементы коллекций последовательно, начиная с первого, и сравнивает их лексикографически. Если первая коллекция лексикографически предшествует второй, метод вернет true, в противном случае — false.

partition(by:)

partition(by:) — функция, которая используется для разделения элементов коллекции на две группы в соответствии с предоставленным условием.

Сложность: O( n )

Этот метод переупорядочивает элементы таким образом, что все элементы, удовлетворяющие условию, будут расположены в начале коллекции, а остальные — в конце.

Например, разделим массив на четные и нечетные числа:

var numbers = [5, 2, 8, 1, 7, 4, 3, 6]
let partitionIndex = numbers.partition(by: { $0 % 2 == 0 })

let evenNumbers = numbers.prefix(upTo: partitionIndex)
let oddNumbers = numbers.suffix(from: partitionIndex)

print("Четные числа: \(evenNumbers)") // Четные числа: [2, 8, 4, 6]
print("Нечетные числа: \(oddNumbers)") // Нечетные числа: [5, 1, 7, 3]

Функция partition использует переданное замыкание для определения условия разделения элементов. Она переупорядочивает элементы так, чтобы те, которые удовлетворяют условию, оказались перед остальными элементами.

Эта функция возвращает индекс, разделяющий элементы коллекции: все элементы до этого индекса удовлетворяют условию, а все элементы, начиная с этого индекса и далее, не удовлетворяют.

В примере выше partitionIndex — это индекс, который разделяет четные и нечетные числа в массиве. Мы можем использовать этот индекс для получения двух групп чисел: четных и нечетных.

Проверка на существование элемента: firstIndex, lastIndex, first, last и contains

firstIndex(where:)

firstIndex(where:) — функция используется для поиска индекса первого элемента в коллекции, удовлетворяющего определенному условию. Возвращает индекс первого найденного элемента или nil, если такой элемент не найден.

Сложность: O( n )

Рассмотрим пример с поиском первого четного числа:

let numbers = [3, 5, 2, 8, 1, 7, 4, 6]
if let firstEvenIndex = numbers.firstIndex(where: { $0 % 2 == 0 }) {
    print("Индекс первого четного числа: \(firstEvenIndex)") //  Индекс первого четного числа: 2
} else {
    print("Четные числа не найдены")
}

Функция firstIndex принимает замыкание, которое определяет условие поиска элемента в коллекции. Она возвращает индекс первого элемента, для которого замыкание возвращает true, или nil, если ни один элемент не соответствует заданному условию.

lastIndex(where:)

lastIndex(where:) — функция используется для поиска индекса последнего элемента в коллекции, удовлетворяющего определенному условию. Возвращает индекс последнего найденного элемента или nil, если такой элемент не найден.

Сложность: O( n )

let numbers = [3, 5, 2, 8, 1, 7, 4, 6]
if let lastEvenIndex = numbers.lastIndex(where: { $0 % 2 == 0 }) {
    print("Индекс последнего четного числа: \(lastEvenIndex)") // Индекс последнего четного числа: 7
} else {
    print("Четные числа не найдены")
}

Метод lastIndex работает аналогично firstIndex, но находит последний элемент, удовлетворяющий условию в коллекции.

first(where:)

first(where:) — функция, которая используется для получения первого элемента коллекции или последовательности.

Сложность: O( n )

Она возвращает опциональное значение — либо первый элемент коллекции, либо nil, если коллекция пуста.

Рассмотрим пример с массивом чисел:

let numbers = [3, 5, 2, 8, 1, 7, 4, 6]
if let firstNumber = numbers.first {
    print("Первый элемент: \(firstNumber)") // Первый элемент: 3
} else {
    print("Коллекция пуста")
}

Функция first возвращает опциональное значение, поэтому результат следует разворачивать при помощи оператора if let или guard let, чтобы извлечь фактическое значение элемента.

last(where:)

last(where:) — функция аналогична функции first, но возвращает последний элемент из коллекции или последовательности.

Сложность: O( n )

Рассмотрим тот же пример с числами:

let numbers = [3, 5, 2, 8, 1, 7, 4, 6]
if let lastNumber = numbers.last {
    print("Последний элемент: \(lastNumber)") // Последний элемент: 6
} else {
    print("Коллекция пуста")
}

Как и first, функция last также возвращает опциональное значение, поскольку коллекция может быть пустой.

contains(_:)

contains(_:) — функция используется для проверки наличия определенного элемента в коллекции или последовательности.

Сложность: O( n )

Функция возвращает булево значение (true или false) в зависимости от того, содержит ли коллекция указанный элемент.

Рассмотрим наш пример с массивом чисел:

let numbers = [3, 5, 2, 8, 1, 7, 4, 6]
let containsSeven = numbers.contains(7)

if containsSeven {
    print("Массив содержит число 7")
} else {
    print("Массив не содержит число 7")
}
// Массив содержит число 7

В данном примере функция contains проверяет наличие числа 7 в массиве numbers. Если число присутствует в массиве, метод вернет true, иначе — false.

Поиск минимального и максимального элемента: min и max

min()

min() — используется для нахождения наименьшего элемента в коллекции или последовательности.

Сложность: O( n )

let numbers = [3, 5, 2, 8, 1, 7, 4, 6]
if let minValue = numbers.min() {
    print("Наименьший элемент: \(minValue)")
} else {
    print("Коллекция пуста")
}
// Наименьший элемент: 1

Функция min возвращает наименьший элемент из коллекции. Возвращаемый элемент является опциональным, так как коллекция может быть пуста.

max()

max() — используется для нахождения наибольшего элемента в коллекции или последовательности.

Сложность: O( n )

let numbers = [3, 5, 2, 8, 1, 7, 4, 6]
if let maxValue = numbers.max() {
    print("Наибольший элемент: \(maxValue)")
} else {
    print("Коллекция пуста")
}
// Наибольший элемент: 8

Функция max возвращает наибольший элемент из коллекции. Аналогично функции min, она также возвращает опциональное значение.

Сравнение элементов: elementsEqual и starts

elementsEqual(_:)

elementsEqual(_:) — функция, которая используется для сравнения двух коллекций на идентичность их элементов.

Сложность: O( m ), где m — меньшый размер сравниваемых коллекций

Функция возвращает булево значение true, если обе коллекции содержат одинаковые элементы в одном и том же порядке, и false в противном случае.

Рассмотрим пример с двумя массивами:

let numbers1 = [1, 2, 3, 4, 5]
let numbers2 = [1, 2, 3, 4, 5]

let areEqual = numbers1.elementsEqual(numbers2)
if areEqual {
    print("Коллекции содержат одинаковые элементы в том же порядке")
} else {
    print("Коллекции не содержат одинаковые элементы или имеют разный порядок")
}
// Коллекции содержат одинаковые элементы в том же порядке

В данном примере elementsEqual сравнивает два массива чисел numbers1 и numbers2. Если обе коллекции содержат одни и те же элементы в том же самом порядке, метод вернет true. В противном случае — false.

starts(with:)

starts(with:) — функция используется для проверки того, начинается ли коллекция с определенной последовательности элементов или нет.

Сложность: O( m ), где m — меньшый размер из коллекций.

Функция возвращает булево значение true, если коллекция начинается с указанной последовательности, и false в противном случае.

Рассмотрим пример с массивом чисел:

let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]
let startsWith123 = numbers.starts(with: [1, 2, 3])

if startsWith123 {
    print("Коллекция начинается с последовательности [1, 2, 3]")
} else {
    print("Коллекция не начинается с последовательности [1, 2, 3]")
}
// Коллекция начинается с последовательности [1, 2, 3]

В этом примере startsWith проверяет, начинается ли массив чисел numbers с последовательности [1, 2, 3]. Если это так, метод вернет true, иначе — false.

Разбиение элементов на несколько массивов: split

split(separator:maxSplits:omittingEmptySubsequences:) — функция используется для разделения коллекции на подколлекции на основе определенного разделителя. Эта метод позволяет разбивать коллекцию на подколлекции согласно заданному условию или разделителю.

Сложность: O( n )

Рассмотрим пример с массивом чисел на основе значения разделителя:

let numbers = [0, 1, 2, 3, 0, 4, 5, 0, 6, 7]
let splitByZero = numbers.split(separator: 0) // [[1, 2 ,3], [3, 5], [6, 7]]

В данном примере метод split разделяет массив чисел numbers на подмассивы (слайсы), используя 0 в качестве разделителя. Результатом будет массив подмассивов, где каждый подмассив будет содержать элементы, идущие между нулями.

Когда вы используете функцию split для разделения коллекции, на самом деле она возвращает ArraySlice. ArraySlice представляет собой представление подколлекции, которая ссылается на часть исходной коллекции. По факту ArraySlice придумали для переиспользования памяти, которая уже была выделена под коллекцию, чтобы не создавать новую область в памяти. Подробнее про ArraySlice можно почитать здесь: Understanding The ArraySlice in Swift и здесь: ArraySlice.

Работа с начальными и конечными элементами: prefix и drop

prefix(_:)

prefix(_:) — функция используется для получения подколлекции из начала исходной коллекции.

Сложность: O( k ), где k — количество элементов, которые нужно выбрать из начала коллекции.

Функция возвращает префикс заданной длины или до определенной позиции коллекции.

Рассмотрим пример получения префикса определенной длины из массива чисел:

let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]
let prefixNumbers = numbers.prefix(3)

print("Префикс из трех элементов:", prefixNumbers) // Префикс из трех элементов: [1, 2, 3]

В этом примере метод prefix(3) возвращает подколлекцию из первых трех элементов массива numbers.

Рассмотрим еще один пример, который позволяет получить подколлекцию до определенной позиции:

let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]
let prefixUpTo5 = numbers.prefix(upTo: 5)

print("Префикс до пятого элемента:", prefixUpTo5) // Префикс до пятого элемента: [1, 2, 3, 4, 5]

Здесь prefix(upTo: 5) возвращает подколлекцию, содержащую элементы исходной коллекции до пятого элемента.

drop(while:)

drop(while:) — функция, которая используется для удаления определенного количества элементов с начала коллекции и возврата оставшихся элементов в виде новой коллекции.

Сложность: O( n )

Рассмотрим два примера использования функции:

Пример использования функции dropFirst() для удаления первых трех элементов из массива чисел:

let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]
let remainingNumbers = numbers.dropFirst(3)

print("Оставшиеся числа:", remainingNumbers) // Оставшиеся числа: [4, 5, 6, 7, 8, 9]

В этом примере метод dropFirst(3) удаляет первые три элемента из массива numbers и возвращает новую коллекцию, содержащую оставшиеся элементы. В данном случае сложность будет O( k ), где k — количество элементов, которые нужно выбрать из начала коллекции.

Также drop позволяет удалить элементы до определенной позиции (по некоторуму условию):

let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]
let remainingAfter5 = numbers.drop(while: { $0 <= 4 })

print("Оставшиеся числа:", remainingAfter5) // Оставшиеся числа: [5, 6, 7, 8, 9]

В этом примере drop(while:) удаляет элементы до тех пор, пока условие $0 <= 4 истинно. Как только условие становится ложным, метод возвращает оставшиеся элементы.

Удаление элементов: removeAll

removeAll(where:) — функцию удаляет все элементы, удовлетворяющие заданному условию.

Сложность: O( n )

Можно использовать метод для удаления значений, удовлетворящих условию:

var strings = ["Раз", "Два", "Три", "Четыре", "Пять", "Шесть"]
strings.removeAll(where: { $0.count > 3 })

print("Оставшиеся слова:", strings) // Оставшиеся слова: ["Раз", "Два", "Три"]

Сокращенная версия:

var strings = ["Раз", "Два", "Три", "Четыре", "Пять", "Шесть"]
strings.removeAll{ $0.count > 3 }

print("Оставшиеся слова:", strings) // Оставшиеся слова: ["Раз", "Два", "Три"]

В этом примере использует замыкание { $0.count > 3 }, чтобы удалить все элементы, у которых количество символов больше 3. В результате останутся только слова, содержащие 3 символа или менее.

Также функцию можно использовать для удаления всех элементов коллекции:

var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]
numbers.removeAll()

print("После удаления всех элементов:", numbers) // После удаления всех элементов: []

Куда пойти дальше:

  1. Функциональное Программирование В Swift
  2. Немного практики функционального программирования в Swift для начинающих
  3. Как подготовиться к алгоритмической секции

Выразить благодарность или найти уникальный материал вы можете в boosty.

Подписывайтесь на мой Telegram-канал iOS Interview Channel, чтобы не пропустить новый материал.


Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *