Big O

Всё про нотацию Big O на примерах базовых коллекций в Python

/notes/big_o/images/feature.png

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

Примеры нотаций Big O в порядке убывания скорости выполнения соответствующих им алгоритмов:

  • O(1): Константная сложность. Читается как “сложность порядка 1”. Время выполнения алгоритма не зависит от размера входных данных, то есть если даже алгоритм выполняется постоянно в 3 шага, то он будет не O(3), а O(1). Является идеальной с точки зрения производительности, однако зачастую недостижима.
    Примеры:

    • Доступ к элементу в массиве по индексу;
    • Вставка или удаление элемента в конец списка (очереди) фиксированной длины.
  • O(log n): Логарифмическая сложность. Время выполнения алгоритма растет медленно с увеличением размера входных данных. Например, бинарный поиск в отсортированном массиве.
    Пример:

    • Бинарный поиск. В этом алгоритме на каждом шаге половина данных отсекается, и поиск продолжается в оставшейся половине. Это означает, что при увеличении размера входных данных вдвое, бинарный поиск требует всего одного дополнительного шага.
  • O(n): Линейная сложность. Читается как “сложность порядка n”. Время выполнения алгоритма пропорционально размеру входных данных.
    Пример:

    • Поиск максимального значения в списке:
    1
    2
    3
    4
    5
    6
    
    def find_max(my_list):
        max_num = my_list[0]
        for i in range(len(my_list)):
            if my_list[i] > max_num:
                max_num = my_list[i]
        return max_num
  • O(n log n): Линейно-логарифмическая сложность. Время выполнения алгоритма растет быстрее, чем линейно, но медленнее, чем квадратично.
    Пример:

    • Сортировка слиянием.
  • O(n^2): Квадратичная сложность.
    Примеры:

    • Сортировка пузырём;
    • Поиск суммы всех пар элементов списка:
    1
    2
    3
    4
    5
    6
    
    def pairs_sum(my_list):
        summ = 0
        for i in range(my_list):
            for j in range(my_list):
                summ += my_list[i] + my_list[j]
        return summ
  • O(n^3): Кубическая сложность.
    Пример:

    • Алгоритмы, имеющие три вложенных цикла.
  • O(2^n): Экспоненциальная сложность. Она обычно не является оптимальным решением из-за своей высокой вычислительной нагрузки при увеличении размера входных данных.
    Пример:

    • Алгоритмы, использующие рекурсию без оптимизации (рекурсивное вычисление чисел Фибоначчи).
  • O(n!): Факториальная сложность. Это самая высокая степень роста времени выполнения алгоритма. Этот тип сложности встречается, например, при переборе всех возможных комбинаций элементов, что делает его чрезвычайно неэффективным для больших значений n.
    Пример:

    • Перебор всех перестановок элементов массива.

График сложностей алгоритмов

https://habrastorage.org/r/w1560/getpro/habr/upload_files/973/455/cf3/973455cf3bb354f49113562838372900.jpg


Несущественное не учитывается

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

Например, при оценке 5n^2 + 3n + 2 можно откинуть 5, 3n, 2. В результате мы получим O(n^2).
Или при оценке n^2 + n + log(n) оставляем самую значимую часть (перемененную с наибольшей степенью) и получаем тот жеO(n^2).


Сложность операций в Python

Рассмотрим некоторые операции, которые часто используют для базовых структур в языке программирования Python. n — размер структуры данных

Списки:

ОперацияПримерСложностьПримечание
Получение элементаl[i]O(1)
Присваивание значения элементуl[i] = 0O(1)
Размер спискаlen(l)O(1)
Добавление элемента в конец спискаl.append(5)O(1)Скорее всего это неверно
Удаление последнего элементаl.pop()O(1)Аналогично l.pop(-1)
Очищение спискаl.clear()O(1)Аналогично l = []
Добавление нескольких элементовl1.extend(l2)O(len(n))Прямая зависимость с размером l2
Создание спискаlist(l)O(n)Прямая зависимость с разером l
Получение срезаl[a:b]O(n)O(a - b)
Сравнение списковl1 == l2O(n)
Вставка через срезl[a:b]O(n)
Проверка наличия элемента в спискеx is/not in lO(n)Тот же перебор элементов
Поверхностное копирование спискаl.copy()O(n)Аналогично l[:]
Удаление элемента через deldel l[i]O(n)Перебор до совпадения индексов,
а значит зависит от i
Удаление элемента через removel.remove(...)O(n)
Удаление элемента через popl.pop(i)O(n)O(n-i)
Получение крайнего значенияmin(l)/max(l)O(n)Снова перебор элементов
Получение обратного спискаl.reverse()O(n)
Переборfor i in l:O(n)
Сортировкаl.sort()O(n * log(n))Или sorted(l)
Умножение списка на константуk*lO(k*n)5*l => O(n), len(l)*l => O(n^2)

Кортежи:

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


Множества:

ОперацияПримерСложностьПримечание
Размер множестваlen(s)O(1)
Добавление элементаs.add(5)O(1)
Проверка наличия элемента в множествеx is/not in sO(1)
Удаления элемента через discards.discard(...)O(1)
Удаления элемента через removes.remove(...)O(1)
Удаления элемента через pops.pop()O(1)Удаляемый элемент выбирается случайно
Очищение множестваs.clear()O(1)Аналогично s = set()
Создание множестваset(...)O(n)
Сравнение множеств через ==, !=s != tO(n)
Сравнение множеств через <=, <s <= tO(n)Аналогично s.issubset(t)
Сравнение множеств через >=, >s >= tO(n)Аналогично s.issuperset(t)
Объединениеs | tO(n)Аналогично s.union(t)
Пересечениеs & tO(n)Аналогично s.intersection(t)
Разностьs - tO(n)Аналогично s.difference(t)
Симметрическая разностьs ^ tO(n)Аналогично s.symmetric_difference(t)
Переборfor i in s:O(n)
Копированиеs.copy()O(n)

Неизменяемые множества

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


Словари

ОперацияПримерСложностьПримечание
Получение элементаd[key]O(1)
Добавление элемента/
Изменение значения
d[key] = valO(1)
Размер словаряlen(d)O(1)
Удаление элемента через deldel d[key]O(1)
Удаление элемента через popitemd.popitem()O(1)Удаляемый элемент выбирается случайно
Удаление элемента через popd.pop(key)O(1)
get() и setdefault()d.get(key)O(1)
Очищение словаряd.clearO(1)Аналогично s = {} или s = dict()
Получение списка ключей/значенийd.keys()O(1)
Создание словаряdict(...)O(n)
Перебор элементовfor key in d:O(n)Для .keys(), .values(), .items()

Прочие обозначения

Кроме обозначения “Big O”, существуют другие обозначения для оценки сложности алгоритмов:

  • Big Theta (Θ): Big Theta также оценивает верхнюю и нижнюю границы временной сложности алгоритма, но описывает точную сложность, а не только наихудший случай, как Big O. Θ(f(n)) обозначает, что время выполнения алгоритма ограничено функцией f(n) как сверху, так и снизу.
  • Big Omega (Ω): Big Omega оценивает нижнюю границу временной сложности алгоритма. Ω(f(n)) говорит о том, что алгоритм выполнится не быстрее, чем функция f(n).
  • Little O (o): Little O представляет собой верхнюю границу, которая строже, чем Big O. Если f(n) является o(g(n)), это означает, что время выполнения алгоритма ограничивается функцией g(n), но алгоритм работает быстрее, чем g(n).
  • Little Omega (ω): Little Omega представляет собой нижнюю границу, которая строже, чем Big Omega. Если f(n) является ω(g(n)), это означает, что алгоритм работает медленнее, чем g(n), но не медленнее, чем f(n).
Поддержать автора
NoisyCake cloudtipscloudtips
0%