ОП.04 · ЗАНЯТИЕ 32026 / 2027

Временная сложность
и O-нотация

Формат
лекция с прогонами + лаборатория
Объём
две пары, точка разрыва после экрана 50
Результат
размеченные 12 фрагментов и две заметки о сложности
Файлы
lesson_02/ · opcount.py · answers.py

нажмите любую клавишу, чтобы начать ▸

01/74

Занятие 3 · раздел 1 · формат интервью и оценка эффективности

Временная
сложность
и O-нотация

Мера трудоёмкости алгоритма: модель вычислений, формальное определение асимптотической оценки, каталог классов роста, анализ программ и границы применимости метода.

9 разделов24 примера9 прогонов

план занятия

  1. I–IV мера, T(n) и O-нотация
  2. V каталог классов роста
  3. VI–VII анализ программ и память
  4. VIII границы метода
  5. IX применение и практика

Точка разрыва между парами — после экрана 50.

03

Раздел I · постановка вопроса

Два решения одной задачи

Дан упорядоченный список из 64 номеров; требуется определить, содержится ли в нём заданный номер. Оба решения возвращают одинаковый результат. Ниже — число обращений к списку в случае, когда искомого номера нет.

последовательный просмотр

обращений к списку 0

половинное деление

обращений к списку 0

64 против 6; при n = 1024 — 1024 против 10. Различие устойчиво и растёт с размером входа. Задача раздела — построить меру, которая описывает это различие независимо от вычислительной машины.

I · МЕРА

Раздел I · постановка вопроса

Чем измерить разницу
между двумя решениями?

Первый ответ, который приходит в голову, — секундомер. Следующий экран показывает, почему он не подходит.

I · МЕРА

Раздел I · первый вариант меры

Секунды описывают запуск, а не алгоритм

Одна и та же программа на одних и тех же данных даёт разное время на разных запусках и на разных машинах.

Машина

Процессор, объём кэша, скорость памяти. На другом ноутбуке та же программа отработает за другое время.

Обстановка

Фоновые процессы, режим питания, температура. Два запуска подряд дают разные числа.

Реализация

Версия интерпретатора, библиотеки. Python 3.11 быстрее 3.8 на том же коде.

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

I · МЕРА

Раздел I · выбранная мера

Считаем операции, а не секунды

Число выполненных действий зависит только от двух вещей: от алгоритма и от входных данных. Оно одинаково у всех, кто запустит тот же код на том же входе.

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

Поиск номера в списке из 8 пропусков

на ноутбуке преподавателя → 0,000004 с

на компьютере в аудитории → 0,000011 с

при запущенном браузере → 0,000019 с

сравнений в любом из трёх запусков → 8

I · МЕРА

Раздел II · что считать за единицу

Элементарная операция

Элементарная операция — действие, время выполнения которого не зависит от размера входных данных. Такое действие принимают за единицу счёта.

=

присваивание

total = 0
+

арифметическое действие

a + b
<

сравнение двух значений

if a > b
[ ]

обращение по индексу

data[5]
→

вызов и возврат

return x

Все пять действий выполняются за одинаковое время и при десяти элементах, и при миллионе. Поэтому каждое из них стоит 1.

II · СЧЁТ

Раздел II · стоимость 1

Что выполняется за одно действие

Список слева содержит миллион номеров. Каждая строка справа выполняется одинаково быстро независимо от этого.

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

СтрокаЧто делаетСтоимость
orders[0]адрес вычисляется по индексу1
orders[-1]адрес последнего известен1
len(orders)длина хранится рядом со списком1
orders.append(x)запись в готовую ячейку в конце1
orders.pop() · с концаснимает последний элемент, сдвига нет1
II · СЧЁТ

Раздел II · стоимость 1 · часть 2

Множество и словарь: почему это тоже единица

Проверка «есть ли такой номер» в списке стоит n, а в множестве — 1. Разница в том, как хранятся данные.

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

Плата за это — память: под множество из n номеров выделяется таблица, и она тем больше, чем больше элементов. Размен «время за память» разбирается в разделе о памяти.

СтрокаЧто делаетСтоимость
x in seen · множествоадрес вычисляется хеш-функцией1
seen.add(x)запись по вычисленному адресу1
counts[key] = 5то же для словаря1
counts.get(key, 0)чтение по ключу1
x == y, x < yсравнение двух чисел1
a, b = b, aобмен значениями переменных1
total += valueсложение и присваивание1
II · СЧЁТ

Раздел II · стоимость n · часть 1

Строки, которые просматривают весь список

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

Цикл при этом может быть записан явно, а может находиться внутри библиотечной функции. Стоимость от этого не меняется: sum(orders) складывает миллион чисел ровно так же, как цикл, написанный руками.

СтрокаЧто делаетСтоимость
for x in orders:перебирает каждый элементn
x in orders · списоксравнивает с элементами подрядn
sum(orders)складывает все элементыn
max(orders), min(orders)сравнивает все элементыn
orders.count(x)считает совпадения до конца спискаn
orders.index(x)ищет позицию переборомn
II · СЧЁТ

Раздел II · стоимость n · часть 2

Строки, которые копируют или сдвигают

Второй источник стоимости n — перемещение данных. Здесь программа ничего не ищет, но переносит каждый элемент на новое место.

Отсюда правило, которое пригодится в практике: вставка и удаление в начале списка стоят n, а в конце — 1. Если алгоритму нужно снимать элементы с начала, список заменяют на deque из модуля collections — двустороннюю очередь, где обе операции стоят 1.

СтрокаЧто делаетСтоимость
orders[1:]копирует элементы в новый списокn
orders.copy()копирует каждый элементn
orders.insert(0, x)сдвигает все элементы вправоn
orders.pop(0)снимает первый и сдвигает остальныеn
orders.remove(x)находит элемент и сдвигает остальныеn
orders.reverse()переставляет все элементыn
set(orders), list(range(n))создаёт новую структуру из n элементовn
II · СЧЁТ

Раздел II · две строки рядом

Одинаковые по виду, разные по стоимости

Обе строки проверяют, встречался ли номер раньше. Написаны они почти одинаково, а работы выполняют разное количество.

множествостоит 1
seen = set()
...
if badge in seen:

1 операция

позиция вычисляется по значению номера — программа сразу смотрит в нужную ячейку

списокстоит n
seen = []
...
if badge in seen:

до n операций

программа сравнивает искомый номер с каждым элементом подряд, пока не найдёт

Разница в одной записи — set() вместо [] — меняет стоимость строки с 1 на n. Это самая частая причина, по которой решение получает верный ответ, но не проходит по времени.

II · СЧЁТ

Раздел II · между единицей и n · часть 1

Дешевле полного прохода и дороже его

Список из 1000 номеров. Полный проход по нему стоит 1000 действий. Первая строка справляется за 10, вторая тратит 10 000 — и обе записаны в одну строку кода.

log n · дешевле прохода
from bisect import bisect_left   # стандартный модуль

i = bisect_left(codes, 480)     # 10 действий
# то же половинным делением вручную:
while low <= high:
    mid = (low + high) // 2
    if codes[mid] < 480:
        low = mid + 1
    else:
        high = mid - 1

После каждого сравнения половина списка отпадает: 1000 → 500 → 250 → … → 1. Работает только на упорядоченном списке: без порядка неизвестно, какую половину отбрасывать.

100050025012562…1
1000 номеров → 10 действий · 10⁶ → 20 · 10⁹ → 30
n log n · дороже прохода
prices.sort()                   # ≈10 000 действий
sorted(prices)[:3]              # столько же
min(prices)                     # 1000 действий

Сортировка складывает список из отсортированных кусков: сначала по одному элементу, потом по два, по четыре. На каждом уровне просматривается весь список целиком.

уровень 1склеиваем куски по 11000
уровень 2по 21000
уровень 3по 41000
…каждый раз кусок вдвое длиннее1000
уровень 10по 512 → готово1000

уровней ровно столько, сколько раз 1000 делится пополам, — 10. Каждый стоит 1000 просмотров: 10 × 1000 = 10 000

1000 цен → около 10 000 действий вместо 1000
II · СЧЁТ

Раздел II · между единицей и n · часть 2

Когда стоимость определяется не длиной списка

Иногда работа определяется не длиной списка, а чем-то другим: размером куска, длиной второй строки или тем, повезло ли на этом шаге. Три таких случая — ниже.

k · размер участка
window = prices[10:20]
first_ten = text[:10]

Срез копирует только те элементы, которые в него попали. Сколько цен в списке — тысяча или миллион — неважно: копируется ровно k = 20 − 10 = 10 штук.

взять 10 элементов → 10 действий при любой длине списка
n · m · две независимые длины
if "отказ" in log_line:
    ...
big_list + other_list

Программа прикладывает слово к первой позиции строки и сверяет буквы, потом ко второй, и так далее. Позиций — n, букв в слове — m, значит до n · m сверок.

строка 1000 символов, слово из 5 букв → до 5000 сверок
1* · амортизированная стоимость
orders.append(x)
orders.pop()

Список лежит в памяти куском фиксированного размера. Пока в куске есть место, добавление стоит 1. Когда место кончилось, берётся кусок вдвое больше и всё копируется — вот это добавление стоит n.

1000 добавлений → 2023 действия всего, то есть 2,02 на одно

Итого стоимость строки бывает четырёх видов. 1 — не зависит от данных; log n — данные делятся пополам; n и выше — данные просматриваются целиком; k или m — работу задаёт отдельная величина. Все четыре встретятся при разметке фрагментов.

II · СЧЁТ

Раздел II · одна операция, разные структуры

Стоимость зависит не от действия, а от структуры данных

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

Действиесписокмножество / словарьdeque
проверить наличие элементаn1n
добавить в конец111
добавить в началоn—1
удалить из началаn—1
обратиться по номеру1—n
найти минимумnnn

Отсюда практическое правило, к которому мы вернёмся в разделе о памяти. Если решение упирается в проверку наличия — берут множество; если в добавление и снятие с обоих концов — deque; если нужен доступ по номеру — список. Замена структуры меняет класс сложности, не меняя логики решения.

II · СЧЁТ

Раздел II · на чём держится счёт

Три допущения, которые мы принимаем

Считая обращение к памяти за единицу, мы пользуемся упрощённой моделью машины. В анализе алгоритмов она называется RAM — машина с произвольным доступом к памяти.

Любая ячейка — за 1

Обращение к началу и к концу списка стоит одинаково.

На практике данные в кэше достаются в десятки раз быстрее
Арифметика — за 1

Сложение двух чисел стоит одну операцию.

Для чисел из тысячи разрядов это неверно: сложение стоит O(разрядов)
Одна команда за раз

Действия выполняются последовательно.

Параллельные вычисления и векторные инструкции модель не учитывает

Модель сознательно огрубляет устройство машины — иначе оценка зависела бы от конкретного процессора. Чем приходится платить за это упрощение, разбирается в разделе VIII.

II · СЧЁТ

Раздел II · вторая величина

Размер входа n

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

n

элементов в списке

[417, 133, 604, …]
m

символов в строке

"A-118"
V, E

вершин и рёбер графа

O(V + E)
n, m

два независимых списка

O(n + m)
log k

разрядов числа k

k = 10⁹ → 30 бит

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

II · СЧЁТ

Раздел III · функция трудоёмкости

Как считается T(n)

T(n) — число операций, выполненных алгоритмом на входе размера n. Считается по тексту программы, а не запуском.

  1. Каждой строке приписать её стоимость: 1 или n.
  2. Умножить на число выполнений: строка внутри цикла выполняется n раз.
  3. Сложить всё, что получилось.

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

Что получится в примерах

① обращение по индексу → T(n) = 3

② поиск перебором → T(n) = 2n + 2

③ два цикла подряд → T(n) = 4n + 5

④ вложенные циклы → T(n) = n² + n + 2

⑤ скрытый проход → T(n) = n² + 2n + 2

III · T(n)

Пример 1 из 5 · пункт выдачи

Обращение по индексу

Функция возвращает первую и последнюю заявки смены. Циклов нет, каждая строка выполняется один раз.

Число операций не зависит от того, 10 заявок в журнале или 10 миллионов: обращений всё равно два плюс возврат.

T(n) = 3 — в записи нет буквы n. Это первый признак того, что алгоритм не зависит от размера входа.

fragments.py · 02построчный подсчёт
def first_and_last(orders):
    a = orders[0]                1
    b = orders[-1]               1
    return a, b                  1

T(n) = 1 + 1 + 1 = 3

III · T(n)

Пример 2 из 5 · стадион

Один цикл по списку

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

Считаем худший случай — искомого номера нет, поэтому цикл проходит до конца. Заголовок цикла выполняется n раз, сравнение внутри — тоже n раз.

T(n) = 2n + 2. При n = 8 это 18 операций, при n = 16 — 34. Удвоение входа удвоило работу.

fragments.py · 01худший случай
def find_pass(passes, code):
    for i in range(len(passes)):     n + 1
        if passes[i] == code:        n
            return i
    return -1                        1

T(n) = (n + 1) + n + 1 = 2n + 2

III · T(n)

Пример 3 из 5 · теплица

Два цикла подряд

Среднее значение и число выбросов выше среднего. Два цикла идут друг за другом, а не один внутри другого.

Каждый цикл даёт по 2n операций: заголовок и тело. Вне циклов выполняется пять действий. Складываем: 2n + 2n + 5.

T(n) = 4n + 5. Работы вдвое больше, чем в примере 2, но растёт она так же: удвоили вход — удвоили работу.

fragments.py · 07построчный подсчёт
def average_and_spikes(temperatures):
    total = 0                     1
    for value in temperatures:    n
        total += value            n
    average = total / len(...)    2
    spikes = 0                    1
    for value in temperatures:    n
        if value > average:       n
            spikes += 1
    return average, spikes        1

T(n) = 2n + 2n + 5 = 4n + 5

III · T(n)

Пример 4 из 5 · проходная

Цикл внутри цикла

Проверка, есть ли среди пропусков два одинаковых. Внешний цикл берёт очередной пропуск, внутренний сравнивает его со всеми следующими.

for i in range(len(badges)):
    for j in range(i + 1, len(badges)):
        if badges[i] == badges[j]:
            return True
return False

Ключевое отличие от примера 3: там циклы шли друг за другом и работа складывалась. Здесь внутренний цикл отрабатывает целиком на каждом шаге внешнего — значит, числа перемножаются.

шаг внешнего цикласравнений внутри

Для пропуска номер i внутренний цикл сравнивает его со всеми, кто стоит правее. Чем дальше i, тем меньше остаётся.

i = 07
i = 16
i = 25
i = 34
i = 43
i = 52
i = 61
i = 70

7 + 6 + 5 + 4 + 3 + 2 + 1 = 28 сравнений

это n(n−1)/2 = 8·7/2 = 28 · при n = 16 будет 120, при n = 1000 — 499 500

III · T(n)

Пример 4 из 5 · откуда берётся n²

Почему 28 сравнений — это n², а не n

Число сравнений равно n(n−1)/2 — половине квадрата. Проверим по определению класса: посмотрим, как оно меняется, когда вход удваивается.

nСравнений: n(n−1)/2Во сколько раз больше предыдущегоДля сравнения: один проход, n
828—8
161204,316
324964,132
642 0164,164
1 000499 5004,01 000
Что видно в таблице

Вход удваивается — число сравнений растёт вчетверо. Так ведёт себя только квадрат: 2² = 4.

Куда делось деление на 2

n(n−1)/2 = n²/2 − n/2. Множитель ½ и младшее слагаемое отбрасываются по правилам записи — остаётся n².

Почему это не n

Если бы работа росла как n, при удвоении входа множитель был бы 2, а не 4. Один проход по списку в последнем столбце — как раз такой случай.

III · T(n)

Пример 5 из 5 · отчёт по заказам

Цикл один, а работы как при вложенных

Удаление повторов с сохранением порядка. Вложенного цикла в тексте нет — но строка row not in result стоит не 1, а n, как мы разобрали на экране про стоимость строк.

Цикл выполняется n раз. На каждом шаге проверка просматривает список result, в котором к концу работы до n элементов. Перемножаем: n · n.

T(n) = n² + 2n + 2. Этот пример — самый частый источник ошибок в разметке: по виду кода решение выглядит линейным.

fragments.py · 10внимание на строку 4
def unique_keep_order(rows):
    result = []                  1
    for row in rows:             n
        if row not in result:    n · n
            result.append(row)   n
    return result                1

T(n) = n² + 2n + 2

III · T(n)

Раздел III · пять примеров рядом

Как ведут себя пять функций T(n)

ПримерT(n)n = 8n = 16n = 1000Во сколько раз выросло
① индекс33331,0 — не изменилось
② перебор2n + 218342 0021,9 — почти вдвое
③ два цикла4n + 537694 0051,9 — почти вдвое
④ вложенныеn² + n + 2742741 001 0023,7 — почти вчетверо
⑤ скрытый проходn² + 2n + 2822901 002 0023,5 — почти вчетверо

Примеры ② и ③ различаются вдвое по числу операций, но ведут себя одинаково. Примеры ④ и ⑤ различаются всего на 8 операций при n = 8 — и тоже ведут себя одинаково. Функции разбиваются на группы по тому, как они ведут себя при росте n. Эти группы и записывает O-нотация.

III · T(n)

Раздел IV · переход к оценке

Зачем упрощать T(n),
если она уже посчитана?

Записи 2n + 2 и 4n + 5 неудобно сравнивать, а на собеседовании их не произносят. Нужна форма, в которой одинаково ведущие себя функции выглядят одинаково.

IV · ОЦЕНКА

Раздел IV · правило 1 из 2

Постоянный множитель отбрасывается

Примеры ② и ③ дали 2n + 2 и 4n + 5. Второй делает вдвое больше работы — и всё же при удвоении входа оба удваиваются одинаково.

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

Поэтому множитель в оценку не входит: O(2n) = O(4n) = O(n).

Функцияn = 1000n = 2000Отношение
2n + 22 0024 0022,00
4n + 54 0058 0052,00
7n + 127 01214 0122,00
100n100 000200 0002,00

Все четыре функции различаются по величине в десятки раз, но столбец «отношение» у них один. Оценка сохраняет этот столбец.

IV · ОЦЕНКА

Раздел IV · правило 2 из 2

Младшие слагаемые отбрасываются

Пример ④ дал n² + n + 2. С ростом n доля слагаемых n и 2 в общей сумме падает: при n = 1000 они составляют одну десятую процента.

Старшее слагаемое растёт быстрее остальных и со временем определяет всю сумму. Младшие в оценку не входят: O(n² + n + 2) = O(n²).

Правило применяется только к слагаемым одной переменной. Если величины две и растут независимо, ни одна не является младшей: O(n + m) так и остаётся.

nn²n + 2Доля младших
8641013,5 %
10010 0001021,0 %
1 00010⁶1 0020,1 %
10⁶10¹²10⁶0,0001 %

При n = 8 младшие слагаемые ещё заметны — это одна из причин, по которой оценка не применяется к малым входам.

IV · ОЦЕНКА

Раздел IV · результат упрощения

Пять функций — два класса

Применим оба правила к пяти примерам, которые считали построчно. Пять разных формул превращаются в три записи, а из них две совпадают.

ПримерT(n)убираем множительубираем младшиеоценка
① обращение по индексу311O(1)
② поиск перебором2n + 2n + 2nO(n)
③ два цикла подряд4n + 5n + 5nO(n)
④ вложенные циклыn² + n + 2n² + n + 2n²O(n²)
⑤ скрытый проходn² + 2n + 2n² + 2n + 2n²O(n²)

Примеры ② и ③ получили одну запись. Точность при этом не теряется: они действительно ведут себя одинаково. Примеры ④ и ⑤ тоже совпали, хотя один написан с вложенным циклом, а другой без него.

IV · ОЦЕНКА

Раздел IV · строгое определение

Что означает запись O(f(n))

T(n) = O(f(n)) ⟺ ∃ c > 0, n₀ ≥ 0 : T(n) ≤ c · f(n) при всех n ≥ n₀

Найдутся такое число c и такой размер входа n₀, что начиная с n₀ функция T(n) не превосходит c · f(n). Читается «T(n) есть O от f(n)» и означает: T(n) растёт не быстрее f(n).

c

постоянный множитель. Позволяет не различать 2n и 100n: обе функции укладываются под c · n при подходящем c.

n₀

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

≤

оценка сверху. O не утверждает, что T(n) растёт именно как f(n), — только что не быстрее.

Обозначение пришло из математического анализа: символ ввёл Пауль Бахман в 1894 году, в анализ алгоритмов его перенёс Дональд Кнут в 1976-м. Отсюда и свойство записи — она описывает поведение функции при больших n.

IV · ОЦЕНКА

Раздел IV · применение определения

Проверим: 4n + 5 действительно O(n)

Чтобы доказать оценку, достаточно предъявить одну пару чисел (c, n₀), при которой неравенство выполняется. Возьмём пример ③ и найдём такую пару.

что доказываем4n + 5 ≤ c · n

требуется подобрать c и n₀

берём c = 54n + 5 ≤ 5n

переносим: 5 ≤ n

значит n₀ = 5при n = 5: 25 ≤ 25

при n = 6: 29 ≤ 30, дальше разрыв растёт

вывод4n + 5 = O(n)

пара (5, 5) предъявлена, определение выполнено

n4n + 55nНеравенство
42120нарушено — n < n₀
52525выполнено
104550выполнено
1 0004 0055 000выполнено
Что значит строка n = 4

При n = 4 неравенство нарушено: 21 > 20. Определение это допускает — оно требует выполнения только начиная с n₀ = 5.

Почему это не ошибка

Оценка описывает поведение при больших n. На четырёх элементах разницы между решениями всё равно нет: любое отработает мгновенно.

Вывод, который нужно запомнить

Запись O(n) не обещает ничего про маленькие входы. Она говорит одно: начиная с некоторого размера работа растёт не быстрее n. Что делать с маленькими входами, показывают замеры — занятие 4.

IV · ОЦЕНКА

Раздел IV · форма ответа

Как записывать оценку

В записи остаётся одно слагаемое — старшее — без множителя. Формы слева автопроверка не принимает и объясняет причину.

НеверноВерноОснование
O(2n)O(n)правило множителя
O(n/2)O(n)правило множителя
O(n² + n)O(n²)правило младших слагаемых
O(4n + 5)O(n)оба правила
O(log₂ n)O(log n)основание даёт множитель
O(n + m)O(n + m)верно: величины независимы

Почему O(n + m) не упрощается

Правило младших слагаемых применимо, когда обе части зависят от одной величины. Если списка два и их размеры не связаны, ни одно слагаемое не является младшим: при n = 10 и m = 10⁶ преобладает m, при обратном соотношении — n.

Запись O(n) здесь означала бы дополнительное предположение о малости второго входа. Условие задачи такого предположения не даёт — значит, в ответе остаются обе величины.

IV · ОЦЕНКА

Раздел IV · соседние обозначения

Кроме O существуют Ω и Θ

O ограничивает функцию сверху. Для полноты картины нужны ещё два символа: Ω ограничивает снизу, Θ — с двух сторон сразу.

Разница видна на трёх знакомых алгоритмах: у одних верхняя и нижняя границы совпадают, у других расходятся. Совпали — значит подходит Θ; разошлись — остаётся называть O и Ω по отдельности.

В курсе используется O: и на собеседовании, и в проверяющей системе нужна гарантия сверху. Ω и Θ нужны, чтобы читать чужой анализ и не путать «не хуже» с «ровно столько».

O(f)T(n) ≤ c·f(n)не быстрее f
Ω(f)T(n) ≥ c·f(n)не медленнее f
Θ(f)c₁·f ≤ T ≤ c₂·fровно как f
Алгоритмлучшийхудшийзапись
Поиск перебором1 сравнение
элемент первый
n сравнений
элемента нет
O(n), Ω(1)
Θ нет: границы разошлись
Сумма спискаn сложенийn сложенийΘ(n)
границы совпали
Быстрая сортировкаn log n
деление пополам
n²
опорный — минимум
O(n²), Ω(n log n)
в среднем n log n
IV · ОЦЕНКА

Раздел IV · правила действий с оценками

Три свойства, которыми пользуются при анализе

Постоянный множитель

O(c · f) = O(f)

O(4n) = O(n). Множитель поглощается константой c из определения.
Сумма — для участков подряд

O(f) + O(g) = O(max(f, g))

Цикл O(n), за ним цикл O(n²) — остаётся O(n²): более дорогой участок поглощает остальные.
Произведение — для вложенных

O(f) · O(g) = O(f · g)

Цикл на n, внутри операция стоимостью O(n) — получается O(n²). Пример ⑤ разбирался именно так.

Эти три свойства и есть весь аппарат, которым разбирается любая нерекурсивная программа: выписать стоимость участков, сложить последовательные, перемножить вложенные, убрать множители. Для рекурсивных решений добавится подсчёт по дереву вызовов — раздел VI.

IV · ОЦЕНКА

Раздел IV · упорядочение оценок

Какая оценка лучше другой

Оценки упорядочены по скорости роста. Порядок устанавливается отношением функций: если отношение стремится к нулю, первая растёт медленнее.

1 ≺ log n ≺ n ≺ n log n ≺ n² ≺ n³ ≺ 2ⁿ ≺ n!

Как проверить, что одна функция растёт медленнее другой. Разделить первую на вторую и посмотреть, что происходит с частным при росте n. Если оно стремится к нулю — первая растёт медленнее; если держится около постоянного числа — функции одного порядка. Такие частные посчитаны в таблице справа.

Основание логарифма на порядок не влияет: log₂n и log₁₀n отличаются множителем 3,32, который отбрасывается по первому правилу. Поэтому основание в записи не указывают.

Делим меньшую функцию на бо́льшую: частное падает к нулю — значит, слева растёт медленнее
Отношениеn = 10n = 1 000n = 10⁶Предел
log n / n0,3320,0100,000020
n / n log n0,3010,1000,0500
n log n / n²0,3320,0100,000020
n² / 2ⁿ0,098≈10⁻²⁹⁶≈00
IV · ОЦЕНКА

Раздел V · каталог

Классы сложности в порядке возрастания

O(1)константная

работа не зависит от того, сколько данных

1
O(log n)логарифмическая

на каждом шаге половина данных отпадает

10 шагов
O(n)линейная

каждый элемент трогаем по разу

1 024
O(n log n)линейно-логарифмическая

проходим весь список, и так log n раз

10 240
O(n²)квадратичная

сравниваем каждый элемент с каждым

523 776
O(2ⁿ)экспоненциальная

каждый новый элемент удваивает работу

10³⁰⁸

В нижней строке карточки — сколько операций получится при n = 1024. Первые четыре класса годятся для больших данных. Квадратичный применим, пока элементов не больше нескольких тысяч. Экспоненциальный — только при n около двадцати: при большем n перебор не укладывается в разумное время. Каждый класс дальше разбирается на двух экранах: определение с прогоном и три примера из разных задач.

V · КЛАССЫ

Класс 1 из 6 · определение и прогон

Константная сложность O(1)

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

def first_and_last(orders):
    return orders[0], orders[-1]

Пункт выдачи · журнал заявок за смену

1 200 заявок → 2 обращения к списку

12 000 заявок → 2 обращения

обращение по индексу · n = 64

операций 0

Адрес ячейки вычисляется по индексу арифметически, поэтому просмотр остальных элементов не требуется.

V · КЛАССЫ

Класс 1 из 6 · примеры

Где встречается константная сложность

Обращение по индексу
orders[0]
orders[-1]
prices[i]

Адрес элемента вычисляется по индексу за одно арифметическое действие независимо от длины списка.

T(n) = O(1) · S(n) = O(1)
Проверка по множеству и словарю
if badge in seen:
    ...
counts[word] = 0

Позиция определяется хеш-функцией от самого значения. При совпадении хешей возможен короткий перебор, поэтому оценка справедлива в среднем; в худшем случае — O(n).

T(n) = O(1) в среднем · память структуры O(n)
Длина и добавление в конец
len(orders)
orders.append(x)

Длина хранится вместе со списком. Добавление в конец записывает элемент в готовую ячейку; изредка требуется расширение — разбор в разделе VI.

T(n) = O(1) · для append — амортизированно

Признак класса: отсутствие цикла по входным данным и отсутствие вызовов, содержащих такой цикл. Второе условие существеннее первого — раздел VI.

V · КЛАССЫ

Класс 2 из 6 · определение и прогон

Логарифмическая сложность O(log n)

Алгоритм имеет логарифмическую сложность, если на каждом шаге объём необработанных данных уменьшается в постоянное число раз. При делении пополам число шагов равно log₂n.

Архив чеков · номера 1…1024 · ищем 480

512 → влево·256 → вправо·384 → вправо·448 → вправо·480 найден

пять шагов вместо 480 сравнений при переборе

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

бинарный поиск · n = 64

шагов деления 0

Осевшие ячейки исключены из рассмотрения целиком: 6 шагов на 64 элемента, 10 на 1024.
Шаги и сравнения: в opcount.py на шаг приходится два сравнения, поэтому при n = 1024 там 20 сравнений, а шагов 10.

V · КЛАССЫ

Класс 2 из 6 · примеры

Где встречается логарифмическая сложность

Бинарный поиск
while low <= high:
    middle = (low + high) // 2
    if ids[middle] < target:
        low = middle + 1
    else:
        high = middle - 1

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

T(n) = O(log n) · S(n) = O(1)
Быстрое возведение в степень
result = 1
while power > 0:
    if power % 2:
        result *= base
    base *= base
    power //= 2

Показатель делится пополам на каждом шаге: для power = 10⁹ достаточно 30 итераций вместо миллиарда умножений.

T(n) = O(log power) · S(n) = O(1)
Угадать число «больше или меньше»
low, high = 1, 1000
while low <= high:
    guess = (low + high) // 2
    if guess < secret:
        low = guess + 1
    else:
        high = guess - 1

Та же идея, что в бинарном поиске, но без списка. Каждый вопрос отсекает половину диапазона: числа от 1 до 1000 угадываются за 10 вопросов, от 1 до миллиона — за 20.

T(n) = O(log n) · S(n) = O(1)

Признак класса: управляющая величина делится, а не уменьшается на единицу. Сравните: i //= 2 даёт log n итераций, i -= 1 — n итераций.

V · КЛАССЫ

Класс 3 из 6 · определение и прогон

Линейная сложность O(n)

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

for i in range(len(passes)):
    if passes[i] == code:
        return i
return -1

Стадион · искомого пропуска в списке нет

8 пропусков → 8 сравнений · 1000 → 1000

два последовательных цикла дают 2n — тот же класс

последовательный просмотр · n = 64

сравнений 0

Показан худший случай: искомый элемент отсутствует, поэтому просмотр выполняется до конца списка.

V · КЛАССЫ

Класс 3 из 6 · примеры

Где встречается линейная сложность

Агрегаты по списку
total = 0
for value in temperatures:
    total += value

Сумма, максимум, минимум, среднее. Библиотечные sum() и max() выполняют тот же проход, записанный внутри библиотеки.

T(n) = O(n) · S(n) = O(1)
Подсчёт частот
counts = {}
for word in words:
    counts[word] = counts.get(word, 0) + 1

Один проход, на каждом шаге операция со словарём за O(1). Дополнительная память растёт с числом различных значений.

T(n) = O(n) · S(n) = O(n)
Два указателя по упорядоченным данным
i = j = 0
while i < len(left) and j < len(right):
    if left[i] <= right[j]: i += 1
    else: j += 1

Каждый указатель проходит свой список один раз и не возвращается назад. Слияние, поиск пары с заданной суммой — тема занятия 8.

T(n) = O(n + m) · S(n) = O(1) без учёта результата

Признак класса: один проход по входу, тело которого выполняется за постоянное время. Несколько последовательных проходов класс не меняют: n + n + n = 3n = O(n).

V · КЛАССЫ

Класс 4 из 6 · определение и прогон

Линейно-логарифмическая сложность O(n log n)

Класс возникает, когда проход по всем n элементам повторяется log n раз. Типичный источник — схема «разделяй и властвуй»: данные делятся пополам, результаты объединяются за линейное время.

Склад · сортировка слиянием восьми цен

[90][15][240][60][75][15][300][45]·[15,90][60,240][15,75][45,300]·[15,60,90,240][15,45,75,300]·[15,15,45,60,75,90,240,300]

три уровня слияния по 8 элементов → 24 просмотра

сортировка слиянием · n = 32

просмотров 0

Полоса пробегается заново на каждом уровне слияния: пять уровней по 32 элемента — 160 просмотров.

V · КЛАССЫ

Класс 4 из 6 · примеры

Где встречается линейно-логарифмическая сложность

Библиотечная сортировка
prices.sort()
sorted(prices)[:3]

В CPython применяется Timsort — сортировка слиянием, которая сначала находит уже упорядоченные куски и склеивает их. На почти отсортированных данных приближается к O(n).

T(n) = O(n log n) · S(n) = O(n)
Сортировка с последующим проходом
orders.sort()
for i in range(1, len(orders)):
    if orders[i] == orders[i - 1]:
        return True

Поиск дубликата без дополнительной памяти: O(n log n) на сортировку плюс O(n) на проход. По свойству суммы остаётся старшее слагаемое.

T(n) = O(n log n) · S(n) = O(1) при сортировке на месте
k-й по величине элемент
def kth_largest(prices, k):
    return sorted(prices, reverse=True)[k - 1]

Третья по величине цена, десятый по времени заказ. Весь список упорядочивается ради одного элемента — отсюда n log n. Способ решить это за O(n) разбирается в разделе про кучу.

T(n) = O(n log n) · S(n) = O(n)

Признак класса: сортировка в решении либо рекурсивное деление задачи пополам с линейным объединением. Наличие sorted() само по себе задаёт нижнюю границу класса — быстрее решение уже не будет.

V · КЛАССЫ

Класс 5 из 6 · определение и прогон

Квадратичная сложность O(n²)

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

Проходная · число пар при переборе

5 пропусков → 10 пар · 10 пропусков → 45 пар

вход вырос вдвое, работа — в 4,5 раза

1000 пропусков → 499 500 пар

все пары · n = 24

сравнений 0

Клетка — пара «элемент i против элемента j». Заполняется треугольник: пар ровно n(n−1)/2 = 276.

V · КЛАССЫ

Класс 5 из 6 · примеры

Где встречается квадратичная сложность

Перебор всех пар
for i in range(len(points)):
    for j in range(i + 1, len(points)):
        distance(points[i], points[j])

Ближайшая пара точек, поиск дубликатов, сравнение каждого с каждым. Множитель 1/2 от условия j > i на класс не влияет.

T(n) = O(n²) · S(n) = O(1)
Сортировка вставками
for i in range(1, len(data)):
    j = i
    while j > 0 and data[j-1] > data[j]:
        data[j-1], data[j] = data[j], data[j-1]
        j -= 1

В худшем случае — обратный порядок — каждый элемент проходит весь пройденный участок. На почти упорядоченных данных вырождается в O(n).

T(n) = O(n²) в худшем, O(n) в лучшем
Линейный поиск внутри цикла
for row in rows:
    if row not in result:
        result.append(row)

Цикл записан один, но not in для списка выполняет собственный проход. Скрытый случай квадратичной сложности — раздел VI.

T(n) = O(n²) · S(n) = O(n)

Признак класса: вложенные циклы по одному входу либо операция стоимостью O(n) внутри цикла по входу. Если внутренний цикл идёт по другой величине m, оценка равна O(n·m).

V · КЛАССЫ

Класс 5 из 6 · продолжение

Полиномиальные классы выше второго

Квадратичный класс — частный случай полиномиального O(n^k). Третья степень возникает при трёх вложенных циклах и встречается в задачах на матрицы и тройки элементов.

Умножение матриц
for i in range(n):
    for j in range(n):
        for k in range(n):
            c[i][j] += a[i][k] * b[k][j]

Три вложенных цикла по n: для каждой клетки результата складываются n произведений. При n = 500 это 1,25·10⁸ операций — предел того, что успевает уложиться в секунду.

T(n) = O(n³) · S(n) = O(n²)
Тройки с нулевой суммой
for i in range(n):
    for j in range(i+1, n):
        for k in range(j+1, n):
            if a[i] + a[j] + a[k] == 0:
                ...

Полный перебор троек. Существует решение за O(n²) — сортировка плюс два указателя; типичная задача собеседования на переход от перебора к оптимизации.

T(n) = O(n³) перебором, O(n²) после сортировки

Допустимый размер входа

O(n²)n ≤ 5·10³2,5·10⁷ операцийукладывается
O(n³)n ≤ 5001,25·10⁸ операцийна границе
O(n⁴)n ≤ 10010⁸ операцийна границе

Полиномиальные алгоритмы считаются практически применимыми, но лишь при малых степенях. Разница между O(n²) и O(n³) при n = 1000 составляет три порядка: 10⁶ против 10⁹ операций.

V · КЛАССЫ

Класс 6 из 6 · определение и границы применимости

Экспоненциальная и факториальная сложность

Алгоритм имеет экспоненциальную сложность, если добавление одного элемента умножает объём работы на постоянную величину. Прогон не показан: при n = 24 потребовалось бы 16,7 миллиона клеток.

Перебор подмножеств · O(2ⁿ)
for mask in range(2 ** len(items)):
    ...

Каждый предмет либо входит в набор, либо нет: 2ⁿ вариантов. Задача о рюкзаке в лоб, поиск подмножества с заданной суммой.

T(n) = O(2ⁿ) · применимо при n ≤ 25
Перебор перестановок · O(n!)
# permutations перебирает все порядки
# обхода городов: 3 города → 6 маршрутов,
# 4 города → 24, 10 городов → 3 628 800
for route in permutations(cities):
    length = total_distance(route)

Задача коммивояжёра полным перебором: проверяется каждый возможный порядок обхода. При 20 городах это 2,4·10¹⁸ маршрутов — перебор невозможен ни при каком быстродействии.

T(n) = O(n!) · применимо при n ≤ 11

Рост числа вариантов

n = 102ⁿ1 024мгновенно
n = 202ⁿ1 048 576доли секунды
n = 402ⁿ1,1·10¹²около суток
n = 642ⁿ1,8·10¹⁹неприменимо

Экспоненциальные решения не всегда означают ошибку: при малом n они допустимы и просты. Ограничение n ≤ 20 в условии задачи, как правило, указывает именно на полный перебор. Способы сократить перебор — раздел 7 курса.

V · КЛАССЫ

Раздел V · сравнение на одной задаче

Поиск дубликата: линейное решение против квадратичного

Определить, есть ли в списке два одинаковых номера. Слева — проход с множеством просмотренных значений, справа — перебор всех пар.

через множество · O(n)

проверок 0

все пары · O(n²)

сравнений 0

Что произошло

24 проверки против 276 при n = 24. На 1024 элементах — в 511 раз.

За счёт чего

Логика та же. Изменилась структура: проверка «видели ли раньше» стоит 1, а не n.

Вывод

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

V · КЛАССЫ

Раздел V · кривые роста

Пять классов на одной координатной сетке

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

  • O(1) горизонтальная прямая: значение не зависит от n
  • O(log n) возрастает всё медленнее: удвоение n добавляет одну операцию
  • O(n) прямая: во сколько раз вырос вход, во столько же выросла работа
  • O(n log n) растёт быстрее линейной, оставаясь сопоставимой с ней
  • O(n²) покидает область графика на втором десятке

Верхняя граница шкалы выбрана по кривой n log n: при шкале по n² три нижних кривых были бы неразличимы. Точки пересечения в левой части — область малых n, где асимптотическая оценка не определяет выбор.

Рост числа операций при n от 0 до 32
V · КЛАССЫ

Раздел V · числовые значения

Порядок роста в числах операций

Классn = 10n = 1 000n = 1 000 000Оценка времени при 10⁷ операций в секунду
O(1)111не зависит от n
O(log n)31020практически не зависит от n
O(n)101 00010⁶0,1 секунды
O(n log n)3310⁴2·10⁷около 2 секунд
O(n²)10010⁶10¹²более суток
O(2ⁿ)1 02410³⁰¹—неприменимо

Столбец n = 10 показывает, почему оценка не применяется к малым входам: различие между 33 и 100 операциями несущественно. Столбец n = 10⁶ показывает обратное: при том же алгоритме различие между O(n log n) и O(n²) достигает пяти порядков. На этом экране заканчивается первая пара.

V · КЛАССЫ

Раздел VI · стоимость библиотечных операций

Операции, содержащие скрытый проход

Отсутствие цикла в тексте не означает постоянной стоимости: часть операций языка выполняет полный просмотр внутри себя. Ниже фрагмент 10 из fragments.py: цикл записан один, сложность квадратичная.

def unique_keep_order(rows):
    result = []
    for row in rows:            # n итераций
        if row not in result:   # до n сравнений
            result.append(row)
    return result

Число операций: n итераций × до n сравнений = O(n²). На отчёте из 10 000 строк без повторов это до 50 миллионов сравнений. Замена списка result на множество снижает оценку до O(n) ценой O(n) памяти.

ОперацияСтоимость
x in data · списокO(n)
x in seen · множествоO(1)
sorted(data)O(n log n)
data[1:] · копирование срезаO(n)
data.insert(0, x) · сдвигO(n)
data.pop(0) · сдвигO(n)
sum(data), max(data)O(n)
data.append(x)O(1)*
len(data)O(1)
VI · АНАЛИЗ

Раздел VI · зависимость от данных

Лучший, средний и худший случай

На входах одного размера алгоритм может выполнять разное число операций. Линейный поиск прекращает работу при первом совпадении, поэтому T(n) — не одна функция, а три.

лучший случай · Ω(1)

[902, 417, 133, 604, 288, 715, 950, 366]

1 сравнение

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

средний случай · O(n)

[417, 133, 604, 902, 288, 715, 950, 366]

4 сравнения

Математическое ожидание по равновероятным позициям равно (n+1)/2 — тот же порядок роста.

худший случай · O(n)

[417, 133, 604, 288, 715, 950, 366, 481]

8 сравнений

Элемент отсутствует либо находится в последней позиции.

В ответе указывают худший случай: он даёт гарантию, не зависящую от распределения данных, и именно его проверяет тестирующая система, подавая предельный вход. Средний случай называют дополнительно, когда он отличается по порядку. У быстрой сортировки это O(n log n) в среднем и O(n²) в худшем, у поиска по хеш-таблице — O(1) и O(n).

VI · АНАЛИЗ

Раздел VI · рекурсивные алгоритмы

Дерево вызовов и подсчёт по уровням

Зачем отдельный приём. Всё, что было раньше, работало с циклами: посчитал итерации — получил оценку. У рекурсии циклов нет, а функция вызывает саму себя, и сколько раз — по коду сразу не видно.

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

Сортировка слиянием: вызов на массиве длины n порождает два вызова на половинах и выполняет слияние за O(n). На каждом уровне суммарная длина обрабатываемых участков равна n, а уровней — log₂n. Произведение даёт O(n log n).

Как считать любую рекурсию: ① выписать, что делает один вызов кроме рекурсивных; ② определить, на сколько частей задача делится и какого они размера; ③ посчитать число уровней; ④ умножить работу уровня на число уровней.

Здесь: один вызов делает слияние за n, деление пополам даёт log₂n уровней, значит n × log n.

сортировка слиянием · n = 8работа уровня
уровень 0
8
8
уровень 1
44
8
уровень 2
2222
8
уровень 3
11111111
8

уровней log₂8 = 3 слияния · работа уровня n = 8 · итого 24 просмотра

VI · АНАЛИЗ

Раздел VI · амортизированный анализ

Стоимость, усреднённая по последовательности операций

Мы записали append в таблицу со стоимостью 1. Изредка это неправда: одно добавление стоит n. Вопрос в том, можно ли всё равно считать его единицей.

Список хранится в непрерывном участке памяти фиксированной ёмкости. Когда ёмкость исчерпана, выделяется участок примерно вдвое больше и элементы копируются: такое append стоит O(n), все остальные — O(1).

Подсчёт для 1000 добавлений: расширений 10, скопировано 1023 элемента, всего около 2023 операций — 2,02 операции на одно добавление. Величина не растёт с n, поэтому append называют операцией амортизированной стоимости O(1).

Добавление №Ёмкость доРасширениеСкопировано
10да0
21да1
32да2
54да4
98да8
10…1616нет0
513…10001024нет0

Дорогие расширения случаются всё реже: промежуток между ними удваивается — поэтому O(1) для append законно. А insert(0, x) дорог каждый раз, и там O(n) остаётся.

Не путать со средним случаем: средний случай усредняет по входным данным, амортизированная стоимость — по последовательности операций, без предположений о данных.

VI · АНАЛИЗ

За клавиатуру · 6 минут

Запустить счётчик операций и прочитать вывод

1 · что сделать

cd lesson_02
python opcount.py \
  --sizes 8 16 32 64 128 1024

Файл лежит в course-materials/complexity/ — скопируйте папку к себе. Скрипт ничего не спрашивает и не требует установки: он считает сравнения и печатает две таблицы.

2 · что появится на экране

     n     O(1)  O(log n)   O(n)  O(n log n)    O(n²)
     8        1         6      8          12       28
    16        1         8     16          32      120
    32        1        10     32          80      496
    64        1        12     64         192     2016
   128        1        14    128         448     8128
  1024        1        20   1024        5120   523776

ВО СКОЛЬКО РАЗ БОЛЬШЕ, КОГДА n УДВАИВАЕТСЯ
    16    1.00x     1.33x  2.00x       2.67x    4.29x
   128    1.00x     1.17x  2.00x       2.33x    4.03x

3 · на что смотреть

  1. Столбец O(1) — единица в каждой строке. Сколько бы ни было данных, работа не меняется.
  2. Столбец O(n) — числа совпадают с n. Один проход по списку.
  3. Столбец O(n²) — при n = 1024 полмиллиона. В 511 раз больше, чем у соседнего столбца O(n).
  4. Вторая таблица — во сколько раз выросло при удвоении n. Это главное: 2.00x у линейного, 4.03x у квадратичного, 1.17x у логарифмического.

Проверка: добавьте 2048 в список размеров. Множитель у O(n²) останется около 4, у O(log n) станет ближе к 1,1. Выпишите оба числа в notes.md.

РУКИ

Раздел VII · память

Память считают по созданным структурам, а не по строкам кода

S(n) — сколько новых ячеек заводит решение сверх того, что ему передали. Список, пришедший в аргументах, не считается: он существовал до вызова.

O(1)

Переменных столько же при любом n: счётчик, границы, накопитель. Сколько бы раз цикл ни повторился, новых ячеек не прибавляется.

O(n)

Создана структура, длина которой повторяет вход: множество, словарь, копия, срез, новый список из sorted.

O(глубины)

Рекурсия: каждый незавершённый вызов держит свой кадр в стеке. Глубина рекурсии и есть оценка памяти.

Отсюда рабочий вопрос к коду: что здесь создаётся и растёт ли оно вместе с n. Цикл сам по себе память не занимает; занимает то, что внутри него накапливается. А требование «дополнительной памяти O(1)» в условии сразу отсекает решения с множеством или копией — даже если по времени они лучше.

список из 1000 номеров пришёл в функцию · сколько ячеек добавилось
0сам список — он был до вызова
1count = 0 — одна переменная, при n = 10⁶ тоже одна
3low, high, mid — три переменные, от n не зависят
1000seen = set(badges) — множество из всех номеров
1000copy = badges[:] — срез копирует целиком
1000sorted(badges) — новый упорядоченный список

Верхние три строки не меняются с ростом n → S(n) = O(1). Нижние три повторяют длину входа → S(n) = O(n). Больше памяти брать неоткуда: всё, что не переменная и не входной список, — это созданная структура.

VII · ПАМЯТЬ

Раздел VII · память · примеры

Сколько памяти у знакомых решений

Четыре функции, которые уже разбирались. Время у них разное, а память определяется одним: завели новую структуру или обошлись переменными.

S(n) = O(1)
def count_late(orders):
    late = 0
    for order in orders:
        if order.delay > 0:
            late += 1
    return late

Одна переменная late. Список из десяти заказов и из миллиона обрабатываются с одной и той же дополнительной памятью.

создано: 1 ячейка
S(n) = O(1)
def has_pair(nums, target):
    for i in range(len(nums)):
        for j in range(i + 1, len(nums)):
            if nums[i] + nums[j] == target:
                return True
    return False

Самое медленное решение из четырёх, O(n²) по времени. Но памяти не занимает: только счётчики i и j.

создано: 2 ячейки · медленно, зато экономно
S(n) = O(n)
def has_duplicate(badges):
    seen = set()
    for badge in badges:
        if badge in seen:
            return True
        seen.add(badge)
    return False

Самое быстрое, O(n) по времени. Плата — множество, которое в худшем случае вырастет до размера входа.

создано: до n ячеек · быстро, но за память
S(n) = O(n)
def top_three(prices):
    return sorted(prices)[:3]

Циклов нет, но sorted() возвращает новый список из всех цен. Срез из трёх элементов берётся уже от него.

создано: n ячеек, хотя нужны три

Правило для разметки: завели множество, словарь, копию или результат sorted() — это O(n) памяти; обошлись счётчиками и индексами — O(1). Промежуточных случаев на этом занятии не встретится.

VII · ПАМЯТЬ

Раздел VIII · границы метода

Два случая, когда оценка не отвечает на вопрос

Оценка говорит, что будет при большом n. Есть две ситуации, где этого недостаточно, и решение выбирают не по ней.

Случай 1 · данных мало

Квадратичное решение с простым телом цикла обгоняет «правильное» n log n, пока элементов немного: у второго больше действий на каждом шаге.

nn²100·n log nбыстрее
162566 400n²
25665 536204 800n²
1 00010⁶997 000поровну
10 00010⁸1,3·10⁷n log n

Множитель 100 — цена одного шага: рекурсия, вызовы, работа с памятью. У двойного цикла такой цены нет.

Случай 2 · данные особенные

Оценка берётся по худшему случаю. Если реальные данные до него не доходят, программа работает быстрее, чем обещает запись.

  • Почти упорядоченный список. Сортировка вставками в худшем случае O(n²), а на таких данных — O(n). Встроенная сортировка Python на этом и построена.
  • Элемент обычно в начале. Поиск перебором — O(n) в худшем случае, но если искомое почти всегда первое, реально это одно-два сравнения.
  • Словарь и множество. O(1) в среднем и O(n) в худшем — при массовых совпадениях хешей. На практике исходят из среднего.

Оценку называют всегда: она отсекает заведомо неподходящие решения до написания кода. Но если размер данных известен и невелик, выбирают замером. Оценка отвечает на вопрос «что будет, когда данных станет много», замер — «что быстрее вот на этих данных». Замерами занимаемся на занятии 4.

VIII · ГРАНИЦЫ

Раздел IX · применение

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

Обратная задача анализа: по ограничению из условия определить, какие решения имеют шанс уложиться в лимит времени. Ориентир для Python — 10⁷ элементарных операций в секунду; величина приблизительная и зависит от машины и содержимого цикла.

ОграничениеДопустимый классТипичное решение
n ≤ 12O(n!)перебор перестановок
n ≤ 25O(2ⁿ)перебор подмножеств
n ≤ 500O(n³)тройной вложенный цикл, матрицы
n ≤ 5 000O(n²)перебор всех пар
n ≤ 10⁵O(n log n)сортировка, бинарный поиск по ответу
n ≤ 10⁶O(n)проходы, множество, словарь
n ≤ 10⁹O(log n) или O(1)формула, половинное деление

Последняя строка: при n ≤ 10⁹ один проход по входу требует около двух минут — значит, исключён самим ограничением. Если при этом на входе одно число, а не список, ответ получают формулой либо делением пополам.

IX · ПРИМЕНЕНИЕ

Раздел IX · формулировка оценки

Как оценка сообщается на собеседовании

Ответ состоит из четырёх элементов: класс по времени с указанием подсчитанных операций, класс по памяти с указанием хранимого, худший случай и рассмотренные альтернативы.

Образец полного ответа

Время — O(n): один проход, на каждом элементе проверка по множеству за O(1).

Память — O(n): храню множество просмотренных номеров.

Худший случай — все номера различны: ранний выход не срабатывает, множество заполняется целиком.

Альтернатива — перебор пар: O(n²) времени и O(1) памяти; выбрал первый вариант, поскольку ограничения по памяти в условии нет.

Что оценивает интервьюер

  • названы операции, а не только запись класса
  • память оценена отдельно от времени
  • указан вход, на котором достигается худший случай
  • назван отвергнутый вариант и причина отказа
  • оценка произнесена до написания кода, а не после

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

IX · ПРИМЕНЕНИЕ

Раздел IX · рабочий порядок

Последовательность анализа незнакомого кода

01Найти величину, которая растёт

Что именно называется n: элементы списка, символы строки, вершины графа. При нескольких входах — все величины.

02Выделить базовую операцию

Сравнение, сложение, просмотр ребра — то, что выполняется чаще прочего и определяет порядок роста.

03Выписать стоимость участков

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

04Сложить и перемножить

Последовательные участки складываются, вложенные перемножаются; для рекурсии — работа уровня умножается на число уровней.

05Упростить запись

Отбросить постоянные множители и младшие слагаемые, оставить старшее.

06Проверить подстановкой

Посчитать операции при n = 8 и n = 16: рост вдвое подтверждает O(n), вчетверо — O(n²), на постоянную величину — O(log n).

Тот же порядок применяется к оценке памяти: вместо числа операций выписывается объём созданных структур, включая кадры стека для рекурсивных решений.

IX · ПРИМЕНЕНИЕ

Задание 1 домашней работы · разбираем постановку · 6 минут

Определить класс сложности чужого кода

В файле fragments.py двенадцать работающих функций из разных предметных областей: стадион, пункт выдачи, проходная, склад, архив чеков, теплица, курьеры, служба поддержки, отчёт, упаковка. Сейчас разбираем первый фрагмент вместе, остальные одиннадцать — дома.

  1. Откройте answers.py: три словаря — TIME, SPACE, WORST_CASE.
  2. Укажите T(n) и S(n) для всех двенадцати фрагментов, применяя порядок разбора по порядку из раздела IX.
  3. Опишите худший случай для фрагментов 01 и 04 с указанием входных данных.
  4. Доведите pytest до полного прохождения — это и есть критерий приёмки задания.

Критерии

  • Запись без множителей и младших слагаемых
  • S(n) — только дополнительная память
  • Худший случай назван через входные данные

Если оценка не сходится

Подставьте n = 8 и n = 16, посчитайте операции и сравните числа: рост вдвое, вчетверо или на постоянную величину прямо указывает на класс.

ДЗ · 1

Задание 2 домашней работы · разбираем образец · 5 минут

Подобрать решение под ограничение

1 · что сделать

В файле limits.md шесть условий. По каждому пишутся ровно три строки — те же три, что в образце справа. Код писать не нужно.

  1. Проходит: какой класс укладывается в лимит. Считается прикидкой: сколько операций даст перебор — n², n·q или 2ⁿ — и сравнивается с ориентиром 10⁷ операций в секунду.
  2. Решение: какая структура снимает лишнюю работу, и какие у неё T(n) и S(n).
  3. Что мешало проще: почему перебор не годится, с числом операций.

Дома: бланк lesson_02/limits.md, шесть условий. Это единственное задание, где ИИ разрешён прямо — порядок работы с ним разобран на экране домашнего задания.

2 · образец: так выглядит ответ на одно условие

Условие 1. Номера заказов за сутки, n ≤ 200 000. Есть ли среди них два одинаковых?

строка 1
проходит

O(n) или O(n log n). Это 2·10⁵ и 3,5·10⁶ операций — доли секунды при ориентире 10⁷.

строка 2
решение

Множество просмотренных номеров: один проход, проверка принадлежности за O(1).
T(n) = O(n) · S(n) = O(n)

строка 3
что мешало проще

Перебор пар пишется в четыре строки и памяти не требует, но это n²/2 = 2·10¹⁰ сравнений — около получаса.

Больше трёх строк не нужно. Если по условию ограничена ещё и память, четвёртой строкой называется запасной вариант: здесь это «отсортировать и пройти соседей» — O(n log n) времени и O(1) памяти.

3 · что проверяется

  1. Названо число. Не «много операций», а 2·10¹⁰ — прикидка по формуле.
  2. Названа структура. Не «сделать быстрее», а множество, словарь, сортировка, бинарный поиск.
  3. Названа цена. Что теряем: память, порядок исходных данных, простоту кода.
  4. Две величины там, где их две. В условиях 3 и 6 в ответе должны стоять обе: O(n + q), а не O(n).
ДЗ · 2

Задание 3 домашней работы · смотрим код · 4 минуты

Оценить незнакомый алгоритм

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

  1. T(n) с указанием подсчитанных операций.
  2. S(n): объём дополнительной памяти и что в нём хранится.
  3. Худший случай: журнал, при котором выполняется наибольшее число операций.
  4. Строка со скрытой стоимостью: операция, содержащая внутренний проход.

Дома: бланк lesson_02/acceptance.md. Оценка засчитывается при наличии обоснования: запись класса без указания подсчитанных операций не показывает хода рассуждения.

Условия задания

Без ИИ и поисковых систем: это контроль. Запуск файла и добавление счётчиков разрешены — так и проверяют себя, когда оценка не сходится.

Сейчас смотрим код и называем, где в нём прячется скрытая стоимость. Четыре пункта пишутся дома.

ДЗ · 3

Карточки · набор А · 6 минут

Определите оценки для шести фрагментов

Для каждой карточки запишите четыре величины: T(n) — число операций формулой, O — класс по времени, S(n) — память, худший случай — вход, на котором работы больше всего.

А1
def first_order(orders):
    if not orders:
        return None
    return orders[0]
А2
def count_late(orders):
    late = 0
    for order in orders:
        if order.delay > 0:
            late += 1
    return late
А3
def avg_and_max(temps):
    total = 0
    for t in temps:
        total += t
    best = temps[0]
    for t in temps:
        if t > best:
            best = t
    return total / len(temps), best
А4
def has_pair(nums, target):
    for i in range(len(nums)):
        for j in range(i + 1, len(nums)):
            if nums[i] + nums[j] == target:
                return True
    return False
А5
def unique_codes(codes):
    result = []
    for code in codes:
        if code not in result:
            result.append(code)
    return result
А6
def steps_to_one(number):
    steps = 0
    while number > 1:
        number //= 2
        steps += 1
    return steps
КАРТОЧКИ

Карточки · набор А · разбор

Ответы с подсчётом

А1O(1)

3 операции: проверка, обращение по индексу, возврат

удвоили вход → ×1, ничего не изменилось

T(n) = 3 · S(n) = O(1) · худший случай: любой вход

А2O(n)

заголовок n, сравнение n, late += 1 в худшем случае тоже n, вне цикла 2

удвоили вход → ×2

T(n) = 3n + 2 · S(n) = O(1) · худший случай: опоздали все заказы

А3O(n)

два цикла подряд, стоимости складываются: 2n + 3n + 6. Во втором цикле best = t в худшем случае выполняется каждый раз

удвоили вход → ×2

T(n) = 5n + 6 · S(n) = O(1) · худший случай: температуры возрастают

А4O(n²)

сравнений n(n−1)/2, плюс заголовки обоих циклов. Это пример ④ из раздела III

удвоили вход → ×4

T(n) = n² + n + 2 · S(n) = O(1) · худший случай: подходящей пары нет

А5O(n²)

цикл один, но not in по списку стоит до n: n × n

удвоили вход → ×4

T(n) = n² + 2n + 2 · S(n) = O(n) · худший случай: все коды различны

А6O(log n)

k = log₂number делений, на каждом три действия: проверка, деление, счётчик. Для 10⁹ — 30 итераций

удвоили вход → +1 операция

T(n) = 3·log₂n + 3 · S(n) = O(1) · размер входа — число разрядов

Как здесь считаются операции

Считаются действия, а не строки: total / len(temps) — два действия. Заголовок цикла for x in seq — по одному действию на итерацию, всего n; for i in range(len(x)) — плюс одно разовое. Строка внутри if считается по худшему случаю: столько раз, сколько условие может оказаться истинным.

Последнее правило и отличает А3 от примера ③ раздела III. Там условие value > average истинно не для всех значений, а здесь строго возрастающие температуры переписывают best на каждом шаге. Отсюда 5n вместо 4n. Константа зависит от того, насколько мелко дробить действия, поэтому сверяется коэффициент при n и старшее слагаемое.

Разбор А3 и А5 рядом показывает главное различие: в А3 циклы идут подряд и стоимости складываются, в А5 проход спрятан внутри строки и стоимости перемножаются.

КАРТОЧКИ

Карточки · набор Б · 6 минут

Шесть фрагментов с другими характеристиками

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

Б1
def top_three(prices):
    return sorted(prices)[:3]
Б2
def merge_reports(day, night):
    result = []
    for row in day:
        result.append(row)
    for row in night:
        result.append(row)
    return result
Б3
def count_words(words):
    counts = {}
    for word in words:
        counts[word] = counts.get(word, 0) + 1
    return counts
Б4
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)
Б5
def assign(orders, couriers):
    for order in orders:
        for courier in couriers:
            if fits(order, courier):
                mark(order, courier)
Б6
def find_code(codes, target):
    low, high = 0, len(codes) - 1
    while low <= high:
        mid = (low + high) // 2
        if codes[mid] == target:
            return mid
        if codes[mid] < target:
            low = mid + 1
        else:
            high = mid - 1
    return -1
КАРТОЧКИ

Карточки · набор Б · разбор

Ответы с подсчётом

Б1O(n log n)

циклов нет, но sorted() сортирует весь список; срез из трёх элементов стоит O(1)

удвоили вход → ×2,2

T(n) ≈ n log n + 4 · S(n) = O(n) — sorted создаёт новый список

Б2O(n + m)

два цикла по разным спискам, в каждом заголовок и append: 2n + 2m + 2. Одной буквой не записывается

удвоили оба входа → ×2

T = 2n + 2m + 2 · S = O(n + m) — результат хранит все строки

Б3O(n)

n итераций, в строке три действия: get, сложение и запись по ключу. Каждое стоит 1

удвоили вход → ×2

T(n) = 4n + 2 · S(n) = O(n) — словарь растёт с числом различных слов

Б4O(2ⁿ)

каждый вызов порождает два новых: 2ⁿ вызовов. fib(40) — около 10⁹

увеличили n на 1 → ×2

T(n) = 2ⁿ · S(n) = O(n) — глубина стека рекурсии

Б5O(n · m)

вложенные циклы по разным величинам: n заказов × m курьеров. Внутри проверка и, в худшем случае, назначение

удвоили оба входа → ×4

T = 3n·m + n · S = O(1) · при n = m это O(n²) · худший случай: подходят все пары

Б6O(log n)

log₂n шагов, на каждом около шести действий: проверка цикла, середина, два сравнения, сдвиг границы. Требует упорядоченного списка

удвоили вход → +1 шаг

T(n) ≈ 6·log₂n + 4 · S(n) = O(1) · худший случай: элемента нет

Три ответа из шести содержат то, о чём чаще всего забывают: Б1 — память под результат sorted(), Б2 и Б5 — вторую величину в оценке, Б4 — память под стек рекурсии. Именно эти три пункта проверяются на приёмке.

КАРТОЧКИ

Справка · материал занятия в сжатом виде

Сводка

Определение

T(n) = O(f(n))
⟺ ∃ c, n₀: T(n) ≤ c·f(n)
при всех n ≥ n₀

Классы

1 ≺ log n ≺ n ≺ n log n
≺ n² ≺ n³ ≺ 2ⁿ ≺ n!

Свойства

O(c·f) = O(f)
O(f) + O(g) = O(max)
O(f) · O(g) = O(f·g)

Правила анализа

подряд — складываем
вложено — перемножаем
деление — логарифм
вызов в цикле — умножаем

Дорогие операции

x in список — O(n)
sorted() — O(n log n)
data[1:] — O(n)
insert(0, x) — O(n)

Память

S(n) — созданное сверх входа
счётчики, индексы — O(1)
set, копия, sorted() — O(n)
рекурсия — O(глубины)

Форма ответа: «Время O(?), поскольку подсчитанные операции. Память O(?): хранится что. Худший случай — входные данные. Альтернатива — вариант и причина отказа».

СПРАВКА

Справка · распространённые ошибки

Шесть неверных утверждений

Два цикла подряд дают O(n²)

Последовательные участки складываются: n + n = 2n, класс O(n). Перемножаются только вложенные.

Половина пар — другой класс

n(n−1)/2 отличается от n² постоянным множителем, который отбрасывается по свойству оценок.

Нет цикла — значит O(1)

sorted(), in для списка и срез выполняют проход по всем элементам без явного цикла.

O(n²) всегда хуже O(n log n)

При постоянном множителе 100 у второго квадратичное решение выигрывает до n ≈ 1000. Оценка описывает поведение при росте n.

Сложность зависит от языка

Число операций определяется алгоритмом. От реализации и машины зависит время выполнения — занятие 4.

Рекурсия не расходует память

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

СПРАВКА

Вопрос на выход

Алгоритм выполняет 2n + 5 сравнений.
Укажите класс сложности и обоснуйте.

Полный ответ содержит три элемента: запись класса, применённые свойства оценок и пару (c, n₀), при которой выполняется определение.

ИТОГ

Домашнее задание № 2 · к следующему занятию

Максим Николаевич не хочет задавать домашнее задание. Но очень хочет

Три задания — это те три практики, которые мы разобрали в конце занятия. Задачи вы уже видели, порядок разбора знаете; дома остаётся довести до конца.

  1. ①
    Разметка · answers.py

    T(n) и S(n) для всех двенадцати фрагментов, худший случай для 01 и 04. Довести до полного прохождения pytest. Без ИИ: оценку называете сами.

  2. ②
    Ограничения · limits.md

    Шесть условий по три строки. С ИИ — и к каждому ответу модели своя строка «почему это верно», с числом.

  3. ③
    Приёмка · acceptance.md

    Оценить незнакомый алгоритм из acceptance_vetclinic.py по четырём пунктам. Без ИИ: это контроль.

Кот с подписью «надеюсь после учёбы стану миллионером»
ДЗ

Домашнее задание № 2 · задание 2 · как именно работать с моделью

Модель отвечает, разбираетесь вы

Второе задание — единственное, где ИИ разрешён прямо и намеренно. Задача — научиться проверять чужую оценку: то же самое вы делаете на review и на секции, когда решение предлагает кто-то другой.

01Спросить

Дать модели условие из limits.md целиком и попросить три строки: какой класс проходит, какое решение, что мешало проще.

02Проверить числом

Пересчитать её прикидку самому: сколько операций даёт перебор и как это соотносится с ориентиром 10⁷ в секунду. Число должно сойтись.

03Дописать «почему»

К каждому из шести ответов — своя строка с обоснованием. Строка без числа не засчитывается.

Обязательный седьмой пункт файла

Одно место, где модель ошиблась или ответила расплывчато, и как вы это поймали. Такое место найдётся: модели уверенно пишут O(n) там, где внутри стоит in по списку, и путают O(n + q) с O(n), когда величин на входе две.

Если после проверки ошибок не нашлось — так и напишите, но приложите расчёт по двум условиям, которым вы её проверяли. Пункт «ошибок нет» без расчёта не принимается.

ДЗ

Домашнее задание № 2 · порядок сдачи

Куда положить и как сдать

Папка

lesson_02/ в личном репозитории: answers.py, limits.md, acceptance.md. Бланки и чек-лист уже лежат в starter-pack.

Ветка и PR

Ветка hw-02, коммит с содержательным сообщением, Pull Request в собственную main.

Срок

До начала занятия 4: с проверки этих оценок замером занятие и начнётся.

Критерии

Зелёный pytest, шесть обоснований с числами, седьмой пункт про ошибку модели, четыре пункта приёмки.

Про ИИ в первом и третьем задании: там он закрыт, потому что на занятии 4 мы измеряем ровно те решения, которые вы разметили, и расхождение сразу видно. Во втором задании ИИ разрешён, я сам так делаю всегда. Разница между «спросил» и «списал» — та самая строка «почему это верно».

ДЗ

ОП.04 · Основы алгоритмизации и программирования · 3 курс

Домашнее задание № 2

Занятие 3 — временная сложность и O-нотация

А/П

Максим Николаевич не хочет задавать вам домашнее задание. Но очень хочет.

Что нужно сделать

  1. Разметка двенадцати фрагментов — файл answers.py. Без ИИ. В fragments.py двенадцать рабочих функций. Для каждой укажите T(n) и S(n), для фрагментов 01 и 04 опишите худший случай через входные данные. Запись без множителей и младших слагаемых: правильно O(n), неправильно O(2n) и O(n + 5). Если величин на входе две и они растут независимо — в ответе остаются обе: O(n + m). Проверка: pytest course-materials/complexity/test_lesson_03.py -q, доводите до полного прохождения. Тест называет фрагмент, но не называет верный ответ, поэтому число попыток не ограничено.
  2. Ограничения — файл limits.md. С ИИ, и это главное в задании. Шесть условий, по каждому три строки: какой класс проходит, какое решение предлагаете, что мешало взять решение попроще. Порядок работы: даёте модели условие целиком и получаете её три строки; пересчитываете её прикидку сами — сколько операций даёт перебор и как это соотносится с ориентиром 10 000 000 операций в секунду; дописываете к каждому ответу свою строку «почему это верно» с числом. Строка без числа не засчитывается. Седьмым пунктом файла — одно место, где модель ошиблась или ответила расплывчато, и как вы это поймали. Если после проверки ошибок не нашлось, так и напишите, но приложите расчёт по двум условиям, которыми вы её проверяли: пункт «ошибок нет» без расчёта не принимается.
  3. Приёмка чужого алгоритма — файл acceptance.md. Без ИИ. В acceptance_vetclinic.py функция, определяющая по журналу приёмов час с наибольшим числом визитов и количество различных пород в этот час. Код рабочий, менять его не нужно. Назовите четыре вещи: T(n) с указанием того, какие операции вы считали; S(n) — объём дополнительной памяти и что в нём хранится; худший случай — журнал, при котором операций больше всего; строку со скрытой стоимостью — операцию, внутри которой прячется ещё один проход. Запуск файла и добавление счётчиков разрешены.

Первое и третье задания без ИИ не из вредности: на занятии 4 мы измерим ровно те решения, которые вы разметили, и расхождение между вашей оценкой и замером будет видно сразу. Второе задание — тренировка того, что происходит на review и на секции: оценку предлагает кто-то другой, а проверяете её вы.

Как сдавать

Папкаlesson_02/ в личном репозитории курса: answers.py, limits.md, acceptance.md. Бланки и чек-лист лежат в starter-pack, папка lesson_02. В первой строке каждого файла — фамилия и группа.
Ветка и PRВетка hw-02, коммит с понятным сообщением, затем Pull Request в свою main с коротким описанием: какие два фрагмента оказались спорными и почему. Merge делаете сами после проверки.
СрокДо начала занятия 4: с проверки этих оценок замером занятие и начнётся.
ПроверяемЗелёный pytest по двенадцати фрагментам, шесть обоснований с числами в limits.md, седьмой пункт про ошибку модели, четыре пункта приёмки с обоснованием. Запись класса без указания подсчитанных операций не показывает хода рассуждения и не засчитывается.
ИИВ заданиях 1 и 3 закрыт. В задании 2 разрешён прямо. Я сам так делаю всегда, поэтому и вам запрещать не буду. Разница между «спросил» и «списал» — та самая строка «почему это верно» с числом.

Словарь занятия

O-нотация
Запись верхней границы роста: число операций растёт не быстрее указанной функции. O(n) — «порядка n», а не «ровно n операций».
T(n)
Временная сложность: как растёт число операций с ростом размера входа n.
S(n)
Пространственная сложность: сколько памяти решение занимает дополнительно к полученному входу.
Худший случай
Вход, на котором решение делает больше всего работы. Именно по нему называется оценка.
Средний случай
Оценка по всем возможным входам сразу. Для линейного поиска это n/2 — тот же класс O(n).
Логарифмическое время
O(log n): каждый шаг отбрасывает половину оставшихся данных. Тысяча элементов — десять шагов.
Скрытая стоимость
Операция без цикла в коде, внутри которой проход всё-таки есть: x in список — O(n), sorted() — O(n log n), срез — O(n), insert(0, x) — O(n).
Размен время/память
Ускорение решения за счёт дополнительной структуры: множество или словарь снимают перебор, но занимают память O(n).
Ориентир 10⁷
Около десяти миллионов простых операций в секунду. Не константа из стандарта, а способ за десять секунд прикинуть, укладывается решение в лимит или нет.

Материалы занятия — в course-materials/complexity/. Бланки — в starter-pack/lesson_02/. Вопросы по заданию — на паре или в чате группы.

ОП.04 · Основы алгоритмизации и программирования · 3 курс

Карточки: определите сложность

Занятие 3 — временная сложность и O-нотация

А/П

Фамилия, имя, группа: ______________________________________________

Для каждого фрагмента укажите четыре величины. T(n) — число операций формулой. O — класс по времени. S(n) — дополнительная память. Худший случай — вход, на котором работы больше всего. В записи O не пишутся постоянные множители и младшие слагаемые: правильно O(n), неправильно O(2n) и O(n + 5). Если величин на входе две и они растут независимо, в ответе остаются обе: O(n + m).

А1
def first_order(orders):
    if not orders:
        return None
    return orders[0]
А2
def count_late(orders):
    late = 0
    for order in orders:
        if order.delay > 0:
            late += 1
    return late
А3
def avg_and_max(temps):
    total = 0
    for t in temps:
        total += t
    best = temps[0]
    for t in temps:
        if t > best:
            best = t
    return total / len(temps), best
А4
def has_pair(nums, target):
    for i in range(len(nums)):
        for j in range(i + 1, len(nums)):
            if nums[i] + nums[j] == target:
                return True
    return False
А5
def unique_codes(codes):
    result = []
    for code in codes:
        if code not in result:
            result.append(code)
    return result
А6
def steps_to_one(number):
    steps = 0
    while number > 1:
        number //= 2
        steps += 1
    return steps
Б1
def top_three(prices):
    return sorted(prices)[:3]
Б2
def merge_reports(day, night):
    result = []
    for row in day:
        result.append(row)
    for row in night:
        result.append(row)
    return result
Б3
def count_words(words):
    counts = {}
    for word in words:
        counts[word] = counts.get(word, 0) + 1
    return counts
Б4
def fib(n):
    if n < 2:
        return n
    return fib(n - 1) + fib(n - 2)
Б5
def assign(orders, couriers):
    for order in orders:
        for courier in couriers:
            if fits(order, courier):
                mark(order, courier)
Б6
def find_code(codes, target):
    low, high = 0, len(codes) - 1
    while low <= high:
        mid = (low + high) // 2
        if codes[mid] == target:
            return mid
        if codes[mid] < target:
            low = mid + 1
        else:
            high = mid - 1
    return -1

Ответы

№T(n)OS(n)Худший случай
А1
А2
А3
А4
А5
А6
Б1
Б2
Б3
Б4
Б5
Б6

Материалы занятия — в course-materials/complexity/. Разбор карточек — в презентации занятия 3.