Узнайте, как монотонный стек превращает O(n²) в O(n). Разбор на примерах Next Greater Element и Largest Rectangle. Напишите код за 5 минут!
Вы когда-нибудь застревали на задаче вроде «найти следующий больший элемент» или «наибольший прямоугольник в гистограмме»? Первая мысль — вложенные циклы, O(n²) и страх перед большими данными. Но есть способ сделать это за O(n) — монотонный стек. Он превращает хаос перебора в элегантный проход по массиву. Готовы открыть для себя этот чит-код?
Монотонный стек — это обычный стек, элементы в котором хранятся в строго монотонном порядке (возрастающем или убывающем) по мере прохода по массиву. Каждый элемент помещается в стек один раз и извлекается не более одного раза. Итоговая сложность — O(n), хотя внутри может быть много операций извлечения.
Рассмотрим классическую задачу Next Greater Element: для каждого индекса i найти первый элемент справа, который больше nums[i]. Если идти слева направо и поддерживать убывающий стек (вершина — наименьший), то:
Каждый элемент может быть извлечён только тогда, когда встретит большего соседа — никакой лишней работы. Стек никогда не содержит более O(n) элементов, и каждый элемент испытывает не более одного помещения и одного извлечения. В этом суть: стек фиксирует причинно-следственную связь — как только элемент заблокирован большим, его судьба решена навсегда.
Та же идея работает для предыдущего меньшего, следующего меньшего, наибольшего прямоугольника в гистограмме, биржевого спреда и многих других. Меняется только тип стека (возрастающий или убывающий) и то, что мы записываем при извлечении.
def next_greater_brute(nums):
res = [-1] * len(nums)
for i in range(len(nums)):
for j in range(i+1, len(nums)):
if nums[j] > nums[i]:
res[i] = nums[j]
break
return res
Два вложенных цикла — если массив убывает, для каждого индекса сканируем весь хвост. Ужасно.
def next_greater_element(nums):
res = [-1] * len(nums)
stack = [] # храним индексы, nums[stack] убывает
for i in range(len(nums)):
# Извлекаем все индексы, для которых nums[i] — следующий больший
while stack and nums[i] > nums[stack[-1]]:
idx = stack.pop()
res[idx] = nums[i]
stack.append(i)
return res
Почему это работает: стек хранит индексы, у которых ещё нет следующего большего, и они расположены в порядке убывания значений. Когда новое значение нарушает этот порядок, оно становится первым большим справа для всех меньших индексов — именно то, что нужно. Каждый индекс помещается один раз и извлекается не более одного раза → O(n) времени, O(n) памяти.
Задача «наибольший прямоугольник в гистограмме» — любимица интервьюеров. Наивное решение (проверка всех пар границ) — O(n²). Монотонный стек (на этот раз возрастающий) — O(n).
def largest_rectangle_area(heights):
stack = [] # индексы с возрастающими высотами
max_area = 0
extended = heights + [0] # дозорный, чтобы опустошить стек
for i in range(len(extended)):
while stack and extended[i] < extended[stack[-1]]:
height = extended[stack.pop()]
# ширина: если стек пуст, прямоугольник тянется от начала
width = i if not stack else i - stack[-1] - 1
max_area = max(max_area, height * width)
stack.append(i)
return max_area
Распространённые ловушки:
Когда вы освоите паттерн монотонного стека, целый класс задач «сканируем один раз, разрешаем позже» станет тривиальным. Например:
Все эти задачи решаются за O(n) времени и O(n) памяти. Код настолько короток, что его можно написать на доске за минуту, а объяснение простое: «Я поддерживаю стек в монотонном порядке; когда порядок нарушается, я нахожу ответ для всех извлечённых элементов».
Попробуйте сами: дан массив nums, найдите максимальное значение nums[j] - nums[i] при i < j и nums[i] ≤ nums[j] (максимальная прибыль от одной сделки, где можно продать только после покупки). Адаптируйте монотонный стек для решения за O(n). Напишите решение в комментариях — давайте продолжим приключение! 🚀
Хочешь закрепить знания на практике?
Решай задачи на Algolit — интерактивная платформа для обучения
Начать бесплатно →