Что такое алгоритмическая сложность (Big O)?

Ответ

Алгоритмическая сложность (Big O нотация) — это способ описать, как время выполнения алгоритма или объем используемой памяти растут с увеличением размера входных данных (n). Она оценивает наихудший или типичный сценарий, отбрасывая константы и менее значимые части.

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

Основные классы сложности (от лучшего к худшему):

Нотация Название Пример в Python Описание роста
O(1) Константная my_dict[key] Время не зависит от n
O(log n) Логарифмическая Бинарный поиск Растет очень медленно
O(n) Линейная for item in list: Пропорционально n
O(n log n) Линейно-логарифмическая Эффективные сортировки (Timsort) Хорошо для сортировки
O(n²) Квадратичная Вложенные циклы Медленно для больших n
O(2ⁿ) Экспоненциальная Рекурсивный Фибоначчи (наивный) Крайне медленно

Практические примеры на Python:

# O(1) - Константное время (доступ по индексу в списке, ключу в словаре)
def get_first_element(items):
    return items[0]  # Всегда одна операция

# O(n) - Линейное время (проход по списку)
def find_item(items, target):
    for item in items:  # В худшем случае n итераций
        if item == target:
            return True
    return False

# O(n²) - Квадратичное время (пузырьковая сортировка, вложенные циклы)
def find_pairs(items):
    pairs = []
    for i in range(len(items)):          # n раз
        for j in range(i + 1, len(items)): # ~n/2 раз в среднем
            pairs.append((items[i], items[j])) # Итог ~ n²
    return pairs

# O(log n) - Логарифмическое время (бинарный поиск)
def binary_search(sorted_list, target):
    low, high = 0, len(sorted_list) - 1
    while low <= high:
        mid = (low + high) // 2
        if sorted_list[mid] == target:
            return mid
        elif sorted_list[mid] < target:
            low = mid + 1  # Отбрасываем половину массива
        else:
            high = mid - 1 # Отбрасываем половину массива
    return -1
# Каждый шаг уменьшает область поиска вдвое. Для n=1_000_000 нужно ~20 шагов.

Вывод для разработчика: При выборе между list и set для проверки наличия элемента (if x in collection), важно помнить, что операция in для списка имеет сложность O(n), а для множества (set) — O(1) в среднем случае. Для больших данных это решающее отличие.

Ответ 18+ 🔞

Слушай, давай я тебе на пальцах объясню, что это за зверь такой — алгоритмическая сложность. Представь, что тебе надо найти одну иголку в стоге сена. Если стог размером с кошачий лоток — это одно дело, а если с пятиэтажный дом — совсем другое, да? Вот эта нотация Big O как раз и описывает, насколько хуёвее становится задача, когда размер данных (наш n) растёт до овердохуища.

Зачем это питонисту? А затем, чувак, чтобы не облажаться на ровном месте. Написал ты красивый код, а он на тысяче записей работает, а на миллионе — просто накрылся медным тазом и завис. Потому что внутри у тебя, например, вложенный цикл, который жрёт ресурсы как не в себя.

Основные градации от быстрого к пиздецу какому-то:

Нотация Что означает Пример из жизни Как растёт
O(1) Константная. Пофиг на размер. Взять книгу с полки по номеру. Не растёт вообще. Идеал.
O(log n) Логарифмическая. Очень медленный рост. Искать слово в словаре, откидывая половину страниц. Для миллиона элементов нужно шагов 20. Волшебство.
O(n) Линейная. Прямая зависимость. Проверить всех людей в очереди, не тот ли это Петя. В 10 раз больше данных — в 10 раз дольше работа.
O(n log n) Линейно-логарифмическая. Для умных сортировок. Разложить колоду карт по мастям и достоинствам эффективно. Быстро, но чуть хуже, чем линейно.
O(n²) Квадратичная. Пиздец начинается. Сравнить каждого человека в толпе с каждым другим для знакомства. В 10 раз больше данных — в 100 раз дольше. Ужас.
O(2ⁿ) Экспоненциальная. Апокалипсис. Перебрать все возможные пароли из N символов. Добавил один символ — время работы упёрлось в потолок.

Смотри, как это выглядит в коде, чтобы не быть распиздяем:

# O(1) - Ёб твою мать, мечта! Взял и готово.
def взять_первый(список):
    return список[0]  # Всегда одна операция, хоть список из триллиона элементов

# O(n) - Нормально так. Прошёлся по всем.
def найти_дурака(список_имен, имя_искомое):
    for имя in список_имен:  # В худшем случае пройдёшь всех
        if имя == имя_искомое:
            return True
    return False

# O(n²) - Вот тут, бля, уже подозрение ебать чувствую. Вложенные циклы.
def найти_все_пары(люди):
    пары = []
    for i in range(len(люди)):          # n раз
        for j in range(i + 1, len(люди)): # и ещё примерно n раз
            пары.append((люди[i], люди[j])) # Итого нихуя себе операций: ~ n²
    return пары

# O(log n) - Хитрая жопа, но эффективная. Бинарный поиск.
def бинарный_поиск(отсортированный_список, цель):
    low, high = 0, len(отсортированный_список) - 1
    while low <= high:
        mid = (low + high) // 2  # Берём середину
        if отсортированный_список[mid] == цель:
            return mid  # Ура, нашли!
        elif отсортированный_список[mid] < цель:
            low = mid + 1   # Отбрасываем левую половину — там нихуя нет
        else:
            high = mid - 1  # Отбрасываем правую половину
    return -1
# Суть в том, что каждый шаг ковыряет не всё, а только половинку. Для миллиона элементов — 20 итераций максимум. Красота!

Главный вывод, который бьёт по мозгам: Когда тебе надо просто проверить, есть ли элемент в коллекции (if x in my_collection), не будь лохом. Список (list) будет тупо перебирать всё до конца — это O(n), пиздец как медленно для больших данных. А множество (set) или словарь (dict) сделают это в среднем за O(1), почти мгновенно. Выбор структуры данных — это, блядь, половина успеха. Не проёбывайся на этом.