Оценка сложности алгоритмов

⏱ 35 мин · 🧩 заданий: 5

Зачем O-большое

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

O(f(n)) — верхняя оценка роста: «при больших n работа растёт не быстрее, чем f(n), с точностью до константы».

Правила:

  • константы отбрасываются: O(3n + 10) → O(n);
  • остаются старшие члены: O(n² + n) → O(n²);
  • разные входы — разные буквы: два списка длины n и m → O(n + m) или O(n·m).

Классы сложности

Сложность Название Пример n = 10⁶
O(1) константная доступ по индексу, dict[key] 1
O(log n) логарифмическая бинарный поиск 20
O(n) линейная один проход по списку 10⁶
O(n log n) линейно-логарифмическая сортировка 2·10⁷
O(n²) квадратичная два вложенных цикла 10¹² ✗
O(2ⁿ) экспоненциальная все подмножества ✗✗
O(n!) факториальная все перестановки ✗✗✗
import math

print(f"{'n':>8} {'log n':>7} {'n log n':>10} {'n²':>14} {'2ⁿ':>12}")
for n in [10, 100, 1_000, 10_000, 100_000]:
    two = f"{2**n:.1e}" if n <= 1000 else "∞"
    print(f"{n:>8} {math.log2(n):>7.1f} {n * math.log2(n):>10.0f} {n * n:>14} {two:>12}")

Как оценивать код

Последовательные блоки складываются

def f(nums):
    total = sum(nums)          # O(n)
    nums.sort()                # O(n log n)
    return nums[0], total      # O(1)
# итого O(n log n) — доминирует сортировка

Вложенные циклы перемножаются

def pairs(nums):
    result = []
    for i in range(len(nums)):          # n раз
        for j in range(i + 1, len(nums)):   # до n раз
            result.append((i, j))
    return result
# n·(n−1)/2 итераций → O(n²)

Скрытая сложность встроенных операций

for x in a:            # n раз
    if x in b:         # b — список: O(m) на каждую проверку!
        ...
# O(n·m), а с b = set(b) — O(n + m)

x in list, list.index, list.remove, list.insert(0, ...), срезы lst[1:], "".join внутри цикла, sorted внутри цикла — у всех линейная или большая цена.

Логарифм — когда задача делится пополам

def steps_halving(n):
    steps = 0
    while n > 1:
        n //= 2
        steps += 1
    return steps

for n in [8, 1_000, 1_000_000, 10**9]:
    print(n, "→", steps_halving(n), "шагов")

Рекурсия — дерево вызовов

calls = 0

def fib(n):
    global calls
    calls += 1
    return n if n < 2 else fib(n - 1) + fib(n - 2)

for n in [10, 15, 20, 25]:
    calls = 0
    fib(n)
    print(f"fib({n}): {calls} вызовов")      # растёт как ~1.6ⁿ → O(2ⁿ)

Каждый вызов порождает два — дерево глубины n, вызовов порядка 2ⁿ. С мемоизацией каждое значение считается один раз → O(n). Это и есть идея динамического программирования.

Амортизированная сложность

list.append иногда копирует весь массив (O(n)), но так редко, что в среднем на операцию — O(1). Так же устроены dict и set при росте.

Сложность по памяти

Считается дополнительная память, кроме входа:

def reverse_copy(nums):
    return nums[::-1]                   # O(n) памяти — новый список

def reverse_in_place(nums):
    i, j = 0, len(nums) - 1
    while i < j:                        # O(1) памяти — только два индекса
        nums[i], nums[j] = nums[j], nums[i]
        i, j = i + 1, j - 1

Рекурсия тратит память на стек: глубина d → O(d). В Python глубина ограничена (~1000, sys.getrecursionlimit()), поэтому глубокую рекурсию переписывают итеративно.

Ограничения → сложность

Примерно 10⁷–10⁸ простых операций в секунду на Python (реально — ближе к 10⁷). По ограничениям задачи можно угадать ожидаемое решение:

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

Как говорить о сложности на собеседовании

  1. Сначала предложите простое решение и назовите его сложность: «перебор пар — O(n²) по времени, O(1) по памяти».
  2. Найдите узкое место: «дорого искать дополнение — можно хранить увиденное в множестве».
  3. Улучшите и снова оцените: «O(n) по времени, O(n) по памяти».
  4. Обсудите компромисс время/память и крайние случаи.

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

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

Итог

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

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Заполнить таблицу

Какова временная сложность функции?

O(1)O(log n)O(n)O(n log n)O(n²)
return nums[0] + nums[-1]
for x in nums: total += x
for x in nums: / for y in nums: ...
while n > 1: n //= 2
nums.sort() затем for x in nums: ...
for x in nums: ... затем отдельно for y in nums: ...
for i in range(n): for j in range(i, n): ...
for i in range(n): for j in range(5): ...
Отметьте ячейку — «+», пустая — «−».
Задание 2 Программа на Python

Функция has_duplicates(nums) работает за O(n²). Перепишите её за O(n) по времени. Проверка запустит её на списке из 200 000 элементов.

Какие тесты проверяют код (3)
assert has_duplicates([1, 2, 3, 1]) and not has_duplicates([1, 2, 3]) and not has_duplicates([])
import time
nums = list(range(3_000))
start = time.perf_counter()
assert has_duplicates(nums) is False
assert time.perf_counter() - start < 0.05, "Слишком медленно — нужен O(n)"

import time
nums = list(range(200_000)) + [5]
start = time.perf_counter()
assert has_duplicates(nums) is True
assert time.perf_counter() - start < 1

Задание 3 Один вариант ответа

Сколько раз выполнится тело цикла?

i = 1
while i < n:
    i *= 2
Задание 4 Один вариант ответа

Какая сложность по памяти у рекурсивного def depth(n): return 0 if n == 0 else 1 + depth(n - 1)?

Задание 5 Один вариант ответа

В задаче n ≤ 10⁵. Какой сложности решения обычно достаточно, а какое не пройдёт?

Войдите, чтобы отмечать пройденные уроки.