Структуры данных и алгоритмы Примечание

Прежде чем вы начнете безумно работать над LeetCode для кодирования интервью, понимание концепции Big O Notation определенно ускорит вашу подготовку и эффективность.

Это восемь общих временных сложностей с их популярными алгоритмами/примерами, которые будут подробно объяснены сегодня.

  1. O(1): доступ к одному элементу в массиве
  2. O(n): линейный поиск
  3. O (log n): двоичный поиск
  4. O (n log n): сортировка слиянием
  5. O(n²): сортировка выбором, сортировка вставками
  6. O(n³)
  7. O (2 ^ n): числа Фибоначчи
  8. O(n!)

В конце также познакомим с правилами вычисления Big O. Начнем!

O(1)

Его также часто называют «постоянным временем», включая такие действия, как прямое получение значения, например: a[0], или использование функции print.

arr1 = [1,2,3,4,5]
pos3 = arr1[2]    # O(1)
print(pos3)    # O(1)

O(n)

Также называемое «линейным временем», означает, что время работы увеличивается линейно с размером n. Например, проверять все элементы массива и каждый раз проверять одно значение.

Линейный поиск

def search(arr, target):
    for i in range(len(arr)):    # O(n)
        if arr[i] == target:
            return i
    return -1
    
    
    
arr = [1,2,3,4,5]
print(search(arr, 3))
>>>>>>>>>>>>>>>>>>>>>>>>
2

Примечание: у вас может возникнуть вопрос: что, если мы запустим search(arr, 1)? Станет ли временная сложность O(1)? Ответ отрицательный, потому что Big O всегда рассматривает наихудший сценарий функции как ее временную сложность.

О (журнал п)

def logn(n):
    count = 0
    while(n > 1):
        n = int(n/2)    # O(log n)
        count += 1
    print(count)
        
        
logn(8)
>>>>>>>>>>>>>>>>>
3

Временная сложность деления n на 2 до определенной ситуации составляет O(log n).

В этом коде нам нужно делить n на 2, пока n=1. Когда n = 8, мы делим 3 раза, чтобы получить 1, что можно рассматривать как log 8 = 3.

Репрезентативным алгоритмом с временной сложностью O(log n) является Двоичный поиск. Поскольку алгоритм делит массив на два подмассива поровну, а затем выбирает подмассивы, соответствующие условиям для следующего шага, продолжайте то же действие до тех пор, пока не будет найдено целевое значение. У меня есть дополнительное объяснение бинарного поиска, не стесняйтесь проверить его в Бинарном поиске — примечание о структурах данных и алгоритмах.

O (n журнал n)

Зная O(n) и O(log n), вам должно быть легко понять, почему временная сложность равна O(n log n) следующего кода:

def nlognfunc(n):
    y = n
    while(n > 1):
        n = int(n/2)    # O(log n)
        for i in range(1, y+1):     #O(n)
            print(i)
            
            
            
nlognfunc(3)
>>>>>>>>>>>>>>>>>>>>>>>>>
1
2
3
1
2
3

Временная сложность: O(n) * O(log n) = O(n log n)

Давайте воспользуемся сортировкой слиянием в качестве практического примера, чтобы объяснить O(n log n):

у нас есть две функции: merge_sort() разбивает массив на небольшой массив и сортирует значения, в то время как merge() в основном выполняет работу по слиянию, сравните значение двух массивы, а меньший поместите в result_array.

ans = merge_sort([1,6,7,3,8,99,403,5])
print(ans)
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
[1, 3, 5, 6, 7, 8, 99, 403]

Каждый раз, когда он вызывает merge_sort(), он будет вызывать merge(), поскольку он рекурсивный, временная сложность составляет O(log n) * O(n ) = O(n log n).

Стоит отметить, что причина, по которой merge()returnresult_arr + left_arr[left_index:] + right_arr[right_index:], заключается в том, что когда цикл while завершается, когда условие не выполняется, один из массивов (левый или правый) добавляется в список результатов, а другой список еще не закончен, может остаться одно или несколько значений, поэтому мы используем [index:], чтобы поместить все левые в result_arr, с другой стороны, [index:] полностью добавленного списка будет пустым списком, который не влияет на результат.

Давайте посмотрим на диаграмму примера merge() ниже:

Примечание. Сортировка слиянием также соответствует концепции алгоритма "разделяй и властвуй".

O(n²)

Обычно произносится как Большое О в квадрате. Вложенный цикл for, который включает один цикл for в цикл for, составляет O(n²). Популярными примерами этого являются сортировка выбором, сортировка вставками. О, худший случай сортировки пузырьком также O(n²)! Если вас интересует разница между этими тремя алгоритмами сортировки, не стесняйтесь переходить по ссылке, чтобы посетить статью: Сравнение сортировки выбором, сортировки вставками и пузырьковой сортировки.

n = 3
count = 0
for i in range(n):    # O(n)
    for j in range(n):    # O(n)
        count += 1
        print(count)
>>>>>>>>>>>>>>>>>>>>>>>>>>
1
2
3
4
5
6
7
8
9

Согласно приведенному выше коду временная сложность составляет O(n) * O(n) = O(n²). В этом случае функция выполняется 9 раз, что равно 3².

O (n³)

Он очень похож на O(n²), но O(n³)обычно представляет собой вложенный цикл for с тремя циклами for.

n = 2
count = 0
for i in range(n):    # O(n)
    for j in range(n):    # O(n)
        for k in range(n):    # O(n)
            count += 1
            print(count)
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
1
2
3
4
5
6
7
8

Временная сложность: O(n) * O(n) * O(n) = O(n³)

O(2^n)

Самый известный пример с временной сложностью O(2^n) — это числа Фибоначчи. Числа Фибоначчи представляют собой последовательность, в которой каждое число является суммой двух предыдущих: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34…

Но как реализовать эту математическую теорию в коде? Это просто, достаточно применить математические формулы для чисел Фибоначчи:

  • Fn = Fn-1 + Fn-2
  • F0 = 0 и F1 = 1

тогда это становится кодом, как показано ниже:

def fib(n):
    if n == 0:
        return 0
    if n == 1:
        return 1
    return fib(n-1)+fib(n-2)
    
    
print(fib(6))
print(fib(4))
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
8
3

И почему временная сложность O(2^n)?

Предположим, мы собираемся найти 4-е число в последовательности, то есть F(4). Начнем с F(4), по формуле Fn = Fn-1 + Fn-2 мы знаем, что следующим слоем будет F(3) + F(2) и так далее, пока не остановимся на F(1), F(0).

Из приведенных выше рисунков мы видим, что:

  • Чтобы найти 4-й номер последовательности, нам нужно ровно 4 слоя, а при расширении до числа n нам нужно всего лишь n слоев.
  • Каждый раз, когда мы переходим на следующий уровень, становится 2 раза вызова для каждого пункта.

Расположив два приведенных выше абзаца следующим образом, мы можем обнаружить, что итоговая временная сложность приблизительно равна O(2^n):

  • Каждый следующий уровень, количество шагов удваивается
  • Всего n слоев
  • Временная сложность: O(2^n)

O(n!)

Функция, которая выполняется факториально (n!) раз.

def func(n):
    if n == 0:
        print('*****')
        return
    
    for i in range(n):
        func(n-1)
    
func(3)
>>>>>>>>>>>>>>>>>>>>>>
*****
*****
*****
*****
*****
*****

Правила вычисления большого O

  • Всегда рассматривайте наихудший случай. Представьте себе, когда масштаб данных исчисляется миллиардами.
  • Удалить константы.
print('1')    # O(1)
for:
 for:         # O( n²)

O(1)+O( n²) = O(1+ n²)=O( n²)

  • Не обращайте внимания на меньшую.

O( n+n²) = O( n²)

  • Относитесь к вводу по-разному.
def func(a, b):
    for i in range(a):    # O(a)
    for i in range(b):    # O(b)

Временная сложность должна быть O(a+b) вместо O(2n).

def func(a, b):
    for i in range(a):    # O(a)
        for j in range(b):    # O(b)

Временная сложность должна быть O(a*b) вместо O(n²).

Ссылка

Курс по большой нотации O от freeCodeCamp

Поддержите меня, став участником Medium сегодня и получив доступ к неограниченному количеству статей.

Больше контента на plainenglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Получите эксклюзивный доступ к возможностям написания и советам в нашем сообществе Discord.