Стеки, очереди и амортизированный анализ

Стек, кольцевая очередь, рост массива и цена последовательности операций. От инвариантов LIFO/FIFO к потенциалу и очереди на двух стеках.

01

Стек: последним вошёл — первым вышел

В редакторе выполнены действия «написать 4», «написать 7», затем отмена: должно исчезнуть 7. После нового действия 2 история содержит [4,2]. Команды push(4), push(7), pop(), push(2) делают именно это: последнее действие отменяется первым.

После оценки числа действий из лекции 01 перейдём к хранению данных. Во многих задачах важно, в каком порядке брать элементы. Начнём с самого простого правила — извлекать последний добавленный элемент.

Выполните push(4), push(7), pop(), push(2). Что получилось?

Стек (Stack) — Контейнер с операциями push(x) и pop(): удаляется последний добавленный элемент (LIFO).

Стек удобно хранить как заполненный префикс массива. push записывает элемент в конец и увеличивает размер; pop уменьшает размер и возвращает последний элемент. При достаточной ёмкости обе операции занимают O(1).

Операции стека
4[0]
7[1]
2[2]

Вершина справа. Доступен последний добавленный элемент.

Стек на массиве
push(x):
    A[n] = x
    n = n + 1

pop():
    assert n > 0
    n = n - 1
    return A[n]

Проверка пустоты нужна, если операция допустима не всегда.

Область ответственностиЕсли массив ограничен, нужно обработать переполнение или заранее гарантировать вместимость. Амортизированное расширение обсуждается далее.

Для [4, 7, 2] вершина равна 2: pop возвращает 2, затем 7. Инвариант массива прост: A[0..n) хранит живые элементы, а A[n] — следующее свободное место. top только читает A[n−1], не уменьшая n. На пустом стеке top и pop требуют отдельного соглашения об ошибке.

В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

Отмена последнего действия

В редакторе команд последнее действие отменяется первым. Храните историю в стеке, а не ищите последний элемент по всему контейнеру. Если операция undo должна восстанавливать данные, вместе с командой сохраните достаточно информации для обратного действия.

Стек выбирает конец, с которого доступны данные. Сменим это правило на порядок поступления и посмотрим, почему обычного массива со сдвигами недостаточно.

Примените алгоритм к новому примеру

Задания раздела

push(4), push(7), pop(), push(2): что вернул pop?

Стек от дна к вершине после этих команд?

Попыток: 0
02

Очередь: первым вошёл — первым вышел

Этот раздел откроется после предыдущих проверок.

Продолжить: Стек: последним вошёл — первым вышел →

Добавим 4, затем 7, затем 2. Стек извлечёт 2, очередь — 4. Здесь одинаковые данные и команды, а разница только в правиле выбора следующего. Сравните также минимум из лекции 02, если уже знакомы с кучей.

Одинаковые команды: стек, очередь и минимум

Следующая команда: add(4). Перед remove предскажите три ответа: последний, самый старый и минимальный.

Стек, вершина справа

Пустой массив

Очередь, голова слева

Пустой массив

Очередь с приоритетами, показаны живые значения

Пустой массив

. Ответы remove (стек / очередь / минимум): ещё нет.

После add(4), add(7), add(2) какой remove будет первым?

У стека открыт один конец. Что изменится, если обслуживать элементы в порядке поступления? Например, запросы к серверу должны уходить в обработку от старых к новым: здесь нужен FIFO, а не LIFO.

Очередь (Queue) — Контейнер с enqueue(x) и dequeue(): удаляется самый давно добавленный элемент (FIFO).

Простая реализация удаляет A[0] и сдвигает остаток: для [4,7,2] приходится переместить 7 и 2. Если удалять так много раз из длинной очереди, набирается квадратичная работа. Оставим значения на месте и будем двигать только индекс head; освобождённые ячейки научимся использовать повторно.

Можно хранить элементы массива между указателями head и tail. Если оба указателя только увеличиваются, даже небольшая очередь со временем доберётся до края памяти.

Решение: интерпретировать массив как кольцевой буфер, заменяя переход за правую границу переходом к нулевой ячейке.

Кольцевой буфер
4 ← head[0]
7[1]
2[2]
·[3]
·[4]
·[5]

head = 0; count = 3; следующая запись = 3.

Храним head и count. Свободное место для записи вычисляем по модулю ёмкости.

next(i) = (i + 1) mod capacity

Инвариант: элементы очереди находятся в циклическом диапазоне длиной count от head.

Как отличить пустой буфер от полного
Если хранить только head и tail, то состояние head = tail неоднозначно. Можно хранить count либо оставлять одну ячейку неиспользованной. Демонстрация использует count.
Структураpush / enqueuepop / dequeue
СтекВ конецИз конца
ОчередьВ хвостИз головы

Одних head и tail недостаточно, чтобы различить пустой и полный буфер: после полного оборота они могут совпасть. В этой лекции храним count отдельно. Запись идёт в (head + count) mod capacity, чтение — из head. При count = capacity сначала расширяем буфер или отклоняем добавление: молча затирать живой элемент нельзя.

Буфер ёмкости 6: head=5, count=2. Чему равен head после одного извлечения?

В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

Обработка заявок по порядку

В симуляции очередь задаёт порядок обслуживания. При q до 200 000 удаление первого элемента массива со сдвигом может дать Θ(q²); кольцевой буфер или deque оставляют суммарную работу линейной.

Кольцевая индексация убрала сдвиги. Теперь отделим механизм хранения от задачи: используем стек для проверки вложенности.

Примените алгоритм к новому примеру

Задания раздела

После add(4),add(7),add(2) сравните первый remove.

capacity=6, head=5, count=2. Новый head после dequeue?

Как отличить пустой от полного при head==tail?

Попыток: 0
03

Для чего нужен стек

Этот раздел откроется после предыдущих проверок.

Продолжить: Стек: последним вошёл — первым вышел →

Порядок извлечения становится полезным, когда сама задача вложенная. Рассмотрим строку ([{}]): последней открылась фигурная скобка, поэтому именно она должна закрыться первой. Стек хранит ещё не завершённые обязательства.

Пройдём ([{}]) посимвольно: сначала открытия (, [, {. Затем } ожидает { и снимает её; ] снимает [; ) снимает (. Для ([)] после ( и [ символ ) ожидает (, но вершина [. Поэтому ответ отрицательный, хотя открытий и закрытий поровну.

Скобки: предскажите ожидаемое открытие

Строка ([{}]); прочитано 0 из 6. Текущий символ: (.

Пустой массив

Во время вызова функции сохраняются адрес возврата и данные текущего вызова: поэтому цепочка рекурсивных вызовов естественно имеет дисциплину LIFO.

Другое применение — разбор вложенных конструкций, например нескольких видов скобок. Каждая закрывающая скобка должна соответствовать последней ещё открытой скобке.

Скобки и незавершённые открытия

Пустой массив

Начинаем с пустого стека.
Шаг 1 / 8
Проверка скобок
stack = []
for c in expression:
    if c is opening:
        stack.push(c)
    else if c is closing:
        if stack.empty() or not matches(stack.top(), c):
            return false
        stack.pop()
return stack.empty()

Проверка работает за O(n) времени и O(n) дополнительной памяти.

ОшибкаНельзя считать последовательность правильной только по равенству количества открывающих и закрывающих скобок. Порядок и типы пар существенны.

В строке ([)] ошибка обнаруживается на первом ). Сначала самостоятельно реализуйте проверку скобок и отмену команд. Затем переходите к монотонному стеку: его дополнительное правило удаляет слишком большие вершины; пример трассировки находится в практике.

Проверка нескольких видов скобок

Стек хранит открытия в порядке появления. Закрытие должно совпасть с вершиной. Вход содержит только скобки; пустая строка корректна. Время O(n), память O(n). В Rust ASCII-скобки обрабатываем как байты.

Проверка нескольких видов скобок / Python
def valid_brackets(text: str) -> bool:
    stack = []
    opening = {')': '(', ']': '[', '}': '{'}
    for c in text:
        if c in '([{':
            stack.append(c)
        elif c in opening:
            if not stack or stack.pop() != opening[c]:
                return False
        else:
            return False
    return not stack

print(str(valid_brackets('([{}])')).lower(), str(valid_brackets('([)]')).lower())
Ожидаемый вывод
true false

В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

Скобки и удаление дублей

LeetCode 20 требует разных типов скобок: одного баланса недостаточно. В LeetCode 1047 вершина стека сравнивается с текущей буквой; совпавшая пара исчезает, остальные буквы остаются ждать.

Мы уже получили линейную обработку строки. Осталось проверить скрытое предположение о свободной памяти под растущий стек.

Проверьте строку из ()[]{}. Верните True, если все скобки правильно вложены, иначе False. Пустая строка корректна.

Tab — табуляция · Ctrl / ⌘ + L — выделить строку · Alt + ↑/↓ — переместить строку или выделенный блок · Ctrl / ⌘ + Enter — запустить. Esc, затем Tab — выйти из редактора.

Открыто 0 из 3

    5 секунд на тесты. При первом запуске загрузится Python; нужен интернет. Код выполняется в браузере.

    Тест 1 valid_brackets("([{}])")Ожидается: True
    Тест 2 valid_brackets("([)]")Ожидается: False
    Тест 3 valid_brackets("")Ожидается: True

    Дополнительные тесты: 5. Проверяются границы и повторы.

    Попыток: 0
    04

    Расширяемый массив

    Этот раздел откроется после предыдущих проверок.

    Продолжить: Стек: последним вошёл — первым вышел →

    Что произойдёт, когда все 4 ячейки уже заняты, а приходит пятый элемент? size=4 означает число живых элементов, capacity=4 — число выделенных мест. Можно выделить 5 ячеек или 8; в обоих случаях сейчас копируются четыре старых значения, но следующая вставка будет стоить по-разному.

    До сих пор ёмкость массива считалась достаточной. В реальной задаче число будущих элементов часто неизвестно. Нельзя молча сохранить оценку O(1): сначала посчитаем цену выделения новой памяти и переноса старых элементов.

    Если увеличивать массив на одну ячейку при каждом переполнении, копирование 1 + 2 + … + (n−1) элементов приведёт к Θ(n²) суммарной работе для n вставок.

    При увеличении ёмкости в фиксированное число раз больше единицы копирование происходит редко; общая стоимость n последовательных вставок O(n). Но отдельная вставка с перевыделением всё ещё стоит O(n).

    Удвоение ёмкости

    Размер 9, ёмкость 16. Последняя вставка: 9 действий; всего: 24.

    size — 9 живых элементов; capacity — 16 выделенных ячеек. Рост на 1: 36 копирований; удвоение: 15. Новых записей в обоих случаях 9.

    РазмерЁмкостьЦена вставкиВсего
    1111
    2223
    3436
    4417
    58512
    68113
    78114
    88115
    916924
    1 + 2 + 4 + … + 2ᵏ < 2ᵏ⁺¹ = O(n)

    При удвоении рост числа скопированных элементов ограничен геометрической суммой.

    Время и память
    Геометрический рост требует до O(n) свободного резервного места и иногда полного копирования. Если важна гарантированная задержка каждой отдельной операции, одной амортизированной оценки недостаточно.

    После 9 добавлений ёмкости 1, 2, 4, 8, 16 дают 1 + 2 + 4 + 8 = 15 копирований и ещё 9 записей новых элементов. Большая цена последней вставки не превращает всю последовательность в квадратичную. Обратное сжатие при каждом pop может вызвать частое копирование; для уменьшения ёмкости нужны разнесённые пороги, например четверть заполнения.

    Считаем цену удвоений

    Явно моделируем ёмкость, записи и копирования. Девять добавлений копируют 15 старых элементов и записывают 9 новых. Модель не измеряет работу аллокатора; она проверяет геометрическую сумму. O(n) времени моделирования, O(1) памяти.

    Считаем цену удвоений / Python
    def growth_cost(n: int) -> tuple[int, int]:
        size, capacity, copies = 0, 1, 0
        for _ in range(n):
            if size == capacity:
                copies += size
                capacity *= 2
            size += 1
        return copies, copies + n
    
    print(*growth_cost(9))
    Ожидаемый вывод
    15 24

    В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

    Число запросов неизвестно заранее

    Если вход поступает потоком, заранее выделить ровно n мест нельзя. Геометрическое расширение ограничивает сумму переносов; увеличение на одну ячейку может скопировать около q²/2 элементов.

    Геометрическая сумма объяснила общий результат. Дадим этому виду оценки точное название и отличим его от среднего по случайным входам.

    Примените алгоритм к новому примеру

    Задания раздела

    Начальная ёмкость 1. За 10 добавлений с удвоением сколько старых элементов скопировано?

    А сколько копирований при росте на одну ячейку?

    После 10 добавлений сопоставьте параметры.

    Попыток: 0
    05

    Что означает амортизированное время

    Этот раздел откроется после предыдущих проверок.

    Продолжить: Стек: последним вошёл — первым вышел →

    Посчитаем десять вставок, начиная с пустого массива ёмкости 1. Их цены: 1,2,3,1,5,1,1,1,9,1. Накопленная работа: 1,3,6,7,12,13,14,15,24,25. Девятая операция дорогая, однако все десять потребовали 25 действий. Таблица в модели расширяемого массива показывает тот же расчёт для любого выбранного числа вставок.

    После удвоений отдельные вставки резко отличаются по времени. Чтобы описать такую структуру честно, сменим единицу анализа: вместо одного вызова рассмотрим всю последовательность, начиная с пустого состояния.

    Амортизированный анализ (Amortized analysis) — Метод оценки суммарной стоимости последовательности операций с учётом редких дорогих действий.

    Вместо требования «каждая операция быстрая» доказывают, что сумма затрат на любую последовательность из m операций ограничена O(m). Это не вероятностное среднее и не утверждение о конкретном замере процессора.

    Одна дорогая вставка и вся последовательность

    В модели считаем запись нового элемента и копирование старого по одной операции.

    Записи: 16
    Копирования: 15
    Всего: 31 < 3n = 48

    Выберите 17: одна вставка скопирует 16 элементов, но суммарная работа останется линейной.

    ПоказательЧто гарантирует
    Худшее время операцииВерхнюю границу каждого одиночного вызова
    Амортизированное времяВерхнюю границу суммы для всей последовательности
    Ожидаемое времяСреднее по случайности модели при её наличии
    ВажноАмортизированное O(1) не следует путать с O(1) в худшем случае и с математическим ожиданием O(1).

    Амортизированная O(1) не означает, что вход случаен или что среднее измерение получилось маленьким. Доказывается граница для любой допустимой последовательности. Если стартовая структура уже содержит n неоплаченных элементов, их начальную подготовку или начальный потенциал нужно включить в оценку.

    За 10 вставок с удвоением: 15 копирований + 10 записей = 25 действий. Какие выводы верны?

    В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

    Монотонный стек

    В Daily Temperatures один шаг способен удалить много индексов из стека, но каждый индекс добавляется и удаляется не больше одного раза. Поэтому вложенный while не делает всю обработку квадратичной.

    Для основного маршрута достаточно суммы копирований и подсчёта судьбы одного элемента. Три формальных метода — необязательное углубление; дальше можно перейти сразу к очереди на двух стеках.

    Примените алгоритм к новому примеру

    Задания раздела

    Цены десяти вставок: 1,2,3,1,5,1,1,1,9,1. Введите сумму.

    Что означает O(1) амортизированно от пустого состояния?

    Попыток: 0
    06

    Очередь на двух стеках

    Этот раздел откроется после предыдущих проверок.

    Продолжить: Стек: последним вошёл — первым вышел →

    Выполним push(3), push(8), pop(), push(5), pop(). Сначала input=[3,8], output пуст. Перенос разворачивает его в output=[8,3], pop выдаёт 3. Новая 5 идёт в input; следующий pop берёт оставшуюся 8 из output без переноса. Только когда output опустеет, можно переносить 5.

    Теперь применим анализ к новой реализации FIFO. Входной стек принимает элементы, выходной отдаёт их в обратном порядке. Второе обращение порядка при переносе позволяет получить именно очередь.

    Одни элементы поступают во входной стек, другие извлекаются из выходного. Если выходной пуст, содержимое входного переносится туда полностью, обращая порядок.

    FIFO из двух LIFO

    Входной стек, вершина справа

    3[0]
    8[1]
    5[2]

    Выходной стек, вершина справа

    Пустой массив

    Выходной стек пуст. Первое извлечение перенесёт весь входной стек.

    Логическая очередь: 3 → 8 → 5. Потенциал 2·|input| = 6.

    Два стека: очередь
    enqueue(x):
        input.push(x)
    
    dequeue():
        if output.empty():
            while not input.empty():
                output.push(input.pop())
        assert not output.empty()
        return output.pop()

    Каждый элемент вставляется и удаляется из каждого стека не больше одного раза.

    Одиночное извлечение при полном переносе может быть O(n), но суммарно m операций — O(m). Следовательно, обе операции выполняются за O(1) амортизированно.

    Для enqueue(3), enqueue(8), dequeue(), enqueue(5), dequeue() ответы равны 3 и 8. Важно переносить входной стек только при пустом выходном: иначе новые элементы окажутся перед старыми. За одну линейную историю каждый элемент проходит input и output не больше одного раза, поэтому m операций требуют O(m) работы и O(n) памяти.

    Восстановите действия для push(3), push(8), pop(), push(5), pop().

    1. input = [3,8]
    2. Извлечь 3 из output
    3. Перенести в пустой output: [8,3]
    4. Без нового переноса извлечь 8
    5. Добавить 5 в input, output остаётся [8]

    Очередь на двух стеках

    Перенос выполняется только при пустом выходном стеке. Пустое извлечение возвращает None. Для одной истории m операций суммарное время O(m), память O(n); один перенос может быть линейным.

    Очередь на двух стеках / Python
    class TwoStackQueue:
        def __init__(self):
            self.incoming = []
            self.outgoing = []
    
        def push(self, value):
            self.incoming.append(value)
    
        def pop(self):
            if not self.outgoing:
                while self.incoming:
                    self.outgoing.append(self.incoming.pop())
            return self.outgoing.pop() if self.outgoing else None
    
    q = TwoStackQueue()
    q.push(3)
    q.push(8)
    first = q.pop()
    q.push(5)
    print(first, q.pop(), q.pop())
    Ожидаемый вывод
    3 8 5

    В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

    Очередь, если разрешены только стеки

    LeetCode 232 запрещает произвольный доступ к середине контейнера. Два стека реализуют нужный FIFO-интерфейс; прямое использование pop(0) в Python меняет модель и даёт линейный сдвиг.

    Каждый элемент проходит каждый стек не более одного раза за одну историю команд: так получается суммарное O(m). Практика ниже закрепляет очередь. Деамортизация следующей главы — дополнительный эксперимент.

    Команды ["push", x] добавляют x, ["pop"] извлекают начало. Верните ответы только на pop; для пустой очереди верните None. Используйте два стека.

    Tab — табуляция · Ctrl / ⌘ + L — выделить строку · Alt + ↑/↓ — переместить строку или выделенный блок · Ctrl / ⌘ + Enter — запустить. Esc, затем Tab — выйти из редактора.

    Открыто 0 из 3

      5 секунд на тесты. При первом запуске загрузится Python; нужен интернет. Код выполняется в браузере.

      Тест 1 queue_results([["push", 3], ["push", 8], ["pop"], ["push", 5], ["pop"], ["pop"]])Ожидается: [3, 8, 5]
      Тест 2 queue_results([["pop"], ["push", 4], ["pop"], ["pop"]])Ожидается: [None, 4, None]
      Тест 3 queue_results([])Ожидается: []

      Дополнительные тесты: 4. Проверяются границы и повторы.

      Попыток: 0
      А—Я

      Справочник терминов

      Справочник собирает определения из разобранных разделов. Используйте поиск, если нужно восстановить термин; затем переходите к оценке ограничений и задачам для самостоятельного решения.

      Общий справочник всех лекций ↗

      Найдено: 12 из 12

      Стек / StackLIFO-контейнер с операциями push и pop.
      Очередь / QueueFIFO-контейнер с enqueue и dequeue.
      Кольцевой буфер / Circular bufferМассив с циклическим переходом индексов.
      LIFO / Last in, first outПоследним добавлен — первым извлечён.
      FIFO / First in, first outПервым добавлен — первым извлечён.
      Амортизированный анализ / Amortized analysisОценка суммарной цены последовательности операций.
      Потенциал / PotentialФункция состояния, учитывающая запас накопленной работы.
      Метод бухгалтерии / Accounting methodОплата будущих дорогостоящих действий накопленными кредитами.
      Динамический массив / Dynamic arrayМассив, который перевыделяется по мере заполнения.
      Деамортизация / DeamortizationРаспределение редкой дорогой работы по последовательным операциям.
      Скобочная последовательность / Bracket sequenceПоследовательность, корректность вложения которой проверяется стеком.
      Два стека / Two-stack queueРеализация FIFO через два LIFO-контейнера.
      07

      Ограничения и выбор алгоритма

      Этот раздел откроется после предыдущих проверок.

      Продолжить: Стек: последним вошёл — первым вышел →

      Сначала подставьте максимальные размеры из условия в оценку времени и памяти. Оценка числа действий — ориентир: константы, язык и ввод проверяются отдельно.

      Сравнение уже разобранных структур

      СтруктураИнвариантДобавитьИзвлечьПосмотреть следующий
      СтекЖивые элементы в префиксе; доступ с вершины (LIFO).O(1)*O(1), вершинуO(1)
      Кольцевая очередьcount элементов от head по кольцу (FIFO).O(1)*O(1), головуO(1)
      Очередь на двух стекахreverse(output) + input задаёт порядок FIFO.O(1)*O(1)*; отдельный перенос O(n)O(1)* с подготовкой output

      Считаем всю последовательность

      Для q = 200 000 команд сумма 1 + … + q достигает примерно 2·10¹⁰. Сдвиг при каждом извлечении из начала массива опасен, даже если сама очередь часто короткая. Сначала выясните, может ли вход специально накопить длинный хвост.

      Стек, кольцевая очередь и очередь на двух стеках дают O(q) суммарной работы в подходящей модели. Для динамического массива отдельно учитывайте копирования: удвоение укладывается в O(q), единичное расширение — нет.

      Вложенный цикл не всегда квадрат

      В монотонном стеке посчитайте судьбу одного индекса: один push и максимум один pop. Это O(n) на весь массив, хотя отдельная итерация удаляет много элементов. В противоположность этому повторный просмотр всех оставшихся элементов такой гарантии не имеет.

      Память и худший отдельный шаг

      Если глубина вложенности не ограничена отдельно, стек скобок может хранить n открытий. При n = 10⁶ это O(n) памяти; Python-объекты и целые индексы заметно тяжелее одного машинного байта. Для одних ASCII-скобок выбирайте компактное представление, если лимит мал.

      Амортизированное время подходит для общего лимита контеста. Оно не доказывает ограничение задержки каждого запроса. В тестах проверьте пустую структуру, одно значение, совпадающие элементы, полный оборот буфера и длинную серию добавлений перед первым извлечением.

      Примените алгоритм к новому примеру

      Задания раздела

      Сопоставьте операции и цену.

      Миллион открывающих скобок подряд: сколько незакрытых типов нужно хранить?

      Попыток: 0
      08

      Практика: 3 уровня

      Этот раздел откроется после предыдущих проверок.

      Продолжить: Стек: последним вошёл — первым вышел →

      Разминка: скобки, отмена, очередь

      Решите эти задачи без внешнего сайта. Используйте стек для отмены и два стека для очереди; затем переходите к Min Stack и монотонному стеку.

      Проверьте строку из ()[]{}. Верните True, если все скобки правильно вложены, иначе False. Пустая строка корректна.

      Tab — табуляция · Ctrl / ⌘ + L — выделить строку · Alt + ↑/↓ — переместить строку или выделенный блок · Ctrl / ⌘ + Enter — запустить. Esc, затем Tab — выйти из редактора.

      Открыто 0 из 3

        5 секунд на тесты. При первом запуске загрузится Python; нужен интернет. Код выполняется в браузере.

        Тест 1 valid_brackets("([{}])")Ожидается: True
        Тест 2 valid_brackets("([)]")Ожидается: False
        Тест 3 valid_brackets("")Ожидается: True

        Дополнительные тесты: 5. Проверяются границы и повторы.

        Попыток: 0

        Команды ["do",x] добавляют действие, ["undo"] отменяют последнее. Верните [ответы_undo, итоговая_история_от_дна_к_вершине]. Для пустой отмены ответ None.

        Tab — табуляция · Ctrl / ⌘ + L — выделить строку · Alt + ↑/↓ — переместить строку или выделенный блок · Ctrl / ⌘ + Enter — запустить. Esc, затем Tab — выйти из редактора.

        Открыто 0 из 3

          5 секунд на тесты. При первом запуске загрузится Python; нужен интернет. Код выполняется в браузере.

          Тест 1 undo_results([["do", 4], ["do", 7], ["undo"], ["do", 2]])Ожидается: [[7], [4, 2]]
          Тест 2 undo_results([["undo"]])Ожидается: [[None], []]
          Тест 3 undo_results([])Ожидается: [[], []]

          Дополнительные тесты: 2. Проверяются границы и повторы.

          Попыток: 0

          Команды ["push", x] добавляют x, ["pop"] извлекают начало. Верните ответы только на pop; для пустой очереди верните None. Используйте два стека.

          Tab — табуляция · Ctrl / ⌘ + L — выделить строку · Alt + ↑/↓ — переместить строку или выделенный блок · Ctrl / ⌘ + Enter — запустить. Esc, затем Tab — выйти из редактора.

          Открыто 0 из 3

            5 секунд на тесты. При первом запуске загрузится Python; нужен интернет. Код выполняется в браузере.

            Тест 1 queue_results([["push", 3], ["push", 8], ["pop"], ["push", 5], ["pop"], ["pop"]])Ожидается: [3, 8, 5]
            Тест 2 queue_results([["pop"], ["push", 4], ["pop"], ["pop"]])Ожидается: [None, 4, None]
            Тест 3 queue_results([])Ожидается: []

            Дополнительные тесты: 4. Проверяются границы и повторы.

            Попыток: 0

            Монотонный возрастающий стек [2,5,7]. Приходит 4: удаляем большие вершины и кладём 4. Сколько pop выполнено?

            Основное применение: Valid Parentheses и Implement Queue using Stacks, затем Min Stack и Daily Temperatures. Largest Rectangle — необязательное испытание после трассировки монотонного стека.

            Примените разобранные приёмы самостоятельно. Для каждой задачи сначала сформулируйте требования к ответу и предложите простое решение. Затем подставьте максимальные ограничения, оцените время и память и решите, какое улучшение требуется. Подсказки с идеей и оценкой раскрываются отдельно.

            Уровни задают порядок практики в курсе, а не единую шкалу платформ. Если уровень отличается от отметки LeetCode, это указано в карточке. Для Codeforces используется оценка курса.

            01

            Лёгкие

            Три задачи для закрепления основных приёмов.

            1. LeetCode · 20

              Valid Parentheses

              Хранить незакрытые скобки и проверять последний тип.

              Ограничения. Длина до 10⁴, три типа скобок.

              Идея и оценка сложности

              Добавляйте открытия в стек. Закрытие обязано совпасть с вершиной; после конца стек пуст. O(n) времени и O(n) памяти.

            2. Восстановить FIFO на двух стеках и доказать суммарное время.

              Ограничения. До 100 вызовов; pop и peek вызываются корректно.

              Идея и оценка сложности

              Переносите input только при пустом output. Каждый элемент проходит каждый стек один раз: O(1) амортизированно, O(n) на отдельный перенос.

            3. Удалять пары соседних букв без повторного сканирования строки.

              Ограничения. До 10⁵ строчных латинских букв.

              Идея и оценка сложности

              Если текущая буква совпала с вершиной, удалите вершину; иначе добавьте букву. Каждый символ участвует в O(1) операциях, итог O(n).

            02

            Средние

            Две задачи на сочетание идей и анализ ограничений.

            1. LeetCode · 155

              Min Stack

              Добавить к стеку инвариант минимума на каждом префиксе.

              Ограничения. До 3·10⁴ вызовов; значения в диапазоне signed int32.

              Идея и оценка сложности

              Храните пару (значение, минимум до этого места). После pop прежний минимум восстановится автоматически. Не забывайте повторяющиеся минимумы. O(1) на операцию при достаточной ёмкости.

            2. LeetCode · 739

              Daily Temperatures

              Обосновать линейное время монотонного стека.

              Ограничения. До 10⁵ температур, значения от 30 до 100.

              Идея и оценка сложности

              Храните индексы дней без найденного ответа. Новая более высокая температура закрывает меньшие на вершине. Каждый индекс добавлен и удалён не более одного раза: O(n), память O(n).

            03

            Тяжёлая

            Одна задача для переноса идеи на новую постановку.

            1. Найти границы максимального прямоугольника монотонным стеком.

              Ограничения. До 10⁵ столбцов высотой от 0 до 10⁴.

              Идея и оценка сложности

              Для каждого столбца нужны ближайшие меньшие границы. При извлечении из возрастающего стека вычисляйте доступную ширину. Добавьте завершающий нулевой столбец или явно опустошите стек. O(n), площадь считайте широким типом.

            Примените алгоритм к новому примеру

            Задания раздела

            Стек [2,5,7], приходит 4. Пока вершина >4, pop. Сколько удалений?

            Почему нельзя переносить input в непустой output?

            Попыток: 0
            Дополнительные материалыДля углублённого разбора · не влияют на прохождение
            ДОП.

            Три метода амортизированного анализа

            Необязательное углублениеПрямой подсчёт из предыдущей главы уже достаточен для практики. Здесь можно изучить бухгалтерию и потенциал; раздел не блокирует основной прогресс.

            Суммарную линейную оценку можно доказывать разными способами. Это три формы одного учёта работы, а не три разных алгоритма. Разберём их на переносе элементов и проверим, где накапливается запас.

            Прямой метод (aggregate): подсчитать суммарное время m операций напрямую. Метод бухгалтерии: начислить операциям условную плату, лишние «монеты» сохранять для будущего. Потенциальный метод: записать запас будущей работы одной функцией состояния.

            âᵢ = aᵢ + Φ(Sᵢ) − Φ(Sᵢ₋₁)

            Амортизированная стоимость âᵢ равна фактической aᵢ плюс изменению потенциала.

            Σâᵢ = Σaᵢ + Φ(Sₘ) − Φ(S₀)

            Если Φ(Sₘ) ≥ Φ(S₀), сумма амортизированных стоимостей не меньше фактической.

            Для расширяемого массива можно выбрать потенциал, который растёт при заполнении, но резко уменьшается при перевыделении. Это «оплачивает» копирование через ранее накопленный резерв.

            Может ли амортизированная стоимость быть отрицательной?
            Да, в потенциальном методе отдельная âᵢ может быть отрицательной, если запас потенциала сильно падает. Существенны суммарная оценка и условия на начальный и конечный потенциалы.

            Для очереди на двух стеках удобно взять Φ = 2·|input|, считая одну операцию push/pop одной единицей. Enqueue стоит 1 и увеличивает потенциал на 2, поэтому его амортизированная цена 3. Перенос k элементов и последующее извлечение стоят 2k + 1, но уменьшают потенциал на 2k: остаётся цена 1. Потенциал неотрицателен и в пустом состоянии равен нулю.

            Какая неотрицательная функция оплачивает перенос k элементов за 2k push/pop?

            В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

            Проверяем плату за операцию

            Для очереди на двух стеках назначьте enqueue цену 3. Две единицы сохраняются на перенос, одна оплачивает запись. Такая проверка объясняет гарантию для любой истории, а не только для чередующихся вставок и извлечений.

            Потенциал позволит объяснить дорогой перенос в очереди на двух стеках без предположения, что переносы редки на удачном входе.

            Примените алгоритм к новому примеру

            Задания раздела

            Что оплачивает перенос input в output?

            Попыток: 0
            ДОП.

            Как получить гарантию на каждую операцию

            Необязательное углублениеОтличайте гарантию общего времени от гарантии задержки одного запроса. Полное расписание постепенного переноса не требуется для основной практики.

            Амортизированная гарантия подходит для общего времени программы, но может быть недостаточна для отдельного запроса. Например, игровому циклу нельзя отдавать весь кадр одному большому переносу. Здесь нужна гарантия на каждый шаг.

            Деамортизация (Deamortization) — Преобразование алгоритма с редкими дорогими операциями в алгоритм с распределённой работой и контролируемой стоимостью каждого шага.

            Можно заранее выполнять часть ожидаемого крупного копирования после каждого поступления элемента, пока старое представление ещё пригодно для чтения. Чтобы это работало, нужно поддерживать корректное состояние во время переноса.

            Лекция обсуждает эту идею для массивов и стека/очереди: дорогостоящая перестройка представляется как расписание небольших порций работы.

            Необходимая оговоркаНедостаточно просто перенести пару элементов «когда удобно»: надо доказать, что копирование завершится до переполнения и все запросы останутся корректными во время миграции.
            Расширение во время копирования
            При деамортизации часто одновременно существуют старый и новый буферы, а операции знают, где находится каждый элемент. Реализация сложнее обычного удваивающегося массива.

            Постепенный перенос требует одновременно читать старую область, писать новую и помнить, какие позиции уже перенесены. Просто заменить цикл на один шаг недостаточно: следующий запрос всё ещё должен находить правильный элемент. Мы разобрали принцип деамортизации; промышленная реализация потребует явного расписания и доказанного инварианта переходного состояния.

            Необязательный эксперимент: расписание переноса

            Старый буфер ёмкости 8 содержит 4 элемента. Заранее выделен новый буфер на 16. После каждого добавления копируем один из 4 старых элементов; новые сразу записываем в новый буфер.

            в старом #0[0]
            в старом #1[1]
            в старом #2[2]
            в старом #3[3]

            Добавлено: 0. Перенесено: 0/4. До заполнения старой ёмкости осталось 4 добавлений; неперенесённых старых элементов тоже 4.

            Чтение старого элемента идёт из нового буфера, если его индекс меньше числа перенесённых, иначе из старого. Этот эксперимент рассматривает только добавления; смешанные удаления требуют отдельного расписания.

            В СПОРТИВНОМ ПРОГРАММИРОВАНИИ

            Ограничение на один запрос

            Симулятор может требовать короткий отклик после каждого события. Суммарное O(q) не запрещает одному событию занять O(q). Если условие требует худший случай, выбирайте готовый deque или доказывайте расписание переноса.

            В следующей лекции появятся узлы и ссылки: они позволят менять локальные связи и сохранять старые версии. Перед этим оценим ограничения и закрепим текущие инварианты на задачах.

            Примените алгоритм к новому примеру

            Задания раздела

            Что требуется для гарантии каждого запроса при постепенном копировании?

            Попыток: 0

            ПРОДОЛЖИТЬ ОБСУЖДЕНИЕ

            Промпт для ИИ

            Скопируйте промпт с текстом лекции в чат с ИИ, чтобы обсудить непонятные места. В него входят дополнительные материалы, псевдокод и примеры на обоих языках.