1. Поговорим о массивах
Изучение данного блока предполагает предварительное знание синтаксиса языка Swift. Для успешного освоения этого материала, необходимо иметь базовое понимание синтаксиса языка Swift. Это включает в себя знание основных структур данных, операторов, циклов, функций, абстракций и других ключевых элементов языка. Без этих фундаментальных знаний будет сложно понять более сложные концепции и примеры, которые будут рассматриваться в данном блоке.
Массивы являются одним из наиболее часто используемых типов данных в Swift. Они предоставляют упорядоченную коллекцию элементов одного типа и обладают богатым набором методов и функций для работы с ними. В этой статье мы подробно рассмотрим массивы в Swift, их основные методы, алгоритмическую сложность операций.
Введение в массивы¶
В Swift массивы представлены типом Array, который является обобщенным и может хранить элементы любого типа, соответствующего определенному условию (например, Array<Int>, Array<String> или просто [Int], [String]). Массивы в Swift являются коллекциями, соответствующими протоколу Collection, что предоставляет им множество функциональностей.
Создание массивов¶
// Пустой массив целых чисел
var numbers = [Int]()
// Массив строк с начальными значениями
let fruits = ["Яблоко", "Банан", "Апельсин"]
// Использование синтаксиса обобщений
var doubles: Array<Double> = [1.0, 2.0, 3.0]
Основные методы и операции с массивами¶
Добавление элементов¶
Добавление одного элемента в конец массива:
numbers.append(5)
numbers.append(contentsOf: [6, 7, 8])
numbers.insert(4, at: 0)
Удаление элементов¶
Удаление элементов по указанному индексу:
let removedNumber = numbers.remove(at: 0)
numbers.removeLast()
numbers.removeAll()
Доступ к элементам¶
Индексирование:
let firstFruit = fruits[0]
if let firstNumber = numbers.first {
print("Первый элемент: \(firstNumber)")
}
if let lastNumber = numbers.last {
print("Последний элемент: \(lastNumber)")
}
Изменение элементов¶
Прямое присваивание по индексу:
numbers[0] = 10
numbers.replaceSubrange(0...1, with: [0, 1])
Функциональные методы¶
Функциональные методы массивов в Swift позволяют применять функциональный подход к обработке коллекций данных. Эти методы позволяют выполнять операции над массивами, не изменяя исходный массив, и часто могут быть объединены для создания цепочек преобразований.
Давайте подробно рассмотрим наиболее важные функциональные методы и приведем примеры их использования.
map(_:)¶
Метод map(_:) применяет заданное преобразование к каждому элементу массива и возвращает новый массив с преобразованными значениями.
func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]
let numbers = [1, 2, 3, 4, 5]
let squares = numbers.map { $0 * $0 }
print(squares) // [1, 4, 9, 16, 25]
{ $0 * $0 }, которое возводит число в квадрат.
compactMap(_:)¶
Метод compactMap(_:) также применяет преобразование к каждому элементу, но возвращает массив без nil значений. Это полезно, когда преобразование может вернуть опциональное значение.
func compactMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult]
let strings = ["1", "2", "three", "4", "five"]
let numbers = strings.compactMap { Int($0) }
print(numbers) // [1, 2, 4]
flatMap(_:)¶
В Swift 4.1 и ранее flatMap(_:) применялся для “выравнивания” массивов массивов. Начиная с Swift 4.1, для этой цели рекомендуется использовать joined().
func flatMap<SegmentOfResult>(_ transform: (Element) throws -> SegmentOfResult) rethrows -> [SegmentOfResult.Element] where SegmentOfResult : Sequence
let nestedArray = [[1, 2, 3], [4, 5, 6]]
let flatArray = nestedArray.flatMap { $0 }
print(flatArray) // [1, 2, 3, 4, 5, 6]
filter(_:)¶
Метод filter(_:) возвращает массив элементов, удовлетворяющих заданному условию.
func filter(_ isIncluded: (Element) throws -> Bool) rethrows -> [Element]
let numbers = [1, 2, 3, 4, 5, 6]
let evenNumbers = numbers.filter { $0 % 2 == 0 }
print(evenNumbers) // [2, 4, 6]
reduce(::)¶
Метод reduce(_:_:) сводит массив к одному значению, используя начальное значение и функцию-аккумулятор.
func reduce<Result>(_ initialResult: Result, _ nextPartialResult: (Result, Element) throws -> Result) rethrows -> Result
let numbers = [1, 2, 3, 4, 5]
let sum = numbers.reduce(0) { $0 + $1 }
print(sum) // 15
reduce(into:_:)¶
Метод reduce(into:_:) похож на reduce, но более эффективен при работе с изменяемыми типами.
func reduce<into Result>(_ initialResult: Result, _ updateAccumulatingResult: (inout Result, Element) throws -> ()) rethrows -> Result
let words = ["apple", "banana", "cherry"]
let wordLengths = words.reduce(into: [:]) { counts, word in
counts[word] = word.count
}
print(wordLengths) // ["apple": 5, "banana": 6, "cherry": 6]
forEach(_:)¶
Метод forEach(_:) выполняет заданное действие для каждого элемента массива.
func forEach(_ body: (Element) throws -> Void) rethrows
let numbers = [1, 2, 3]
numbers.forEach { print($0) }
// Вывод:
// 1
// 2
// 3
contains(_:) и contains(where:)¶
Методы для проверки наличия элемента или соответствия условия.
func contains(_ element: Element) -> Bool
func contains(where predicate: (Element) throws -> Bool) rethrows -> Bool
let numbers = [1, 2, 3, 4, 5]
let hasThree = numbers.contains(3) // true
let hasEvenNumber = numbers.contains { $0 % 2 == 0 } // true
allSatisfy(_:)¶
Проверяет, удовлетворяют ли все элементы заданному условию.
func allSatisfy(_ predicate: (Element) throws -> Bool) rethrows -> Bool
let numbers = [2, 4, 6]
let allEven = numbers.allSatisfy { $0 % 2 == 0 } // true
first(where:) и firstIndex(where:)¶
Находит первый элемент или индекс, удовлетворяющий условию.
func first(where predicate: (Element) throws -> Bool) rethrows -> Element?
func firstIndex(where predicate: (Element) throws -> Bool) rethrows -> Int?
let numbers = [1, 3, 5, 6, 7]
if let firstEven = numbers.first(where: { $0 % 2 == 0 }) {
print(firstEven) // 6
}
dropFirst(:) и dropLast(:)¶
Возвращает массив без первых или последних n элементов.
func dropFirst(_ k: Int = 1) -> SubSequence
func dropLast(_ k: Int = 1) -> SubSequence
let numbers = [1, 2, 3, 4, 5]
let withoutFirstTwo = numbers.dropFirst(2) // [3, 4, 5]
let withoutLastTwo = numbers.dropLast(2) // [1, 2, 3]
prefix(:) и suffix(:)¶
Возвращает первые или последние n элементов.
func prefix(_ maxLength: Int) -> SubSequence
func suffix(_ maxLength: Int) -> SubSequence
let numbers = [1, 2, 3, 4, 5]
let firstThree = numbers.prefix(3) // [1, 2, 3]
let lastTwo = numbers.suffix(2) // [4, 5]
enumerated()¶
Возвращает последовательность пар (индекс, элемент).
func enumerated() -> EnumeratedSequence<Array<Element>>
let fruits = ["Apple", "Banana", "Cherry"]
for (index, fruit) in fruits.enumerated() {
print("\(index): \(fruit)")
}
// Вывод:
// 0: Apple
// 1: Banana
// 2: Cherry
zip(_:)¶
Объединяет два массива в последовательность пар.
func zip<Sequence1, Sequence2>(
_ sequence1: Sequence1,
_ sequence2: Sequence2
) -> Zip2Sequence<Sequence1, Sequence2> where Sequence1 : Sequence, Sequence2 : Sequence
let letters = ["A", "B", "C"]
let numbers = [1, 2, 3]
let zipped = zip(letters, numbers)
for (letter, number) in zipped {
print("\(letter): \(number)")
}
// Вывод:
// A: 1
// B: 2
// C: 3
lazy¶
Позволяет отложить выполнение цепочки операций до момента, когда результат действительно нужен.
Пример использования:
let numbers = 1...1000
let result = numbers.lazy
.map { $0 * 2 }
.filter { $0 % 3 == 0 }
.prefix(5)
print(Array(result)) // [6, 12, 18, 24, 30]
partition(by:)¶
Разделяет массив на две части в зависимости от условия.
mutating func partition(by belongsInSecondPartition: (Element) throws -> Bool) rethrows -> Int
var numbers = [3, 1, 4, 1, 5, 9, 2]
let pivotIndex = numbers.partition(by: { $0 > 3 })
let lessThanOrEqualThree = numbers[..<pivotIndex] // [3, 1, 1, 2]
let greaterThanThree = numbers[pivotIndex...] // [4, 5, 9]
partition(by:) переставляет элементы массива так, чтобы элементы, удовлетворяющие условию, были в конце массива.
sorted(by:)¶
Возвращает отсортированный массив на основе заданного критерия.
func sorted(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows -> [Element]
let names = ["Charlie", "Alice", "Bob"]
let sortedNames = names.sorted(by: <)
print(sortedNames) // ["Alice", "Bob", "Charlie"]
reversed()¶
Возвращает массив в обратном порядке.
func reversed() -> ReversedCollection<Array<Element>>
let numbers = [1, 2, 3, 4, 5]
let reversedNumbers = numbers.reversed()
print(Array(reversedNumbers)) // [5, 4, 3, 2, 1]
firstIndex(of:)¶
Находит индекс первого вхождения заданного элемента.
func firstIndex(of element: Element) -> Int?
let letters = ["a", "b", "c", "a", "b", "c"]
if let index = letters.firstIndex(of: "b") {
print(index) // 1
}
drop(while:) и prefix(while:)¶
Позволяют пропустить или выбрать элементы, пока условие выполняется.
func drop(while predicate: (Element) throws -> Bool) rethrows -> SubSequence
func prefix(while predicate: (Element) throws -> Bool) rethrows -> SubSequence
let numbers = [1, 2, 3, 4, 5]
let dropped = numbers.drop(while: { $0 < 3 }) // [3, 4, 5]
let prefixed = numbers.prefix(while: { $0 < 3 }) // [1, 2]
Алгоритмическая сложность операций¶
Понимание алгоритмической сложности операций с массивами важно для написания эффективного кода. Ниже мы рассмотрим наиболее распространенные операции с массивами, их временную сложность и приведем примеры для лучшего понимания.
Добавление элементов¶
append(_:)¶
O(1) амортизированное время.
В большинстве случаев добавление элемента в конец массива происходит за константное время, так как массивы в Swift хранятся в непрерывной области памяти и имеют дополнительную емкость для новых элементов. Однако, когда внутренний буфер массива переполнен, происходит перераспределение памяти и копирование элементов в новый более крупный буфер, что может занять O(n) времени.
var numbers = [Int]()
for i in 1...1000 {
numbers.append(i)
}
append(_:) имеет амортизированную сложность O(1), общее время выполнения цикла будет O(n), где n — количество добавляемых элементов.
Вставка и удаление элементов¶
insert(_:at:)¶
O(n).
При вставке элемента по определенному индексу все элементы после этого индекса должны быть сдвинуты на одну позицию вправо, что требует времени, пропорционального количеству элементов после индекса.
var numbers = [1, 2, 3, 4, 5]
numbers.insert(0, at: 0) // Вставка в начало
remove(at:)¶
O(n).
При удалении элемента по индексу все последующие элементы должны быть сдвинуты на одну позицию влево.
var numbers = [1, 2, 3, 4, 5]
numbers.remove(at: 2) // Удаление элемента с индексом 2
Удаление последнего элемента¶
removeLast()¶
O(1).
Удаление последнего элемента не требует сдвига других элементов, поэтому операция выполняется за константное время.
var numbers = [1, 2, 3, 4, 5]
numbers.removeLast()
Доступ к элементам¶
Доступ по индексу [i]¶
O(1).
Массивы в Swift хранятся в непрерывной области памяти, поэтому доступ к элементу по индексу происходит за константное время, используя адресную арифметику.
let numbers = [10, 20, 30, 40, 50]
let element = numbers[3] // 40
Поиск элементов¶
firstIndex(of:)¶
O(n).
В худшем случае метод firstIndex(of:) может потребоваться проверить каждый элемент массива, чтобы найти совпадение.
let numbers = [1, 2, 3, 4, 5]
if let index = numbers.firstIndex(of: 5) {
print("Индекс: \(index)")
}
Перебор массива¶
for-in цикл¶
O(n).
Итерирование по массиву требует посещения каждого элемента один раз.
for number in numbers {
print(number)
}
Функциональные методы¶
map(:), filter(:), reduce(_:)¶
O(n).
Эти методы проходят по каждому элементу массива, применяя функцию или выполняя операцию, поэтому время выполнения пропорционально количеству элементов.
let numbers = [1, 2, 3, 4, 5]
// map
let squares = numbers.map { $0 * $0 } // [1, 4, 9, 16, 25]
// filter
let evenNumbers = numbers.filter { $0 % 2 == 0 } // [2, 4]
// reduce
let sum = numbers.reduce(0, +) // 15
Сортировка¶
sort() и sorted()¶
O(n log n).
Методы сортировки обычно используют алгоритмы с временной сложностью O(n log n), такие как QuickSort или MergeSort.
var numbers = [5, 2, 3, 1, 4]
numbers.sort()
Поиск максимального и минимального значения¶
max() и min()¶
O(n).
Для нахождения максимального или минимального значения необходимо проверить каждый элемент массива.
if let maxNumber = numbers.max() {
print("Максимальное число: \(maxNumber)")
}
Проверка наличия элемента¶
contains(_:)¶
O(n).
В худшем случае метод должен проверить каждый элемент массива.
let exists = numbers.contains(3)
Добавление коллекции элементов¶
append(contentsOf:)¶
O(k), где k — количество добавляемых элементов.
Добавление нескольких элементов требует копирования этих элементов в конец массива.
var numbers = [1, 2, 3]
numbers.append(contentsOf: [4, 5, 6])
Резервирование емкости¶
reserveCapacity(_:)¶
O(n).
Резервирование емкости может потребовать перераспределения памяти и копирования существующих элементов.
var numbers = [Int]()
numbers.reserveCapacity(1000)
Удаление всех элементов¶
removeAll(keepingCapacity:)¶
O(n).
Удаление всех элементов требует обнуления ссылок или вызова деструкторов для каждого элемента.
numbers.removeAll()
Итоговая таблица сложностей¶
| Операция | Сложность |
|---|---|
| append(_:) | O(1)* |
| insert(_:at:) | O(n) |
| remove(at:) | O(n) |
| removeLast() | O(1) |
Доступ по индексу [i] |
O(1) |
| first и last | O(1) |
| map(_:) | O(n) |
| filter(_:) | O(n) |
| reduce(::) | O(n) |
| sort() и sorted() | O(n log n) |
| max() и min() | O(n) |
| contains(_:) | O(n) |
| append(contentsOf:) | O(k) |
| reserveCapacity(_:) | O(n) |
| removeAll() | O(n) |