Экспресс-курс · No. 23
Структура данных — это способ сложить данные так, чтобы то, что ты делаешь чаще всего, было быстрым. Выбрал правильную — операция мгновенная; выбрал неверную — та же задача ползёт. Big-O — язык для этой разницы: как растёт цена операции, когда растут данные.
16 понятий · схема у каждого · шпаргалка в конце
До конкретных структур — идея, общая для всех. Структура данных — не академия. Это тот самый выбор, который чаще всего решает, будет код быстрым или мучительно медленным.
Карта курса: 16 понятий в шести группах
Карта метро: сначала видишь все линии, потом едешь по станциям.
Сверху — три базовые идеи: Data Structure, Trade-off и Big-O. Дальше шесть групп: формы роста Big-O, массив, хеш-таблица и множество, структуры с порядком (стек и очередь), структуры с формой (дерево и граф) и, наконец, как всё это выбирать на практике.
Как читать курс
Паспорт: одинаковые поля у всех, поэтому их легко сравнивать.
У каждого понятия одно и то же: английское название (его ты встретишь в документации и на собеседовании), образ из жизни, схема и карточка: что это, как работает, пример, цена и связи с другими структурами. Главное правило курса: лучшей структуры не бывает — бывает подходящая под твою частую операцию.
Data Structure — структура данных
Ящик с инструментами, где у каждого своё гнездо, против мешка, куда всё свалено. Инструменты те же, но из ящика нужный берёшь сразу, а в мешке каждый раз роешься.
Структура данных (data structure) — это способ расположить данные в памяти. Разные расположения делают быстрыми разные операции: одно отлично ищет по имени, другое держит порядок, третье всегда отдаёт самый свежий элемент. Вся игра — подобрать структуру под то, что ты делаешь чаще всего.
Та же задача бывает тривиальной или почти невозможной — смотря как лежат данные. Найти слово в отсортированном словаре — секунды; в тех же словах, сваленных как попало, — целый день. Поэтому удивительно много «сделай быстрее» на деле значит «храни в правильной форме». Структура и есть решение.
- Что это
- Способ расположить данные в памяти под определённые операции.
- Как работает
Расположение заранее решает, какие операции будут дешёвыми, а какие — дорогими.
- Пример
Найти пользователя по ID: в одной структуре — мгновенно, в другой — перебор всех.
- Цена
- Неверный выбор не лечится «хитрой оптимизацией» потом.
- Связи
Скорость структур сравнивают через Big-O; выбирают через Trade-off.
Trade-off — каждая структура это размен
Спорткар и грузовик для переезда — оба транспорт. Ты не спрашиваешь, какой лучше; ты спрашиваешь: лучше для чего?
Ни одна структура не лучшая во всём. Одна даёт мгновенный поиск, но без порядка; другая держит порядок, но медленно ищет; третья быстро добавляет, но медленно находит что-то внутри. Это и есть trade-off (размен): сила в одной операции оплачена слабостью в другой. Бесплатного обеда нет — твоя работа выбрать размен, который подходит твоему случаю.
- Что это
- Сила структуры в одной операции всегда стоит слабости в другой.
- Пример
Hash Map ищет по ключу мгновенно, но не хранит порядок; Array хранит порядок, но медленно ищет по значению.
- Когда думать
Каждый раз, когда выбираешь, где хранить данные: что я делаю часто, а что редко?
- Связи
- Размен меряется в Big-O — у каждой операции своя «форма».
Структура данных раскладывает данные так, чтобы частые операции были быстрыми. Правильная форма делает сложную задачу лёгкой — и каждая структура платит слабостью в одном за силу в другом.
Чтобы сравнивать структуры, нужен язык скорости, который не зависит от компьютера. Big-O и есть этот язык: он меряет, как растёт работа, когда растут данные.
Big-O notation — рост, а не секунды
Вопрос не «сколько длится одна доставка», а «как меняется общее время, когда в маршруте не десять остановок, а десять тысяч». Форма роста, а не показания секундомера.
Нотация Big-O описывает, как растёт цена операции, когда растёт объём данных — его обозначают n. Она не смотрит на миллисекунды и конкретную машину, а ловит форму: удвоили данные — работа удвоилась, учетверилась или почти не изменилась? Эта форма и решает, останется ли код быстрым на больших данных. Поэтому Big-O — способ сравнить подходы, даже не запуская их.
- Что это
- Описание того, как растёт работа при росте данных n.
- Как работает
Отбрасываем точные секунды и детали железа, оставляем форму роста.
- Пример
«Поиск по массиву — O(n)»: в десять раз больше элементов — примерно в десять раз больше работы.
- Подвох
- Не говорит, сколько секунд займёт; говорит, как это масштабируется.
- Связи
- Основные формы — в Complexity classes.
Complexity classes — формы от отличной до ужасной
Четыре способа, как растёт работа по дому: одна остаётся одним делом, сколько бы ни было вещей; другая растёт вместе с кучей; третья взрывается как куча в квадрате; четвёртая почти не растёт, даже когда куча растёт.
Несколько форм покрывают почти весь код:
- O(1) — константа: время одно и то же при любом размере (взять элемент по индексу).
- O(log n) — логарифм: почти не растёт, даже когда данных становится очень много (каждый шаг делит поиск пополам).
- O(n) — линейная: растёт вместе с данными (просмотреть весь список).
- O(n²) — квадратичная: взрывается (сравнить каждый элемент с каждым). Классика «в тестах нормально, в проде умирает».
Для десяти элементов любая структура кажется мгновенной — разница не видна. Big-O — про то, что будет, когда n станет большим. Поэтому код «вдруг» тормозит в проде: плохая форма была там всегда, просто пряталась, пока данные не выросли.
- Что это
- Названия типичных форм роста работы.
- Пример
Индекс в массиве — O(1); поиск в сбалансированном дереве — O(log n); перебор списка — O(n); цикл в цикле — O(n²).
- Подвох
O(n²) прекрасно работает на тестовых данных и молотит минутами на миллионе записей.
- Правило
O(1) и O(log n) переживают масштаб; O(n) честная; O(n²) — сигнал тревоги.
- Связи
- Map before loop — главный способ превратить O(n²) в O(n).
Big-O меряет, как растёт работа вместе с данными, а не секунды. O(1) и O(log n) переживают масштаб; O(n) честная; O(n²) нормальна в тестах и умирает в проде.
Самая базовая структура — массив, или список: элементы по порядку. Если точно знать, в чём он быстр и в чём медленен, всё остальное легко сравнивать с ним.
Array — упорядоченная нумерованная последовательность
Ряд пронумерованных шкафчиков: вещи стоят по порядку, у каждой свой номер — 0, 1, 2… — и к любому шкафчику можно подойти сразу по номеру.
Массив (array, list) хранит элементы по порядку, каждый на своей позиции — индексе (index), начиная с 0. Элементы лежат подряд, в известном месте памяти, поэтому прыгнуть к любой позиции по индексу — мгновенно, O(1). Массив — выбор по умолчанию для «кучи вещей по порядку», а доступ по позиции — его суперсила.
Но есть цена. Добавить или убрать элемент в конце — дёшево. А вставка или удаление в середине сдвигает всё, что после, чтобы сохранить порядок и нумерацию, — это O(n).
- Что это
- Элементы по порядку, у каждого номер-индекс с нуля.
- Быстро
- Доступ по индексу — O(1); добавление в конец — дёшево.
- Медленно
- Поиск по значению — O(n); вставка и удаление в середине — O(n).
- Пример
- Лента постов, которую читают по порядку и дополняют в конец.
- Когда брать
- Читаешь по позиции и в основном добавляешь в конец.
- Связи
Его слабость — поиск по значению — закрывает Hash Map; Stack и Queue часто строятся поверх него.
Linear search — перебор по значению
Шкафчик 500 открываешь мгновенно. Но если знаешь только, что лежит внутри, а не номер, — придётся открывать один за другим, пока не найдёшь.
Подвох массива: он быстр, только когда знаешь индекс. Чтобы найти элемент по содержимому — «есть ли этот email в списке?» — нужно проверить каждый по очереди. Это линейный поиск (linear search), O(n): на больших списках — медленно. Ровно эту дыру закрывает следующая структура.
- Что это
- Проверить элементы по одному, пока не найдёшь нужный.
- Цена
- O(n): в десять раз больше данных — в десять раз дольше.
- Пример
emails.includes(x)на списке из миллиона адресов.- Подвох
Внутри другого цикла превращается в O(n²) — тот самый «тормоз в проде».
- Связи
- Заменяется поиском в Hash Map или Set за O(1).
Массив — упорядоченная последовательность с индексами: мгновенный доступ по позиции, но O(n) на поиск по значению и вставку в середину.
Если какая-то структура заслуживает быть твоей рабочей лошадкой по умолчанию — это хеш-таблица. Она закрывает главную слабость массива — поиск по значению — и делает это почти мгновенно.
Hash Map — пары ключ-значение
Телефонная книга: ищешь человека по имени и сразу получаешь номер. Ты не листаешь все записи — имя ведёт прямо к ответу.
Хеш-таблица (hash map; ещё dictionary, hash table или просто map) хранит данные как пары ключ-значение (key-value): даёшь ключ — получаешь значение. Пользователь по ID, настройка по имени, счётчик по слову — напрямую, по ключу. Массив заставляет знать позицию; хеш-таблица даёт искать по осмысленному имени.
- Что это
- Пары ключ → значение с поиском по ключу.
- Быстро
- Поиск, добавление и удаление по ключу — фактически O(1).
- Медленно
- Порядка нет; найти «по значению», а не по ключу — снова перебор.
- Пример
users[id],settings["theme"], подсчёт словcounts[word] += 1.- Когда брать
- Постоянно ищешь что-то по идентификатору.
- Связи
- Скорость даёт Hash function; без значений превращается в Set.
Hash function — почему поиск мгновенный
Волшебный шкаф: по метке сразу открывается нужный ящик, другие он не проверяет. Сколько ящиков — неважно.
Магия хеш-таблицы в том, что поиск по ключу фактически O(1) — постоянное время, сколько бы элементов в ней ни было. Ключ прогоняют через хеш-функцию (hash function): она вычисляет, где лежит значение, и таблица прыгает прямо туда, а не ищет. Миллион записей или десять — найти одну одинаково быстро. Поэтому хеш-таблица и рабочая лошадка: «найди по имени» — то, что программы делают постоянно.
- Что это
- Функция, которая по ключу вычисляет место значения в таблице.
- Как работает
Один и тот же ключ всегда даёт одно и то же место — поэтому поиск не перебирает записи.
- Пример
hash("eve")→ ячейка 3 → там лежит номер Евы.- Связи
- Сердце Hash Map и Set.
Set — есть или нет, без значений
Список гостей на входе: детали не нужны, только «да или нет» — есть это имя в списке? Проверено мгновенно, без листания.
Близкий родственник — множество (set): хеш-таблица, которая хранит только ключи, без значений, чтобы быстро отвечать на один вопрос — «это здесь есть?». Проверка присутствия и защита от дубликатов — обе O(1). Каждый раз, когда ловишь себя на переборе списка ради «я это уже видел?», множество превращает медленную O(n)-проверку в мгновенную.
- Что это
- Набор уникальных ключей без значений.
- Быстро
- Проверка «есть ли?» и добавление — O(1); дубликаты отбрасываются сами.
- Пример
seen = set()— пропускать письма, которые уже отправили; убрать повторы из списка.- Когда брать
- Уникальность и быстрые проверки присутствия.
- Связи
- Это Hash Map без значений.
Хеш-таблица находит значение по ключу фактически за O(1) — рабочая лошадка для поиска по идентификатору. Её родственник Set так же мгновенно отвечает «это здесь есть?».
Иногда порядок, в котором вынимаешь вещи, важен не меньше того, что ты хранишь. Две структуры закрепляют два естественных порядка — и разница между ними в одном простом правиле.
Stack — последним пришёл, первым ушёл (LIFO)
Стопка тарелок: кладёшь сверху и берёшь сверху. Последняя положенная — первая, которую возьмёшь.
Стек (stack) работает по правилу LIFO — last in, first out (последним пришёл — первым ушёл). Добавляешь (push) и убираешь (pop) с одного конца, поэтому первым выходит самый свежий элемент. Это подходит всему, что вкладывается или разматывается в обратном порядке: история отмены (undo), кнопка «назад», стек вызовов программы (call stack). Обе операции — O(1).
- Что это
- Добавляем и убираем только с вершины.
- Быстро
pushиpop— O(1).- Пример
- Undo в редакторе, кнопка «назад» в браузере, call stack.
- Когда брать
- Нужен порядок «самое свежее первым» или «отмени последнее».
- Связи
- Зеркало Queue: те же данные, другой конец для выдачи.
Queue — первым пришёл, первым ушёл (FIFO)
Очередь в кассу: обслуживают в порядке прихода, новые встают в хвост, следующим идёт тот, кто впереди.
Очередь (queue) работает по правилу FIFO — first in, first out (первым пришёл — первым ушёл). Добавляешь в хвост (enqueue), забираешь спереди (dequeue), поэтому элементы обрабатываются в порядке прихода. Это структура для честности и порядка: задачи на обработку, ждущие работы, запросы по очереди — та же идея, что у очереди сообщений из курса про очереди. Как и у стека, операции — O(1).
- Что это
- Добавляем в хвост, забираем с головы.
- Быстро
enqueueиdequeue— O(1).- Пример
- Задачи для фоновых воркеров, запросы, которые обслуживают по очереди.
- Когда брать
- Порядок прихода должен стать порядком обработки.
- Связи
- Зеркало Stack; в большом масштабе вырастает в message queue.
Правило определяет поведение
Одна и та же куча работы, две двери: одна отдаёт самое свежее, другая — самое старое. Один выбор — с какого конца брать — меняет всё дальше.
Стек и очередь хранят одно и то же; вся разница — с какого конца забираешь. Выбирать между ними — значит выбирать порядок, в котором работа к тебе возвращается: обратный и вложенный (стек) или честный и последовательный (очередь). Маленькое решение с большим эффектом: угадай порядок — и остальная логика встанет на место.
Stack — последним пришёл, первым ушёл: отмена, вложенность, разворот. Queue — первым пришёл, первым ушёл: честная обработка по порядку. Данные те же; конец, с которого забираешь, — вся разница.
Две последние структуры — для данных, которые не лежат в одну линию, а имеют форму: ветвятся в иерархию или сплетаются в сеть. Так моделируют связи между вещами.
Tree — иерархия
Семейное древо или оргструктура компании: одно наверху, дальше ветвится вниз на детей, у тех свои дети. Расходится веером и никогда не замыкается обратно.
Дерево (tree) хранит данные как иерархию: сверху корень (root), от него ветвятся узлы (nodes) с детьми, и так вниз. Оно моделирует всё вложенное — папки в папках, комментарии и ответы на них, меню категорий. Форма дерева повторяет реальную вложенность, поэтому оно естественный выбор, когда данные — «вещи внутри вещей». Большинство иерархий в софте — деревья под капотом.
- Что это
- Иерархия: у каждого узла есть дети, но нет петель назад.
- Пример
- Папки и файлы, ветка комментариев, дерево категорий в магазине.
- Когда брать
- Данные устроены как «вещи внутри вещей».
- Подвох
Если впихнуть иерархию в плоский список, придётся склеивать её обратно руками.
- Связи
Упорядоченное и ровное дерево — Balanced tree; дерево — частный случай Graph.
Balanced tree — поиск за O(log n)
Ищешь имя в телефонной книге: открываешь середину, потом середину нужной половины, и снова. Каждый шаг выкидывает половину оставшегося.
Хорошо организованное, сбалансированное (balanced) дерево даёт мощное свойство: в нём можно искать за O(log n), каждый раз деля пространство пополам — как ищешь слово в словаре, не читая каждую страницу. Логарифм почти не растёт, даже когда данные взрываются: миллиард элементов — всего около тридцати шагов. Так базы данных держат поиск быстрым на огромном масштабе: упорядоченные данные в форме дерева, поиск делением пополам.
- Что это
- Упорядоченное дерево одинаковой глубины со всех сторон.
- Как работает
На каждом узле сравниваем и уходим в одну половину — вторую больше не смотрим.
- Быстро
- Поиск — O(log n): миллиард элементов ≈ 30 шагов.
- Пример
- Индексы в базах данных: упорядоченные ключи в форме дерева.
- Связи
- Частный случай Tree; в отличие от Hash Map хранит порядок.
Graph — сеть связей
Карта городов, соединённых дорогами, или люди, связанные дружбой: всё связано со всем в любом узоре, без верха и низа.
Граф (graph) — самая общая форма: узлы (nodes), соединённые рёбрами (edges), в любом узоре, включая петли. Он моделирует сети — социальные связи, дороги между городами, ссылки между страницами, зависимости между задачами. Если данные по сути «вещи, связанные с другими вещами», а не иерархия и не линия, — это граф. Многие сложные задачи — кратчайший маршрут, кто с кем связан — в основе графовые.
- Что это
- Узлы и рёбра между ними — без иерархии, петли разрешены.
- Пример
Друзья в соцсети, дороги между городами, ссылки между страницами, зависимости задач.
- Типичные задачи
- Кратчайший маршрут; кто с кем связан.
- Когда брать
- Данные — сеть, а не линия и не иерархия.
- Связи
- Tree — это граф без петель с одним корнем.
Tree моделирует иерархию и, если сбалансировано, ищет за O(log n) делением пополам. Graph моделирует сеть связей. Бери их, когда у данных есть форма — вложенная или связанная.
С нуля эти структуры почти никто не пишет — они уже есть в твоём языке. Навык — выбрать правильную под задачу и знать Big-O настолько, чтобы почуять плохой выбор до того, как он укусит.
Dominant operation — выбирай по частой операции
Кухню собирают под то, что готовят каждый вечер, а не под то, что эффектно смотрится.
Практическое решение почти всегда одно: какую операцию я делаю чаще всего, и какая структура делает именно её быстрой? Постоянный поиск по ключу — Hash Map. Порядок прихода — Queue. В основном чтение по позиции — Array. Пусть доминирующая операция (dominant operation) выбирает структуру, а с медленностью в редких операциях смирись. Оптимизируй частый случай — структура и есть способ.
- Идея
- Структуру выбирает самая частая операция, а не привычка.
- Пример
Тысячи поисков пользователя по ID в секунду → Hash Map, даже если порядок теряется.
- Цена
- Редкие операции станут медленнее — это осознанный размен.
- Связи
- Применение Trade-off на практике.
Map before loop — сначала таблица, потом цикл
Прежде чем искать человека в толпе по лицам, проверь, нет ли списка гостей: список отвечает мгновенно на то, что поиск делает медленно.
Очень частый и очень полезный ход: когда ловишь себя на переборе списка, чтобы найти или сопоставить что-то, — O(n)-цикл или, хуже, O(n²)-цикл внутри цикла, — спроси, не сделает ли это мгновенным хеш-таблица или множество. Замена вложенного поиска на поиск по таблице — одно из самых частых реальных ускорений. Чинит обычно структура, а не более быстрый цикл.
- Проблема
Для каждого элемента перебираем другой список — O(n²), «тормоз в проде».
- Идея
Один раз построить Hash Map или Set, дальше искать в нём за O(1).
- Пример
Вместо
users.find(u => u.id === order.userId)в цикле по заказам — заранееbyIdиbyId[order.userId].- Цена
- Немного памяти под таблицу.
- Связи
- Превращает Linear search внутри цикла в O(1).
- Какая операция главная — поиск, порядок, добавление/удаление?
- Поиск по ключу? Hash Map (или Set для «есть ли») — O(1).
- Доступ по позиции и добавление в конец? Подходит Array.
- Важен порядок выдачи — самое свежее (Stack) или самое старое (Queue)?
- Вложенные или связанные данные — Tree или Graph?
- Какой Big-O на большом n — и нет ли где-то скрытого O(n²)?
- Выбираешь структуру по частой операции, а не по привычке.
- Берёшь Hash Map или Set вместо перебора списка, чтобы найти или убрать дубликаты.
- Можешь назвать Big-O своего горячего пути — и в нём нет скрытого O(n²).
- Берёшь Stack или Queue, когда важен порядок выдачи, и Tree или Graph, когда у данных есть форма.
- Код остаётся быстрым, когда данные растут, — потому что ты выбрал форму раньше, чем масштаб её вскрыл.
Как всё связано
Генеалогия инструментов: каждый следующий закрывает слабость предыдущего.
Big-O — линейка, которой меряют все структуры. Hash Map закрывает слабость Array (поиск по значению), Set — это Hash Map без значений. Stack и Queue — последовательность с правилом «забирай только с конца». Tree даёт иерархию, а сбалансированное — поиск за O(log n); Graph снимает последнее ограничение и разрешает любые связи.
Шпаргалка: все 16 понятий
Основы
- Data Structureструктура данных
Способ расположить данные так, чтобы частые операции были быстрыми.
- Trade-offразмен
Сила в одной операции стоит слабости в другой.
Big-O
- Big-O notation«О большое»
Как растёт работа, когда растут данные n.
- Complexity classesO(1), O(log n), O(n), O(n²)
Константа, логарифм, линейная, квадратичная. O(n²) — тревога.
Массив
- Arrayмассив, list
По порядку, с индексом: по позиции O(1), вставка в середину O(n).
- Linear searchперебор
Ищем по значению, проверяя по одному. O(n).
Хеш-таблица
- Hash Mapdictionary, map
Ключ → значение, поиск по ключу фактически O(1). Без порядка.
- Hash functionхеш-функция
Вычисляет по ключу, где лежит значение, — поэтому без перебора.
- Setмножество
Только ключи: «есть ли?» за O(1), без дубликатов.
Порядок
- StackLIFO
Последним пришёл — первым ушёл. Undo, «назад», call stack.
- QueueFIFO
Первым пришёл — первым ушёл. Обработка в порядке прихода.
Форма
- Treeдерево
Иерархия: корень, узлы, дети, без петель.
- Balanced treeсбалансированное дерево
Поиск делением пополам — O(log n). Так работают индексы баз.
- Graphграф
Узлы и рёбра в любом узоре. Сети, маршруты, зависимости.
Как выбирать
- Dominant operationчастая операция
Структуру выбирает самая частая операция.
- Map before loopтаблица вместо цикла
Перед вложенным перебором — построй Hash Map или Set.
Ты не строишь их с нуля — ты их выбираешь. Подбирай структуру под частую операцию, тянись к таблице раньше, чем к циклу, и пусть Big-O предупредит тебя раньше, чем масштаб.