Структуры данных
Массивы: Фундамент структур данных
У Netflix **200 миллионов** подписчиков. Когда ты открываешь приложение, оно находит твой профиль среди этих миллионов за **микросекунды**. Не перебирая всех. Как? Ответ - в самой простой структуре данных.
Цели урока
- Понять, как массив хранится в памяти (contiguous)
- Объяснить, почему доступ O(1), а вставка O(n)
- Различать статические и динамические массивы
- Знать, когда массив - правильный выбор
Предварительные знания
- Базовая работа со списками в Python
Почему доступ к элементу массива мгновенный, а вставка в середину - медленная? Ответ не в алгоритмах, а в устройстве памяти.
- **FAANG интервью**: половина задач - массивы (two pointers, sliding window, prefix sum)
- **90% структур данных** используют массивы внутри: хеш-таблицы, кучи, графы
- **Cache locality**: массивы дружат с кэшем CPU → реальная скорость в разы выше
Массив - старейшая структура данных
Массив старше почти всех остальных структур данных, потому что он напрямую отражает устройство памяти: непрерывный блок ячеек с последовательными адресами. Первым языком высокого уровня с настоящими массивами стал FORTRAN, выпущенный командой Джона Бэкуса в IBM в 1957 году. Индексация и многомерные массивы были там центральной фичей, ведь язык создавался для научных и инженерных расчётов. В 1962 году Кеннет Айверсон в книге A Programming Language (язык APL) сделал массив главным объектом вычислений: целые операции выполнялись над массивами целиком, без явных циклов. Эта идея позже легла в основу NumPy и MATLAB. В 1972 году Деннис Ритчи в языке C закрепил связь массива и указателя: запись arr[i] эквивалентна *(arr + i), и именно эта арифметика адресов даёт O(1) доступ, который мы изучаем.
Память как улица с домами
Представь, что память компьютера - это **бесконечная улица с пронумерованными домами**. Каждый дом имеет адрес: 0, 1, 2, 3...
Массив - это **бронь подряд идущих домов**. Если тебе нужно 5 элементов, ты бронируешь дома 100, 101, 102, 103, 104. Они идут **непрерывно** (contiguous).
**Ключевой инсайт:** Зная начальный адрес (100) и индекс (i), компьютер **мгновенно** вычисляет, где лежит элемент: `100 + i`. Никакого поиска!
Массив начинается с адреса 200. Где в памяти лежит arr[7]?
O(1) доступ - суперсила массива
Благодаря непрерывному расположению, массив даёт **random access** (произвольный доступ) за **O(1)** - константное время.
**O(1)** означает: неважно, миллион элементов в массиве или миллиард - доступ к любому занимает **одинаковое время**. Просто арифметика: `base + index`.
Netflix и O(1)
У Netflix 200+ миллионов подписчиков. Каждый имеет ID (число). Данные хранятся в массиво-подобных структурах. ``` user_data[user_id] → профиль пользователя ``` Доступ к профилю пользователя #150,000,000 занимает столько же времени, сколько к пользователю #1. Это O(1).
В массиве 1 миллиард элементов. Сколько операций нужно, чтобы получить arr[999999999]?
Вставка: Почему O(n)?
Вот и **обратная сторона медали**. Чтобы вставить элемент в середину, нужно **подвинуть всех соседей**.
**Худший случай:** вставка в начало. Нужно сдвинуть **все n элементов**. Это **O(n)**.
**Лучший случай:** вставка в конец. Сдвигать никого не надо. Это **O(1)** (если есть место).
В массиве 10,000 элементов. Сколько элементов нужно сдвинуть для вставки на позицию 3?
Статические vs Динамические массивы
В классических языках (C, C++) массив имеет **фиксированный размер**. Объявил `int arr[10]` - всё, ровно 10 ячеек, ни больше, ни меньше.
**Динамические массивы** (Python list, Java ArrayList, C++ vector) **растут автоматически**. Как?
**Amortized O(1):** Хотя редко бывает копирование всех элементов (O(n)), в среднем добавление в конец - O(1). Почему? Потому что capacity удваивается, и копирование происходит всё реже.
Математика amortization
Добавляем 1000 элементов. Копирования происходят когда capacity = 1, 2, 4, 8, 16, 32, 64, 128, 256, 512. Всего операций копирования: 1 + 2 + 4 + ... + 512 = 1023. Всего вставок: 1000. Среднее: 1023/1000 ≈ 1 операция на вставку = O(1) amortized.
Динамический массив имеет capacity = 16 и size = 16. Добавляем элемент. Что произойдёт?
Сводная таблица сложности
Теперь ты понимаешь, **почему** у массива такие сложности, а не просто заучил таблицу.
**Когда использовать массив:**
- ✅ Нужен быстрый доступ по индексу
- ✅ Данные редко вставляются/удаляются в середине
- ✅ Известен примерный размер заранее
**Когда НЕ использовать:**
- ❌ Частые вставки/удаления в начало
- ❌ Размер сильно колеблется
**Помнишь вопрос из начала?** Почему Netflix может хранить миллионы фильмов и мгновенно находить любой по ID? Потому что ID - это индекс, а доступ по индексу - O(1). Арифметика вместо поиска.
Массивы - устаревшая структура, лучше всегда использовать что-то сложнее
Массивы - основа большинства структур данных. O(1) доступ + cache locality делают их незаменимыми для многих задач. Даже HashMap внутри использует массив!
Ты строишь чат-приложение. Новые сообщения добавляются в конец, старые удаляются с начала. Подходит ли массив?
Анализ сложности Определи сложность каждой операции: ```python arr = [1, 2, 3, 4, 5] # Операция 1 x = arr[2] # Операция 2 arr.append(6) # Операция 3 arr.insert(0, 0) # Операция 4 for item in arr: print(item) ```
# Операция 1: O(1) - доступ по индексу # Операция 2: O(1) amortized - вставка в конец # Операция 3: O(n) - вставка в начало, сдвиг всех # Операция 4: O(n) - проход по всем элементам
Оптимизация алгоритма Этот код неэффективен. Почему? Как исправить? ```python def build_list_badly(n): result = [] for i in range(n): result.insert(0, i) # Вставляем в начало return result # При n = 100,000 работает очень медленно! ```
def build_list_well(n): result = [] for i in range(n): result.append(i) # O(1) вместо O(n) result.reverse() # Один проход O(n) return result # Или ещё проще: def build_list_pythonic(n): return list(range(n-1, -1, -1))
Выбор структуры данных Для каждого сценария определи: подходит ли массив? Почему? 1. **Лента Instagram**: показ постов сверху вниз, подгрузка новых сверху 2. **Рейтинг игроков**: топ-100, доступ по месту (1st, 2nd, 3rd...) 3. **Undo/Redo в редакторе**: последнее действие отменяется первым 4. **Кэш браузера**: быстрый доступ к странице по URL
# 1. Лента Instagram # ❌ Массив не идеален # Новые посты вставляются в начало - O(n) # Лучше: deque или связный список # 2. Рейтинг игроков # ✅ Массив идеален # Доступ по индексу (месту) - O(1) # Изменения редки # 3. Undo/Redo # ✅ Массив (как стек) # Добавление/удаление только с конца - O(1) # Стек - это массив с ограниченным интерфейсом # 4. Кэш браузера # ❌ Массив не подходит # Нужен поиск по ключу (URL), не по индексу # Лучше: хеш-таблица (dict)
Куда дальше?
Массив - фундамент. Вот что строится на нём:
- Связные списки — Решают проблему O(n) вставки ценой O(n) доступа
- Хеш-таблицы — Массив + хеш-функция = O(1) поиск по ключу
- Алгоритмы сортировки — Большинство алгоритмов сортировки работают с массивами
Ключевые идеи
- **Массив** - непрерывный блок памяти с O(1) доступом по индексу
- **Вставка/удаление** из середины - O(n) из-за сдвига элементов
- **Динамические массивы** растут x2, давая amortized O(1) append
- **Выбор структуры** зависит от частых операций, не от "моды"
- **Netflix находит профиль** за микросекунды потому что ID = индекс = O(1)
Вопросы для размышления
- Почему Python list - это динамический массив, а не связный список?
- Какие операции были бы быстрее/медленнее со связным списком?
- Помнишь вопрос из начала про Netflix? Теперь понимаешь, как это работает?
Связанные уроки
- prog-11-arrays — Базовые массивы в программировании - предпосылка для понимания их внутреннего устройства
- alg-01-big-o — Big O нотация нужна для понимания O(1) vs O(n) операций массива
- ds-02-linked-lists — Связные списки - прямая альтернатива массивам с разными компромиссами
- arch-01-binary — Адресация памяти компьютера - физическая основа концепции массива
- arch-08-memory-hierarchy