1. Основы анализа сложности (Big O) и оптимизация коллекций Swift
Основы анализа сложности (Big O) и оптимизация коллекций Swift
Добро пожаловать в курс «Алгоритмическое собеседование на Swift». Мы начинаем наше путешествие не с написания сложного кода, а с фундамента, на котором строится вся Computer Science — анализа сложности алгоритмов.
Многие iOS-разработчики считают, что Big O нужна только для прохождения собеседований в крупные компании. Это миф. Понимание того, как ваш код масштабируется, позволяет создавать плавные интерфейсы (60 FPS), экономить заряд батареи пользователя и избегать зависаний приложения при работе с большими данными.
В этой статье мы разберем нотацию Big O, рассмотрим основные классы сложности и, самое главное, узнаем, как стандартные коллекции Swift (Array, Dictionary, Set) работают «под капотом».
Что такое Big O Notation?
Big O (О-большое) — это математическая нотация, которая описывает, как изменяется время выполнения алгоритма (или потребление памяти) при увеличении размера входных данных.
Мы не измеряем скорость в секундах, потому что секунды зависят от процессора, заряда батареи и фоновых задач. Мы измеряем количество операций в зависимости от количества данных.
!График сравнения роста сложности алгоритмов: от константного O(1) до квадратичного O(n^2).
Основные классы сложности
Рассмотрим самые популярные варианты, с которыми вы столкнетесь.
1. — Константная сложность
Время выполнения не зависит от размера входных данных. Это идеальный сценарий.
Пример из жизни: Вы берете верхнюю книгу из стопки. Неважно, в стопке 5 книг или 1000 — вы всегда берете одну книгу за одно и то же время.
Пример в Swift:
2. — Линейная сложность
Время выполнения растет прямо пропорционально количеству данных.
Математически это записывается так:
где — время выполнения, — количество элементов, а — некоторая константа времени на одну операцию.
Пример из жизни: Вам нужно прочитать все названия книг на полке. Если книг 10, вы потратите 10 секунд. Если 100 — 100 секунд.
Пример в Swift:
Здесь цикл for проходит по каждому элементу. Если массив увеличится в 10 раз, время работы функции тоже увеличится в 10 раз.
3. — Квадратичная сложность
Время выполнения пропорционально квадрату количества элементов. Обычно это вложенные циклы.
Формула роста:
где — количество операций, а — размер входных данных.
Пример в Swift:
Если в массиве 10 элементов, будет 100 операций печати. Если 100 элементов — 10 000 операций. Такие алгоритмы становятся очень медленными на больших данных.
4. — Логарифмическая сложность
Время растет очень медленно. Обычно это алгоритмы, которые на каждом шаге отсекают половину данных (например, бинарный поиск).
Формула:
где — количество шагов, — размер данных, а — логарифм по основанию 2.
Если , то алгоритму потребуется всего около 10 шагов (). Если , то всего 20 шагов. Это очень эффективно.
Производительность коллекций в Swift
На собеседовании важно знать не только теорию, но и то, как работают стандартные типы данных Swift. Рассмотрим Array и Dictionary.
Array (Массив)
Массивы в Swift хранят элементы в непрерывном блоке памяти. Это обеспечивает высокую скорость чтения, но накладывает ограничения на изменение.
| Операция | Сложность | Комментарий |
| :--- | :--- | :--- |
| Доступ по индексу arr[i] | | Мы знаем адрес начала и смещение. |
| Добавление в конец append | (в среднем) | Если есть место в памяти. |
| Вставка в начало/середину insert | | Все последующие элементы нужно сдвинуть вправо. |
| Удаление из начала/середины remove | | Все последующие элементы нужно сдвинуть влево. |
> Важно: Вставка элемента в начало массива (insert(_:at: 0)) — это одна из самых дорогих операций для массива. Избегайте её в циклах.
#### Оптимизация: reserveCapacity
Когда вы добавляете элементы в массив, и в выделенной памяти заканчивается место, Swift создает новый, больший массив, копирует туда все старые элементы и удаляет старый массив. Это тяжелая операция.
Если вы заранее знаете примерное количество элементов, используйте reserveCapacity(_:).
Dictionary (Словарь) и Set (Множество)
Словари и множества в Swift реализованы на основе хеш-таблиц. Это позволяет им быть невероятно быстрыми для поиска данных.
| Операция | Сложность (Средняя) | Сложность (Худшая) | | :--- | :--- | :--- | | Поиск по ключу | | | | Вставка | | | | Удаление | | |
Почему ? Потому что для поиска значения Swift вычисляет хеш ключа (целое число) и сразу переходит в нужную ячейку памяти, не перебирая все элементы.
Когда случается ? Это называется коллизией. Если хеш-функция плохая или таблица переполнена, разные ключи могут иметь одинаковый хеш. Тогда Swift вынужден проверять элементы списком. Но в стандартной библиотеке Swift хеширование реализовано очень качественно, поэтому мы обычно рассчитываем на .
Copy-on-Write (CoW)
В Swift коллекции (Array, Dictionary, Set) являются Value Types (структурами). Это значит, что при передаче в функцию они должны копироваться.
Однако, копирование большого массива — это , где — количество элементов. Если бы это происходило каждый раз, Swift был бы очень медленным.
Swift использует оптимизацию Copy-on-Write (копирование при записи). Данные копируются только тогда, когда вы пытаетесь их изменить, и только если на эти данные ссылается кто-то еще.
Понимание CoW критично для анализа производительности. Передача массива в функцию — это дешевая операция , пока вы не начнете менять его внутри функции.
Итоги
В следующей статье мы перейдем к практическим задачам на массивы и строки, используя эти знания для решения популярных алгоритмических проблем.