0Pricing
DSA Interview Prep · Урок

Нотация Big-O с нуля

Поймите, зачем изучать асимптотический рост, как отбрасывать константы и члены низших порядков и как сразу читать Big-O

«Нотация Big-O с нуля» — бесплатный урок DSA Interview Prep на CoddyKit. Это урок 1 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения DSA Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс DSA Interview Prep содержит 4 уроков всего.

Зачем измерять эффективность алгоритма

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

# O(n) approach
def find_max_linear(nums):
    m = nums[0]
    for n in nums:
        if n > m: m = n
    return m

# O(n^2) approach (unnecessary double loop)
def find_max_quadratic(nums):
    for i in range(len(nums)):
        is_max = all(nums[i] >= nums[j] for j in range(len(nums)))
        if is_max: return nums[i]

print(find_max_linear([3, 1, 4, 1, 5, 9]))  # 9

Big-O: асимптотическая верхняя граница

Big-O описывает верхнюю границу роста стоимости вычислений в худшем случае. Главное правило: отбросьте константы и члены меньшего порядка, поскольку при больших размерах задачи значение имеет только доминирующий член. См. код.

# T(n) = 3n^2 + 5n + 100 is O(n^2)
# because the n^2 term dominates for large n

# T(n) = 2n + 1000 is O(n)
# the constant 1000 becomes negligible

# Rule: drop constants and lower-order terms
# 5n^3 + 2n^2 + n + 1  =>  O(n^3)
# 100 * log(n) + n      =>  O(n)
print('O(n^2) example: counting iterations')
n = 1000
count = sum(1 for i in range(n) for j in range(n))
print(count)  # 1_000_000 = n^2

Распространённые классы сложности

От самого быстрого к самому медленному: O(1), O(log n), O(n), O(n log n), O(n^2), O(2^n), O(n!). Знание этих классов позволяет выбрать правильный подход ещё до написания первой строки.

import math

n = 1000
print(f'O(1):       {1}')
print(f'O(log n):   {int(math.log2(n))}')
print(f'O(n):       {n}')
print(f'O(n log n): {int(n * math.log2(n))}')
print(f'O(n^2):     {n**2}')
# O(2^n) for n=1000 is astronomically large
# O(n!) even larger

Отбрасывание констант: почему это важно

Выполнение 5n шагов или 2n шагов — это в обоих случаях O(n): константы зависят от оборудования, а не от алгоритма. O-большое отбрасывает их, чтобы сравнивать масштабирование на равных условиях.

# Both are O(n) — different constants
def count_a(n):
    total = 0
    for i in range(n):   # n ops
        total += 1
    for i in range(n):   # n ops
        total += 1
    return total  # T(n) = 2n  =>  O(n)

def count_b(n):
    total = 0
    for i in range(5 * n):  # 5n ops
        total += 1
    return total  # T(n) = 5n  =>  O(n)

print(count_a(10), count_b(10))  # 20 50

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

O-большое описывает худший случай; Омега — лучший случай, а Тета — точную асимптотическую границу для обоих. Когда на собеседовании спрашивают «сложность», почти всегда имеют в виду худший случай.

def linear_search(nums, target):
    for i, n in enumerate(nums):
        if n == target:
            return i  # best case: target at index 0 => O(1)
    return -1         # worst case: not found => O(n)

# Best case O(1): target is first element
print(linear_search([5,1,2,3], 5))   # 0

# Worst case O(n): target not in list
print(linear_search([1,2,3,4], 9))   # -1

O(log n): сокращение пространства поиска вдвое

Алгоритм имеет сложность O(log n), если на каждом шаге сокращает входные данные вдвое, как двоичный поиск. Даже для миллиарда элементов это всего около 30 шагов — невероятно быстро. Посмотрите код.

def binary_search(arr, target):
    lo, hi = 0, len(arr) - 1
    steps = 0
    while lo <= hi:
        steps += 1
        mid = (lo + hi) // 2
        if arr[mid] == target:
            return mid, steps
        elif arr[mid] < target:
            lo = mid + 1
        else:
            hi = mid - 1
    return -1, steps

import math
arr = list(range(1000))
idx, s = binary_search(arr, 999)
print(f'Found at {idx} in {s} steps (log2(1000)~={math.log2(1000):.1f})')

O(n log n): нижняя граница сортировки

Любая сортировка сравнениями в худшем случае требует как минимум O(n log n) — это настоящая математическая нижняя граница. Поэтому сортировка с последующим просмотром в целом имеет сложность O(n log n), а не O(n^2). В коде показана сортировка слиянием.

# Merge sort: O(n log n)
def merge_sort(arr):
    if len(arr) <= 1:
        return arr
    mid = len(arr) // 2
    left  = merge_sort(arr[:mid])
    right = merge_sort(arr[mid:])
    return merge(left, right)

def merge(a, b):
    res, i, j = [], 0, 0
    while i < len(a) and j < len(b):
        if a[i] <= b[j]: res.append(a[i]); i+=1
        else:             res.append(b[j]); j+=1
    return res + a[i:] + b[j:]

print(merge_sort([5,2,8,1,9,3]))  # [1,2,3,5,8,9]

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

Амортизированный анализ усредняет стоимость множества операций. Операция append в Python имеет амортизированную сложность O(1): обычно она выполняется мгновенно, а редкое расширение за O(n) равномерно распределяется между всеми операциями append.

# Dynamic array append is O(1) amortised
import sys

lst = []
capacities = []
for i in range(16):
    lst.append(i)
    capacities.append(sys.getsizeof(lst))

# Size jumps show reallocation events
for i, c in enumerate(capacities):
    if i > 0 and capacities[i] != capacities[i-1]:
        print(f'Realloc at i={i}, new size={c} bytes')

Распознавание сложности в коде

Быстрое правило: считайте циклы. Один цикл — O(n), два вложенных — O(n^2), цикл с делением пополам — O(log n). Независимые проходы выполняют операцию add; только вложенные циклы перемножаются. Посмотрите код.

# Two independent passes: O(n) + O(n) = O(n)
def two_passes(nums):
    total = sum(nums)           # O(n)
    mean = total / len(nums)
    diffs = [abs(n - mean) for n in nums]  # O(n)
    return max(diffs)           # O(n)
# Overall: O(n) -- NOT O(n^2)

# Nested loops: O(n) * O(n) = O(n^2)
def all_pairs(nums):
    pairs = []
    for i in range(len(nums)):       # O(n)
        for j in range(i+1, len(nums)): # O(n)
            pairs.append((nums[i], nums[j]))
    return pairs  # O(n^2)

Основы пространственной сложности

Пространственная сложность учитывает дополнительную память, используемую сверх входных данных. Разворот на месте имеет сложность O(1), а хеш-таблица — O(n). Если Вы обмениваете время на память, всегда указывайте оба показателя.

# O(1) space: reverse in-place
def reverse_inplace(arr):
    l, r = 0, len(arr) - 1
    while l < r:
        arr[l], arr[r] = arr[r], arr[l]
        l += 1; r -= 1

# O(n) space: create reversed copy
def reverse_copy(arr):
    return arr[::-1]

a = [1, 2, 3, 4, 5]
reverse_inplace(a)
print(a)  # [5, 4, 3, 2, 1]

Обсуждение сложности на собеседованиях

Всегда сами называйте сложность, не дожидаясь вопроса: «Время — O(n log n), память — O(n)». Затем предложите более быстрый вариант. Такая привычка показывает настоящий профессиональный уровень.

# Example of explaining complexity step by step
def two_sum(nums, target):
    # O(n) time: one pass through nums
    # O(n) space: hash map stores up to n elements
    seen = {}  # value -> index
    for i, n in enumerate(nums):
        complement = target - n
        if complement in seen:   # O(1) lookup
            return [seen[complement], i]
        seen[n] = i
    return []

print(two_sum([2, 7, 11, 15], 9))  # [0, 1]

Быстрая проверка

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

Итоги урока

Итоги: O-большое описывает рост в худшем случае с отброшенными константами; Вы знаете классы от O(1) до O(n!), а независимые циклы складываются, тогда как вложенные перемножаются.

Часто задаваемые вопросы

Урок «Нотация Big-O с нуля» бесплатный?

Да — полный текст урока «Нотация Big-O с нуля» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс DSA Interview Prep, подпишись на CoddyKit PRO. Курс DSA Interview Prep содержит 4 уроков всего.

Чему я научусь в уроке «Нотация Big-O с нуля»?

Поймите, зачем изучать асимптотический рост, как отбрасывать константы и члены низших порядков и как сразу читать Big-O Ты практикуешь DSA Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.

Нужен ли мне опыт, чтобы начать DSA Interview Prep?

Предыдущий опыт не требуется. DSA Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 1 из 4.

Сколько времени занимает урок «Нотация Big-O с нуля»?

Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.

Можно ли писать и запускать код в этом уроке DSA Interview Prep?

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

Все уроки этого курса

  1. Нотация Big-O с нуля
  2. Анализ циклов и вложенных циклов
  3. Рекурсия и метод дерева рекурсии
  4. Пространственная сложность и компромиссы
← Назад к DSA Interview Prep