Стек: последним вошёл — первым вышел
В редакторе выполнены действия «написать 4», «написать 7», затем отмена: должно исчезнуть 7. После нового действия 2 история содержит [4,2]. Команды push(4), push(7), pop(), push(2) делают именно это: последнее действие отменяется первым.
После оценки числа действий из лекции 01 перейдём к хранению данных. Во многих задачах важно, в каком порядке брать элементы. Начнём с самого простого правила — извлекать последний добавленный элемент.
Стек (Stack) — Контейнер с операциями push(x) и pop(): удаляется последний добавленный элемент (LIFO).
Стек удобно хранить как заполненный префикс массива. push записывает элемент в конец и увеличивает размер; pop уменьшает размер и возвращает последний элемент. При достаточной ёмкости обе операции занимают O(1).
Вершина справа. Доступен последний добавленный элемент.
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 должна восстанавливать данные, вместе с командой сохраните достаточно информации для обратного действия.
Стек выбирает конец, с которого доступны данные. Сменим это правило на порядок поступления и посмотрим, почему обычного массива со сдвигами недостаточно.
Очередь: первым вошёл — первым вышел
Этот раздел откроется после предыдущих проверок.
Продолжить: Стек: последним вошёл — первым вышел →Добавим 4, затем 7, затем 2. Стек извлечёт 2, очередь — 4. Здесь одинаковые данные и команды, а разница только в правиле выбора следующего. Сравните также минимум из лекции 02, если уже знакомы с кучей.
Следующая команда: add(4). Перед remove предскажите три ответа: последний, самый старый и минимальный.
Стек, вершина справа
Пустой массив
Очередь, голова слева
Пустой массив
Очередь с приоритетами, показаны живые значения
Пустой массив
. Ответы remove (стек / очередь / минимум): ещё нет.
У стека открыт один конец. Что изменится, если обслуживать элементы в порядке поступления? Например, запросы к серверу должны уходить в обработку от старых к новым: здесь нужен FIFO, а не LIFO.
Очередь (Queue) — Контейнер с enqueue(x) и dequeue(): удаляется самый давно добавленный элемент (FIFO).
Простая реализация удаляет A[0] и сдвигает остаток: для [4,7,2] приходится переместить 7 и 2. Если удалять так много раз из длинной очереди, набирается квадратичная работа. Оставим значения на месте и будем двигать только индекс head; освобождённые ячейки научимся использовать повторно.
Можно хранить элементы массива между указателями head и tail. Если оба указателя только увеличиваются, даже небольшая очередь со временем доберётся до края памяти.
Решение: интерпретировать массив как кольцевой буфер, заменяя переход за правую границу переходом к нулевой ячейке.
head = 0; count = 3; следующая запись = 3.
Храним head и count. Свободное место для записи вычисляем по модулю ёмкости.
Инвариант: элементы очереди находятся в циклическом диапазоне длиной count от head.
Как отличить пустой буфер от полного
| Структура | push / enqueue | pop / dequeue |
|---|---|---|
| Стек | В конец | Из конца |
| Очередь | В хвост | Из головы |
Одних head и tail недостаточно, чтобы различить пустой и полный буфер: после полного оборота они могут совпасть. В этой лекции храним count отдельно. Запись идёт в (head + count) mod capacity, чтение — из head. При count = capacity сначала расширяем буфер или отклоняем добавление: молча затирать живой элемент нельзя.
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Обработка заявок по порядку
В симуляции очередь задаёт порядок обслуживания. При q до 200 000 удаление первого элемента массива со сдвигом может дать Θ(q²); кольцевой буфер или deque оставляют суммарную работу линейной.
Кольцевая индексация убрала сдвиги. Теперь отделим механизм хранения от задачи: используем стек для проверки вложенности.
Для чего нужен стек
Этот раздел откроется после предыдущих проверок.
Продолжить: Стек: последним вошёл — первым вышел →Порядок извлечения становится полезным, когда сама задача вложенная. Рассмотрим строку ([{}]): последней открылась фигурная скобка, поэтому именно она должна закрыться первой. Стек хранит ещё не завершённые обязательства.
Пройдём ([{}]) посимвольно: сначала открытия (, [, {. Затем } ожидает { и снимает её; ] снимает [; ) снимает (. Для ([)] после ( и [ символ ) ожидает (, но вершина [. Поэтому ответ отрицательный, хотя открытий и закрытий поровну.
Строка ([{}]); прочитано 0 из 6. Текущий символ: (.
Пустой массив
Во время вызова функции сохраняются адрес возврата и данные текущего вызова: поэтому цепочка рекурсивных вызовов естественно имеет дисциплину LIFO.
Другое применение — разбор вложенных конструкций, например нескольких видов скобок. Каждая закрывающая скобка должна соответствовать последней ещё открытой скобке.
Пустой массив
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-скобки обрабатываем как байты.
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())fn valid_brackets(text: &str) -> bool {
let mut stack = Vec::new();
for c in text.bytes() {
let expected = match c {
b'(' | b'[' | b'{' => { stack.push(c); continue; }
b')' => b'(', b']' => b'[', b'}' => b'{', _ => return false,
};
if stack.pop() != Some(expected) { return false; }
}
stack.is_empty()
}
fn main() { println!("{} {}", valid_brackets("([{}])"), valid_brackets("([)]")); }true false
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Скобки и удаление дублей
LeetCode 20 требует разных типов скобок: одного баланса недостаточно. В LeetCode 1047 вершина стека сравнивается с текущей буквой; совпавшая пара исчезает, остальные буквы остаются ждать.
Мы уже получили линейную обработку строки. Осталось проверить скрытое предположение о свободной памяти под растущий стек.
Расширяемый массив
Этот раздел откроется после предыдущих проверок.
Продолжить: Стек: последним вошёл — первым вышел →Что произойдёт, когда все 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.
| Размер | Ёмкость | Цена вставки | Всего |
|---|---|---|---|
| 1 | 1 | 1 | 1 |
| 2 | 2 | 2 | 3 |
| 3 | 4 | 3 | 6 |
| 4 | 4 | 1 | 7 |
| 5 | 8 | 5 | 12 |
| 6 | 8 | 1 | 13 |
| 7 | 8 | 1 | 14 |
| 8 | 8 | 1 | 15 |
| 9 | 16 | 9 | 24 |
При удвоении рост числа скопированных элементов ограничен геометрической суммой.
Время и память
После 9 добавлений ёмкости 1, 2, 4, 8, 16 дают 1 + 2 + 4 + 8 = 15 копирований и ещё 9 записей новых элементов. Большая цена последней вставки не превращает всю последовательность в квадратичную. Обратное сжатие при каждом pop может вызвать частое копирование; для уменьшения ёмкости нужны разнесённые пороги, например четверть заполнения.
Считаем цену удвоений
Явно моделируем ёмкость, записи и копирования. Девять добавлений копируют 15 старых элементов и записывают 9 новых. Модель не измеряет работу аллокатора; она проверяет геометрическую сумму. O(n) времени моделирования, O(1) памяти.
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))fn growth_cost(n: usize) -> (usize, usize) {
let (mut size, mut capacity, mut copies) = (0, 1, 0);
for _ in 0..n {
if size == capacity { copies += size; capacity *= 2; }
size += 1;
}
(copies, copies + n)
}
fn main() { let (copies,total)=growth_cost(9); println!("{} {}",copies,total); }15 24
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Число запросов неизвестно заранее
Если вход поступает потоком, заранее выделить ровно n мест нельзя. Геометрическое расширение ограничивает сумму переносов; увеличение на одну ячейку может скопировать около q²/2 элементов.
Геометрическая сумма объяснила общий результат. Дадим этому виду оценки точное название и отличим его от среднего по случайным входам.
Что означает амортизированное время
Этот раздел откроется после предыдущих проверок.
Продолжить: Стек: последним вошёл — первым вышел →Посчитаем десять вставок, начиная с пустого массива ёмкости 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). Это не вероятностное среднее и не утверждение о конкретном замере процессора.
В модели считаем запись нового элемента и копирование старого по одной операции.
Выберите 17: одна вставка скопирует 16 элементов, но суммарная работа останется линейной.
| Показатель | Что гарантирует |
|---|---|
| Худшее время операции | Верхнюю границу каждого одиночного вызова |
| Амортизированное время | Верхнюю границу суммы для всей последовательности |
| Ожидаемое время | Среднее по случайности модели при её наличии |
Амортизированная O(1) не означает, что вход случаен или что среднее измерение получилось маленьким. Доказывается граница для любой допустимой последовательности. Если стартовая структура уже содержит n неоплаченных элементов, их начальную подготовку или начальный потенциал нужно включить в оценку.
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Монотонный стек
В Daily Temperatures один шаг способен удалить много индексов из стека, но каждый индекс добавляется и удаляется не больше одного раза. Поэтому вложенный while не делает всю обработку квадратичной.
Для основного маршрута достаточно суммы копирований и подсчёта судьбы одного элемента. Три формальных метода — необязательное углубление; дальше можно перейти сразу к очереди на двух стеках.
Очередь на двух стеках
Этот раздел откроется после предыдущих проверок.
Продолжить: Стек: последним вошёл — первым вышел →Выполним 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. Входной стек принимает элементы, выходной отдаёт их в обратном порядке. Второе обращение порядка при переносе позволяет получить именно очередь.
Одни элементы поступают во входной стек, другие извлекаются из выходного. Если выходной пуст, содержимое входного переносится туда полностью, обращая порядок.
Входной стек, вершина справа
Выходной стек, вершина справа
Пустой массив
Выходной стек пуст. Первое извлечение перенесёт весь входной стек.
Логическая очередь: 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) памяти.
Очередь на двух стеках
Перенос выполняется только при пустом выходном стеке. Пустое извлечение возвращает None. Для одной истории m операций суммарное время O(m), память O(n); один перенос может быть линейным.
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())#[derive(Default)]
struct TwoStackQueue { incoming: Vec<i64>, outgoing: Vec<i64> }
impl TwoStackQueue {
fn push(&mut self, value: i64) { self.incoming.push(value); }
fn pop(&mut self) -> Option<i64> {
if self.outgoing.is_empty() {
while let Some(x) = self.incoming.pop() { self.outgoing.push(x); }
}
self.outgoing.pop()
}
}
fn main() {
let mut q = TwoStackQueue::default(); q.push(3); q.push(8);
let first=q.pop().unwrap(); q.push(5);
println!("{} {} {}",first,q.pop().unwrap(),q.pop().unwrap());
}3 8 5
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Очередь, если разрешены только стеки
LeetCode 232 запрещает произвольный доступ к середине контейнера. Два стека реализуют нужный FIFO-интерфейс; прямое использование pop(0) в Python меняет модель и даёт линейный сдвиг.
Каждый элемент проходит каждый стек не более одного раза за одну историю команд: так получается суммарное O(m). Практика ниже закрепляет очередь. Деамортизация следующей главы — дополнительный эксперимент.
Справочник терминов
Справочник собирает определения из разобранных разделов. Используйте поиск, если нужно восстановить термин; затем переходите к оценке ограничений и задачам для самостоятельного решения.
Общий справочник всех лекций ↗
Найдено: 12 из 12
Ограничения и выбор алгоритма
Этот раздел откроется после предыдущих проверок.
Продолжить: Стек: последним вошёл — первым вышел →Сначала подставьте максимальные размеры из условия в оценку времени и памяти. Оценка числа действий — ориентир: константы, язык и ввод проверяются отдельно.
Сравнение уже разобранных структур
| Структура | Инвариант | Добавить | Извлечь | Посмотреть следующий |
|---|---|---|---|---|
| Стек | Живые элементы в префиксе; доступ с вершины (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-скобок выбирайте компактное представление, если лимит мал.
Амортизированное время подходит для общего лимита контеста. Оно не доказывает ограничение задержки каждого запроса. В тестах проверьте пустую структуру, одно значение, совпадающие элементы, полный оборот буфера и длинную серию добавлений перед первым извлечением.
Практика: 3 уровня
Этот раздел откроется после предыдущих проверок.
Продолжить: Стек: последним вошёл — первым вышел →Разминка: скобки, отмена, очередь
Решите эти задачи без внешнего сайта. Используйте стек для отмены и два стека для очереди; затем переходите к Min Stack и монотонному стеку.
Основное применение: Valid Parentheses и Implement Queue using Stacks, затем Min Stack и Daily Temperatures. Largest Rectangle — необязательное испытание после трассировки монотонного стека.
Примените разобранные приёмы самостоятельно. Для каждой задачи сначала сформулируйте требования к ответу и предложите простое решение. Затем подставьте максимальные ограничения, оцените время и память и решите, какое улучшение требуется. Подсказки с идеей и оценкой раскрываются отдельно.
Уровни задают порядок практики в курсе, а не единую шкалу платформ. Если уровень отличается от отметки LeetCode, это указано в карточке. Для Codeforces используется оценка курса.
Лёгкие
Три задачи для закрепления основных приёмов.
- LeetCode · 20
Valid Parentheses
Хранить незакрытые скобки и проверять последний тип.
Ограничения. Длина до 10⁴, три типа скобок.
Идея и оценка сложности
Добавляйте открытия в стек. Закрытие обязано совпасть с вершиной; после конца стек пуст. O(n) времени и O(n) памяти.
- LeetCode · 232
Implement Queue using Stacks
Восстановить FIFO на двух стеках и доказать суммарное время.
Ограничения. До 100 вызовов; pop и peek вызываются корректно.
Идея и оценка сложности
Переносите input только при пустом output. Каждый элемент проходит каждый стек один раз: O(1) амортизированно, O(n) на отдельный перенос.
- LeetCode · 1047
Remove All Adjacent Duplicates In String
Удалять пары соседних букв без повторного сканирования строки.
Ограничения. До 10⁵ строчных латинских букв.
Идея и оценка сложности
Если текущая буква совпала с вершиной, удалите вершину; иначе добавьте букву. Каждый символ участвует в O(1) операциях, итог O(n).
Средние
Две задачи на сочетание идей и анализ ограничений.
- LeetCode · 155
Min Stack
Добавить к стеку инвариант минимума на каждом префиксе.
Ограничения. До 3·10⁴ вызовов; значения в диапазоне signed int32.
Идея и оценка сложности
Храните пару (значение, минимум до этого места). После pop прежний минимум восстановится автоматически. Не забывайте повторяющиеся минимумы. O(1) на операцию при достаточной ёмкости.
- LeetCode · 739
Daily Temperatures
Обосновать линейное время монотонного стека.
Ограничения. До 10⁵ температур, значения от 30 до 100.
Идея и оценка сложности
Храните индексы дней без найденного ответа. Новая более высокая температура закрывает меньшие на вершине. Каждый индекс добавлен и удалён не более одного раза: O(n), память O(n).
Тяжёлая
Одна задача для переноса идеи на новую постановку.
- LeetCode · 84
Largest Rectangle in Histogram
Найти границы максимального прямоугольника монотонным стеком.
Ограничения. До 10⁵ столбцов высотой от 0 до 10⁴.
Идея и оценка сложности
Для каждого столбца нужны ближайшие меньшие границы. При извлечении из возрастающего стека вычисляйте доступную ширину. Добавьте завершающий нулевой столбец или явно опустошите стек. O(n), площадь считайте широким типом.
Дополнительные материалыДля углублённого разбора · не влияют на прохождение
Три метода амортизированного анализа
Суммарную линейную оценку можно доказывать разными способами. Это три формы одного учёта работы, а не три разных алгоритма. Разберём их на переносе элементов и проверим, где накапливается запас.
Прямой метод (aggregate): подсчитать суммарное время m операций напрямую. Метод бухгалтерии: начислить операциям условную плату, лишние «монеты» сохранять для будущего. Потенциальный метод: записать запас будущей работы одной функцией состояния.
Амортизированная стоимость âᵢ равна фактической aᵢ плюс изменению потенциала.
Если Φ(Sₘ) ≥ Φ(S₀), сумма амортизированных стоимостей не меньше фактической.
Для расширяемого массива можно выбрать потенциал, который растёт при заполнении, но резко уменьшается при перевыделении. Это «оплачивает» копирование через ранее накопленный резерв.
Может ли амортизированная стоимость быть отрицательной?
Для очереди на двух стеках удобно взять Φ = 2·|input|, считая одну операцию push/pop одной единицей. Enqueue стоит 1 и увеличивает потенциал на 2, поэтому его амортизированная цена 3. Перенос k элементов и последующее извлечение стоят 2k + 1, но уменьшают потенциал на 2k: остаётся цена 1. Потенциал неотрицателен и в пустом состоянии равен нулю.
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Проверяем плату за операцию
Для очереди на двух стеках назначьте enqueue цену 3. Две единицы сохраняются на перенос, одна оплачивает запись. Такая проверка объясняет гарантию для любой истории, а не только для чередующихся вставок и извлечений.
Потенциал позволит объяснить дорогой перенос в очереди на двух стеках без предположения, что переносы редки на удачном входе.
Как получить гарантию на каждую операцию
Амортизированная гарантия подходит для общего времени программы, но может быть недостаточна для отдельного запроса. Например, игровому циклу нельзя отдавать весь кадр одному большому переносу. Здесь нужна гарантия на каждый шаг.
Деамортизация (Deamortization) — Преобразование алгоритма с редкими дорогими операциями в алгоритм с распределённой работой и контролируемой стоимостью каждого шага.
Можно заранее выполнять часть ожидаемого крупного копирования после каждого поступления элемента, пока старое представление ещё пригодно для чтения. Чтобы это работало, нужно поддерживать корректное состояние во время переноса.
Лекция обсуждает эту идею для массивов и стека/очереди: дорогостоящая перестройка представляется как расписание небольших порций работы.
Расширение во время копирования
Постепенный перенос требует одновременно читать старую область, писать новую и помнить, какие позиции уже перенесены. Просто заменить цикл на один шаг недостаточно: следующий запрос всё ещё должен находить правильный элемент. Мы разобрали принцип деамортизации; промышленная реализация потребует явного расписания и доказанного инварианта переходного состояния.
Старый буфер ёмкости 8 содержит 4 элемента. Заранее выделен новый буфер на 16. После каждого добавления копируем один из 4 старых элементов; новые сразу записываем в новый буфер.
Добавлено: 0. Перенесено: 0/4. До заполнения старой ёмкости осталось 4 добавлений; неперенесённых старых элементов тоже 4.
Чтение старого элемента идёт из нового буфера, если его индекс меньше числа перенесённых, иначе из старого. Этот эксперимент рассматривает только добавления; смешанные удаления требуют отдельного расписания.
В СПОРТИВНОМ ПРОГРАММИРОВАНИИ
Ограничение на один запрос
Симулятор может требовать короткий отклик после каждого события. Суммарное O(q) не запрещает одному событию занять O(q). Если условие требует худший случай, выбирайте готовый deque или доказывайте расписание переноса.
В следующей лекции появятся узлы и ссылки: они позволят менять локальные связи и сохранять старые версии. Перед этим оценим ограничения и закрепим текущие инварианты на задачах.