2.1.1 — язык, на котором говорят об алгоритмах2.1.1 — the language algorithms are discussed in

Асимптотический анализAsymptotic Analysis

Прежде чем сравнивать два алгоритма, нужен способ описать, как растёт время их работы с ростом входа — независимо от процессора, языка и константных множителей. Здесь: O, Ω, Θ и о-малое с нуля, и амортизационный анализ — как доказать, что "иногда дорого" в среднем всё равно дёшево.Before comparing two algorithms you need a way to describe how their running time grows with input size — independent of the CPU, the language, and constant factors. Here: O, Ω, Θ, and little-o from scratch, plus amortized analysis — how to prove that "occasionally expensive" is still cheap on average.

частый вопрос на собеседованииcommon interview question

Зачем вообще асимптотика — с нуляWhy asymptotics at all — from scratch

Если измерить время работы алгоритма секундомером, результат зависит от процессора, загрузки системы, языка программирования и компилятора — сравнивать такие числа между разными алгоритмами бессмысленно. Асимптотический анализ отбрасывает всё это и оставляет только один вопрос: как число операций растёт, когда размер входа n становится очень большим? Ответ — это функция от n с точностью до константного множителя и младших слагаемых, и именно эту функцию описывают нотации O, Ω, Θ.Timing an algorithm with a stopwatch gives a result that depends on the CPU, system load, programming language, and compiler — comparing such numbers across algorithms is meaningless. Asymptotic analysis throws all of that away and keeps exactly one question: how does the operation count grow as the input size n becomes very large? The answer is a function of n up to a constant factor and lower-order terms, and that's precisely what the O, Ω, Θ notations describe.

n время O(log n) O(n) O(n log n) O(n²)

одни и те же n входных элементов — но количество операций расходится на порядки при больших nthe same n input elements — but the operation count diverges by orders of magnitude as n grows

O, Ω, Θ и о-малоеO, Ω, Θ, and little-o

Все четыре нотации отвечают на один и тот же вопрос — "как f(n) соотносится с g(n) при n → ∞" — но с разной строгостью границы. Путаница между ними — самая частая ошибка на собеседованиях: "алгоритм работает за O(n)" технически верно и для алгоритма, работающего за O(1), потому что O — это верхняя граница, а не точная оценка.All four notations answer the same question — "how does f(n) relate to g(n) as n → ∞" — but with a different strictness of bound. Confusing them is the most common interview mistake: "the algorithm runs in O(n)" is technically true even for an O(1) algorithm, because O is an upper bound, not an exact estimate.

нотацияnotationсмыслmeaningформальноformallyбытовая аналогияeveryday analogy
O(g)верхняя граница ("не хуже чем")upper bound ("no worse than")∃c,n₀: f(n) ≤ c·g(n) ∀n≥n₀"доеду не дольше чем за час""I'll arrive in at most an hour"
Ω(g)нижняя граница ("не лучше чем")lower bound ("no better than")∃c,n₀: f(n) ≥ c·g(n) ∀n≥n₀"доеду не быстрее чем за 20 минут""I won't arrive in under 20 minutes"
Θ(g)точная граница (и O, и Ω)tight bound (both O and Ω)∃c₁,c₂,n₀: c₁g(n) ≤ f(n) ≤ c₂g(n)"доеду ровно за 40-50 минут""I'll arrive in exactly 40-50 minutes"
o(g)строго меньше (не просто "не хуже")strictly less (not just "no worse")∀c ∃n₀: f(n) < c·g(n) ∀n≥n₀"стану сколь угодно быстрее с ростом дистанции""I become arbitrarily faster as distance grows"
O(g(n)) c·g(n) f(n) f растёт не быстрее g Ω(g(n)) c·g(n) f(n) f растёт не медленнее g Θ(g(n)) f зажата сверху и снизу

O — только потолок, Ω — только пол, Θ — и то, и другое одновременно (с точностью до констант c₁, c₂)O is only a ceiling, Ω is only a floor, Θ is both at once (up to constants c₁, c₂)

На практике инженеры почти всегда говорят "O(n log n)", даже когда по смыслу имеют в виду Θ(n log n) — то есть точную, а не только верхнюю границу. Это не ошибка в быту, но на собеседовании стоит явно проговорить разницу, если алгоритм действительно доказуемо Θ, а не просто O.In practice, engineers almost always say "O(n log n)" even when they mean Θ(n log n) — a tight bound, not just an upper one. This is harmless in casual speech, but in an interview it's worth stating the difference explicitly when the algorithm is provably Θ, not merely O.

Худший случай против амортизацииWorst case vs. amortized

Асимптотика описывает одну операцию в худшем случае, но некоторые структуры данных дороги редко и дёшевы почти всегда — например, `push_back` в динамическом массиве (std::vector) в среднем O(1), но конкретно та вставка, что триггерит реаллокацию всего массива, стоит O(n). Амортизационный анализ отвечает не на вопрос "сколько стоит одна операция в худшем случае", а на вопрос "сколько стоит последовательность из n операций, делённая на n" — и это принципиально другая, часто более полезная гарантия.Asymptotics describes one operation in the worst case, but some data structures are rarely expensive and almost always cheap — for example, `push_back` on a dynamic array (std::vector) is O(1) on average, but the specific insertion that triggers a full-array reallocation costs O(n). Amortized analysis doesn't answer "how much does one operation cost in the worst case" but rather "how much does a sequence of n operations cost, divided by n" — a fundamentally different, often more useful guarantee.

операции push_back по порядку реаллокация O(n) амортизированная стоимость — среднее по всем n, O(1)

редкие пики O(n) "размазываются" по множеству дешёвых O(1) операций — суммарно O(n) на n операций, то есть O(1) на операциюrare O(n) spikes get "smeared" across many cheap O(1) operations — O(n) total over n operations, i.e. O(1) per operation

Метод группировки (aggregate method)The aggregate method

Самый прямой способ: посчитать суммарную стоимость всех n операций и поделить на n. Для std::vector, который удваивает вместимость при переполнении: реаллокации происходят на операциях 1, 2, 4, 8, ..., то есть их суммарная стоимость — геометрическая прогрессия 1+2+4+...+n ≈ 2n. Добавить сюда n обычных вставок стоимостью O(1) каждая — итого O(n) на n операций, то есть O(1) амортизированно на операцию.The most direct approach: sum the total cost of all n operations and divide by n. For a std::vector that doubles capacity on overflow: reallocations happen at operations 1, 2, 4, 8, ..., so their total cost is the geometric series 1+2+4+...+n ≈ 2n. Add n ordinary O(1) insertions on top — total O(n) over n operations, i.e. O(1) amortized per operation.

Метод потенциалов (potential method)The potential method

Более формальный и переносимый инструмент. Вводится потенциальная функция Φ, отображающая состояние структуры данных в неотрицательное число — интуитивно, "запас прочности", накопленный структурой. Амортизированная стоимость операции i определяется как â(i) = c(i) + Φ(i) − Φ(i−1), где c(i) — реальная стоимость. Идея: дешёвые операции "платят вперёд", увеличивая Φ, а дорогая операция "расплачивается" за счёт накопленного Φ — так пик реальной стоимости гасится падением потенциала, и â(i) остаётся маленьким на каждом шаге.A more formal, more portable tool. A potential function Φ is introduced, mapping the data structure's state to a non-negative number — intuitively, the "safety margin" the structure has built up. The amortized cost of operation i is defined as â(i) = c(i) + Φ(i) − Φ(i−1), where c(i) is the real cost. The idea: cheap operations "pay ahead," raising Φ, and an expensive operation "cashes in" the accumulated Φ — so the real-cost spike is absorbed by a drop in potential, and â(i) stays small at every step.

Для std::vector: пусть Φ = 2·(размер) − (вместимость). После реаллокации размер = вместимость/2 → новая вместимость = 2·размер, значит сразу после роста Φ = 0. Перед реаллокацией размер = вместимость, значит Φ = размер — большое число, "накопленное" предыдущими дешёвыми вставками. Именно эта накопленная величина и гасит стоимость O(n) самой реаллокации в формуле â(i).For std::vector: let Φ = 2·(size) − (capacity). Right after a reallocation, size = capacity/2 → new capacity = 2·size, so Φ = 0 immediately after growth. Right before a reallocation, size = capacity, so Φ = size — a large value "saved up" by preceding cheap insertions. That saved-up value is exactly what absorbs the reallocation's O(n) cost in the â(i) formula.

// std::vector::push_back — амортизированный O(1), пиковый O(n)// std::vector::push_back — amortized O(1), worst case O(n)
void push_back(int x) {
    if (size == capacity) {
        capacity = (capacity == 0) ? 1 : capacity * 2; // O(n) редко// O(n) rarely
        reallocate_and_copy(capacity);
    }
    data[size++] = x; // O(1) почти всегда// O(1) almost always
}

5 примеров из реальной жизни5 real-world examples

01

Автодополнение поисковика — почему важен худший случайSearch-engine autocomplete — why the worst case matters

Подсказки должны появляться за миллисекунды на каждый символ, а не "в среднем быстро" — здесь важна гарантия худшего случая (Θ), а не амортизация: один медленный запрос из тысячи — это заметная задержка для реального пользователя.Suggestions must appear within milliseconds on every keystroke, not "fast on average" — here the worst-case guarantee (Θ) matters, not amortization: one slow request out of a thousand is a noticeable lag for a real user.

02

std::vector в игровом движке — амортизация оправданаstd::vector in a game engine — amortization is justified

Массив активных игровых объектов растёт по push_back каждый кадр; редкая реаллокация O(n) раз в несколько сотен кадров незаметна на фоне амортизированного O(1) — если только она не совпадёт с кадром, где и так много другой нагрузки (тогда нужен reserve() заранее).An array of active game objects grows via push_back every frame; a rare O(n) reallocation once every few hundred frames is invisible against the amortized O(1) — unless it happens to land on a frame that's already heavy (then a pre-emptive reserve() is needed).

03

Хеш-таблица в биржевом ордербуке — реальная стоимость реаллокацииA hash table in an exchange order book — the real cost of rehashing

Вставка ордера в хеш-таблицу амортизированно O(1), но rehashing при превышении load factor — O(n) и происходит непредсказуемо для латентно-чувствительного кода; в HFT-системах поэтому таблицы часто предаллоцируют ёмкость заранее, отказываясь от амортизационной гарантии в пользу детерминированного худшего случая.Inserting an order into a hash table is amortized O(1), but rehashing past the load factor is O(n) and happens unpredictably for latency-sensitive code; HFT systems therefore often pre-allocate capacity up front, trading the amortized guarantee for a deterministic worst case.

04

Сравнение O(n log n) и O(n²) сортировок на реальных данныхComparing O(n log n) and O(n²) sorts on real data

На n=10 пузырьковая сортировка (O(n²)) может быть быстрее quicksort (O(n log n)) из-за меньших констант и лучшей работы с кэшем; асимптотика говорит про поведение при n → ∞, а не про конкретное маленькое n — отсюда гибридные сортировки (Timsort, IntroSort), переключающиеся на insertion sort для маленьких подмассивов.At n=10, bubble sort (O(n²)) can be faster than quicksort (O(n log n)) thanks to smaller constants and better cache behavior; asymptotics describes behavior as n → ∞, not a specific small n — hence hybrid sorts (Timsort, IntroSort) that switch to insertion sort for small subarrays.

05

Garbage collector с амортизированной паузойA garbage collector with an amortized pause

Generational GC (например, в JVM) делает частые дешёвые сборки молодого поколения и редкие дорогие full GC — классический амортизационный паттерн; проблема в реальных системах — не средняя пауза, а именно её дисперсия (stop-the-world full GC), из-за чего low-latency сервисы выбирают GC вроде ZGC/Shenandoah, жертвуя throughput ради снижения пика.A generational GC (e.g., in the JVM) does frequent cheap young-generation collections and rare expensive full GCs — a classic amortized pattern; the real-world problem isn't the average pause but its variance (a stop-the-world full GC), which is why low-latency services pick GCs like ZGC/Shenandoah, trading throughput for a lower worst-case spike.

ГраблиGotchas

"O(n)" на собеседовании, когда имеется в виду Θ(n)Saying "O(n)" on an interview when you mean Θ(n)

Технически O(n) верно и для алгоритма, работающего за O(1) — O задаёт только потолок. Если интервьюер спрашивает "точно ли n шагов, не меньше", корректный ответ требует Θ, а бездумное "O везде" читается как непонимание разницы между границами.O(n) is technically true even for an O(1) algorithm — O only sets a ceiling. If an interviewer asks "is it exactly n steps, not fewer," the correct answer needs Θ, and reflexively saying "O everywhere" reads as not understanding the difference between the bound types.

Амортизированный O(1) — это не гарантия для одного вызоваAmortized O(1) is not a per-call guarantee

"push_back — O(1) амортизированно" не значит "каждый push_back быстрый" — конкретный вызов может стоить O(n). В коде с жёстким бюджетом на кадр или на такт (игры, аудио, торговые системы) путать амортизированную и худшую сложность — источник редких, трудно воспроизводимых просадок производительности."push_back is amortized O(1)" doesn't mean "every push_back is fast" — a specific call can cost O(n). In code with a hard per-frame or per-tick budget (games, audio, trading systems), conflating amortized and worst-case complexity is a source of rare, hard-to-reproduce performance drops.

Игнорирование констант там, где они на самом деле решаютIgnoring constants where they actually matter

Асимптотика намеренно прячет константный множитель, но при фиксированном небольшом n (типичный размер входа в реальной задаче, а не n → ∞) именно константа определяет, какой алгоритм быстрее на практике — отсюда гибридные алгоритмы (2.2.1) и необходимость профилирования, а не только анализа сложности на бумаге.Asymptotics deliberately hides the constant factor, but for a fixed small n (a typical real-world input size, not n → ∞) the constant is exactly what determines which algorithm is faster in practice — hence hybrid algorithms (2.2.1) and the need for profiling, not just complexity analysis on paper.