1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6

# теория · шаг 1 из 6

Стек и очередь: LIFO и FIFO

Коротко

Стек отдаёт элементы в обратном порядке: последним пришёл - первым ушёл (LIFO). Операции push (положить) и pop (снять) работают с ОДНОГО конца, и на массиве обе O(1) - конец массива дёшев. Очередь отдаёт в порядке прихода: первым пришёл - первым ушёл (FIFO). enqueue кладёт в хвост, dequeue снимает с головы - с РАЗНЫХ концов. И тут ловушка: наивная очередь на массиве снимает с головы через pop(0), а это сдвиг всего хвоста - O(n). Как сделать очередь без сдвига - следующие два урока.

Если ты знаешь, что стек - это LIFO, а очередь - FIFO, почему стек на массиве даёт O(1), и в чём беда наивной очереди через pop(0), листай до «Теперь сам».

Как думаешь, в каком порядке структура отдаёт то, что в неё положили

До сих пор мы брали элемент по индексу или искали по значению. Но есть структуры, где ты вообще не выбираешь, какой элемент достать, - структура сама решает по ПОРЯДКУ. Их две, и различаются они одним: с какого конца отдавать. Стек отдаёт с того же конца, куда клал, - получается обратный порядок. Очередь отдаёт с другого конца - получается прямой. Оба ограничения не бедность, а сила: чем меньше структура обещает, тем дешевле её операции.

На что это похоже

Стек - стопка тарелок. Новую кладёшь сверху, и берёшь тоже сверху: последняя положенная уходит первой. Достать нижнюю, не сняв верхние, нельзя - и не надо, стек этого и не обещает. Очередь - живая очередь в кассу. Встал в хвост, ждёшь; обслуживают с головы, в порядке прихода. Кто пришёл раньше, уйдёт раньше. Две дисциплины: у стопки вход и выход с одного конца, у очереди - с разных.

Механизм: один конец против двух

Стек, дисциплина LIFO (last in, first out). Две операции: push(x) кладёт x наверх, pop() снимает верхний и возвращает его. Оба конца - один и тот же (вершина). На динамическом массиве стек ложится идеально: push - это append (в конец, амортизированно O(1)), pop - снятие с конца (O(1), сдвигать нечего). Вершина стека - конец массива, а конец у массива дёшев с обеих сторон.

Очередь, дисциплина FIFO (first in, first out). enqueue(x) кладёт x в хвост, dequeue() снимает с головы. Концы РАЗНЫЕ: кладём с одного, снимаем с другого. Вот тут массив и подводит. Хвост-то дёшев (append), а голова - это НАЧАЛО массива, и снять оттуда через pop(0) значит сдвинуть весь оставшийся хвост на позицию влево - O(n).

стек (LIFO) cba push/pop сверху ↓↑ очередь (FIFO) ab cd dequeue (голова) enqueue (хвост) стек: один конец → обратный порядок; очередь: два конца → прямой порядок
Стек кладёт и снимает с одного конца, очередь - с разных. На массиве стек дёшев весь, а у очереди голова упирается в pop(0).

Правило

Стек - дисциплина LIFO (последним пришёл - первым ушёл): push и pop с одного конца, на массиве обе O(1). Очередь - дисциплина FIFO (первым пришёл - первым ушёл): enqueue в хвост, dequeue с головы, с разных концов. Наивная очередь на массиве снимает с головы через pop(0) - это сдвиг всего хвоста, O(n) на операцию и O(n²) на серию. Стек массиву по руке, а очередь без ухищрений - нет.

Разбор: почему стек на массиве - это O(1)

Стек трогает только один конец - вершину, - и мы сажаем её на КОНЕЦ массива, где всё дёшево. push(x) - это a.append(x): добавление в конец, амортизированно O(1) (динамический массив с удвоением, доказали в прошлом разделе). pop() - это снятие последнего элемента: за ним ничего не лежит, сдвигать нечего, O(1). Ни одна операция стека не лезет в начало массива, поэтому дорогого сдвига не возникает вовсе. Серия из n операций стека - O(n). Стек и массив совпали по слабым местам: оба не любят начало, а стек в начало и не ходит.

Разбор: почему наивная очередь на массиве - это O(n²)

Очередь вынуждена трогать оба конца. Хвост дёшев: enqueue(x) - это снова append, O(1). А dequeue() снимает с ГОЛОВЫ, то есть с начала массива. Если делать это в лоб через a.pop(0), произойдёт то же, что при удалении первого элемента: вся оставшаяся часть сдвинется на позицию влево, чтобы закрыть дырку в начале, - O(длины). Проведи n пар «enqueue, dequeue»: каждый dequeue сдвигает то, что накопилось, суммарно выходит Θ(n²). Абстрактно очередь ничем не хуже стека - разница в одной операции, dequeue с головы, которую массив делает дорого. Значит, дело не в дисциплине FIFO, а в РЕАЛИЗАЦИИ головы: убрать сдвиг - и очередь снова станет O(1). Как - в следующих двух уроках.

Что ломается без этого

Главная ошибка - реализовать очередь «в лоб» списком с pop(0) и получить O(n²) там, где ждёшь линию. Это одна из самых частых причин внезапно медленного кода: очередь выглядит невинно, а pop(0) в цикле тихо превращает обработку в квадрат. Вторая ошибка - лечить это через pop(0) «поосторожнее» (реже вызывать и т. п.): проблема не в частоте, а в самом сдвиге головы, и лечится она сменой реализации, а не экономией вызовов. Стек такой беды не знает - но именно потому, что не трогает начало.

Зачем это в работе

Стек и очередь - рабочие лошади программирования. Стек держит порядок вложенных дел: стек вызовов функций, отмена действий (undo), проверка скобок, обход в глубину. Очередь держит порядок ожидания: задачи на обработку, сообщения между сервисами, обход в ширину. В Python стек делают прямо на list (append/pop - обе O(1)), а вот очередь на list через pop(0) - классическая ошибка, поэтому для очереди берут collections.deque, где снятие с головы O(1). Понимать, ПОЧЕМУ list не годится для очереди, - значит не наступить на этот квадрат в реальной задаче.

Теперь сам

Ты кладёшь в структуру числа 1, 2, 3, а потом снимаешь три раза. В каком порядке они выйдут, если структура - стек? Если очередь? И почему стек на list (append/pop) даёт O(1) на операцию, а очередь на list через pop(0) - нет?

Ответ. Стек (LIFO) отдаёт с того же конца, куда клал: выйдут 3, 2, 1 - обратный порядок. Очередь (FIFO) отдаёт с другого конца: выйдут 1, 2, 3 - порядок прихода. Стек на list дёшев, потому что трогает только конец массива: push = append (O(1) аморт.), pop = снятие с конца (O(1), сдвигать нечего). Очередь на list дёшева на enqueue (append в хвост), но dequeue через pop(0) снимает с начала массива - а это сдвиг всего хвоста влево, O(n); на серии - O(n²). Беда не в FIFO, а в дорогой голове массива.

Главное

Стек - дисциплина LIFO: push/pop с одного конца, на массиве обе O(1) (конец массива дёшев, а стек в начало не ходит). Очередь - дисциплина FIFO: enqueue в хвост (O(1)), dequeue с головы. Наивная очередь на массиве снимает голову через pop(0) - сдвиг всего хвоста, O(n) на операцию, O(n²) на серию. Беда не в дисциплине FIFO, а в реализации головы: убрать сдвиг - и очередь снова O(1). Стек делают на list, для очереди берут deque.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.