Стек, очередь и куча
Вопросы про стек, очередь и кучу проверяют одно умение: знать, что когда брать. Правильная структура превращает квадрат в линейный проход, и интервьюер смотрит именно на момент выбора.
Типовые формулировки: «топ-10 частых слов в большом файле», «через сколько дней потеплеет», «медиана потока чисел».
// Чаще всего валят на мифе «куча - это отсортированная структура». Это неправда, и на следующих карточках видно, почему.
Стек и его секретное оружие
Стек живёт по правилу «последним вошёл - первым вышел» и появляется везде, где что-то надо отложить и потом вернуться: проверка скобок, отмена действий, стек вызовов функций, обход в глубину. Скобки - канонический пример: открывающие складываем, на каждой закрывающей вершина обязана совпасть по типу, в конце стек должен опустеть.
Секретное оружие - монотонный стек, то есть стек, в котором значения намеренно держатся возрастающими или убывающими. Он решает за один проход целый класс задач «найди ближайший больший элемент справа».
Пройдём по температурам [3, 1, 4, 2], задача - через сколько дней станет теплее. Кладём в стек индексы. День 0 (3 градуса): стек пуст, кладём. День 1 (1 градус): вершина - тройка, она больше единицы, не трогаем, кладём. День 2 (4 градуса): единица меньше четвёрки - снимаем, для того дня ответ 2 − 1 = 1; тройка тоже меньше - снимаем, ответ 2 − 0 = 2; кладём. День 3 (2 градуса): вершина - четвёрка, больше двойки, просто кладём. В стеке остались дни без ответа, им ноль. Итог [2, 1, 0, 0].
// Почему это O(n), хотя внутри есть вложенный while: каждый индекс кладётся в стек ровно один раз и снимается не больше одного раза. Всего действий не больше 2n, независимо от того, как они распределились по шагам.
- монотонный стек
- стек, в котором поддерживается порядок значений. Нарушители выталкиваются, и в момент выталкивания как раз и находится ответ для них
Очередь: порядок поступления
Очередь работает по правилу «первым вошёл - первым вышел» и обрабатывает элементы в порядке прихода: обход графа в ширину, разбор дерева по уровням, буферы задач.
Есть классическая проверка на понимание: подмени в обходе в ширину очередь на стек, и он молча превратится в обход в глубину. Код останется рабочим, ошибки не будет, но обход перестанет идти по уровням, и кратчайшие пути сломаются - потому что кратчайший путь в невзвешенном графе гарантирован именно порядком уровней.
Deque - двусторонняя очередь, у которой обе операции на обоих концах стоят O(1). Нужна там, где приходится и добавлять, и выбрасывать с разных сторон: скользящий максимум в окне, история действий с ограниченной глубиной.
// Разминочный вопрос, который до сих пор задают: как сделать очередь из двух стеков. Ответ - один стек на вход, второй на выход; когда выходной пуст, всё содержимое входного переливается в него, разворачиваясь. Амортизированно O(1), потому что каждый элемент переливается ровно один раз.
Куча: экстремум, а не порядок
Куча - это двоичное дерево, у которого выполнено ровно одно условие: родитель не больше любого из своих детей (для кучи на минимум). Никаких требований к порядку между братьями нет.
Хранится она обычным массивом, без единой ссылки: дети элемента с индексом i лежат на позициях 2i+1 и 2i+2. Дерево получается сбалансированным по построению, поэтому его высота - log n, и отсюда сразу цена операций: вставка и снятие минимума стоят O(log n), потому что каждая из них поднимает или опускает один элемент вдоль пути от корня к листу.
Куча отвечает ровно на один вопрос - «дай минимум за O(log n), не сортируя всё остальное». На этом стоят топ-k, слияние k отсортированных списков, планировщики задач и медиана потока двумя кучами: в одной левая половина чисел максимумом вверх, в другой правая минимумом вверх, медиана всегда на стыке.
// Куча НЕ отсортирована, и это главный источник ошибок. Гарантия есть только у корня, а порядок соблюдается лишь вдоль путей от корня к листьям. Пройтись по массиву кучи подряд - совсем не то же самое, что перебрать элементы по возрастанию; полный порядок получается только последовательным выниманием. Ещё две неожиданности: построить кучу из готового массива можно за O(n), а не за O(n log n), зато найти в куче произвольный элемент стоит O(n) - она для экстремума, не для поиска.
- приоритетная очередь
- абстракция «выдай самый приоритетный элемент». Куча - её стандартная реализация, поэтому эти два слова часто употребляют как синонимы
Как отвечать: «Найди топ-10 самых частых слов в большом файле»
Сначала один проход по файлу со словарём частот. Дальше не сортирую весь словарь, а держу кучу на минимум размера десять: кладу туда пары «частота и слово», и как только размер переваливает за десять, снимаю минимум. В куче всегда остаётся текущая десятка, а наверху - кандидат на вылет. Получается O(n log 10), практически линейно, против O(n log n) у полной сортировки. Если файл не влезает в память, считаю частоты по кускам и сливаю их внешним слиянием, либо беру приближённые счётчики вроде count-min sketch, когда точность не критична. В Python heapq - это именно куча на минимум, что для задачи про топ-частые как раз удобно.
Паттерн «куча размера k», честно посчитанная сложность и заранее взятый поворот на «а если не влезает в память». Три уровня глубины вместо одного, и последний интервьюер обычно и хочет услышать.
На чём валят
- −Считать, что обход массива кучи подряд даёт элементы по возрастанию: порядок есть только вдоль путей корень-лист.
- −Искать произвольный элемент в куче: это O(n), куча приспособлена только к экстремуму.
- −Подменить очередь стеком в обходе в ширину: код работает, но это уже обход в глубину, и кратчайшие пути сломаны.
- −Забыть, что heapq в Python - только куча на минимум; для максимума кладут отрицательные значения или кортежи со знаком.
- −Сортировать весь массив ради топ-k, когда куча размера k делает то же дешевле.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 21, остальные разбираются в тренажёре.
- Очередь (FIFO) реализуют на двух стеках. Какова амортизированная сложность dequeue?A)O(n), потому что при каждом dequeue приходится перекладывать вообще все элементы обеих стопок все оставшиеся элементы очереди на одну позицию влевоB)O(log n)C)O(1) амортизированно: перекладывание in→out случается редко, каждый элемент переносится один разD)O(n²) в худшем случае
показать ответ и разбор
+C)O(1) амортизированно: перекладывание in→out случается редко, каждый элемент переносится один раз// разбор: Два стека: in (для enqueue) и out (для dequeue). dequeue берёт с out; если out пуст — переливаем весь in в out (порядок разворачивается в FIFO). Каждый элемент переливается ровно один раз за свою жизнь, поэтому суммарная работа на m операций — O(m), амортизированно O(1). Отдельный dequeue с переливом всё же O(n) — гарантия амортизированная.
- Задачи нужно обрабатывать строго в порядке поступления. Какая структура данных подходит?A)стекB)хеш-таблицаC)очередь (queue)D)куча (heap)
показать ответ и разбор
+C)очередь (queue)// разбор: Очередь — дисциплина FIFO: элементы выходят в порядке, в котором вошли. Это модель задач воркера, сообщений брокера, BFS-обхода графа. Стек (LIFO) — противоположность: последний вошёл — первый вышел. А если задачам нужен приоритет вместо порядка прихода — берут приоритетную очередь (кучу).
- Кнопка «отменить» (Ctrl+Z) в редакторе. В какой структуре хранить действия пользователя?A)в очереди — действия отменяются в порядке выполненияB)в куче — сначала отменяются самые «тяжёлые» действияC)в хеш-таблице — быстрый доступ к действию по названиюD)в стеке — отменяется последнее сделанное действие
показать ответ и разбор
+D)в стеке — отменяется последнее сделанное действие// разбор: Undo — классический стек: каждое действие кладётся сверху, отмена снимает верхнее, то есть последнее сделанное. Redo — второй стек, куда переезжают отменённые действия. Тот же паттерн везде, где нужен возврат в обратном порядке: стек вызовов функций, скобочные последовательности, откат транзакций.
- Нужно отдавать медиану потока чисел в любой момент. Какая структура?A)Отсортированный массив с бинарной вставкойB)Две кучи: max-куча меньшей половины и min-куча большейC)Хеш-таблица со счётчиками встреченных значенийD)Очередь: медиана — это середина по порядку прихода
показать ответ и разбор
+B)Две кучи: max-куча меньшей половины и min-куча большей// разбор: Держим две кучи почти равного размера: в max-куче меньшая половина чисел, в min-куче большая. Их вершины и зажимают середину. Вставка стоит O(log n), сама медиана достаётся за O(1). Отсортированный массив потребовал бы O(n) на сдвиг при вставке, а очередь говорит о порядке прихода, а не о величине.
- Очередь с приоритетом: задачи с равным приоритетом должны выходить в порядке поступления. Как это обеспечить?A)Никак: куча порядок вставки не хранитB)Класть в кучу пару (приоритет, порядковый номер)C)После извлечения досортировывать группу равных по времениD)Держать две очереди: одну по приоритету, другую FIFO
показать ответ и разбор
+B)Класть в кучу пару (приоритет, порядковый номер)// разбор: Куча сравнивает ключи и на равных не обещает ничего. Добавляем в ключ монотонно растущий счётчик: (приоритет, seq). При равном приоритете побеждает меньший seq, то есть тот, кто пришёл раньше. Тот же приём спасает, когда сами задачи сравнивать нельзя: до них сравнение просто не доходит.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.