Если вы окунулись в мир компьютерных наук и программирования, возможно, вы сталкивались с термином «Большая нотация О». Эта концепция часто пугает новичков, но не бойтесь! В этой статье мы объясним нотацию Big O, почему она важна и как ее интерпретировать.

Что такое нотация большого O?

Нотация Big O — это способ анализа и описания эффективности алгоритмов. Это помогает нам понять, как производительность алгоритма меняется по мере увеличения размера входных данных.

Представьте, что вам нужно выполнить задачу, например, найти определенное число в массиве. Теперь предположим, что у вас есть разные алгоритмы для решения этой задачи. Нотация Big O позволяет нам сравнить эти алгоритмы и понять, как их производительность зависит от размера списка.

Рассмотрим два алгоритма поиска целевого числа в массиве из 12 элементов.

int[] array = { 1, 2, 3, 7, 9, 10, 15, 21, 24, 29, 30, 50 };
int target = 21;

Алгоритм 1
Он просматривает каждое число в массиве одно за другим, пока не найдет целевое число.

public static bool LinearSearch(int[] array, int target)
{
    for (int i = 0; i < array.Length; i++)
    {
        if (array[i] == target)
        {
            return true;
        }
    }

    return false;
}

Алгоритм 2
Он многократно делит массив на две половины, пока либо не найдет целевое число, либо не определит, что оно отсутствует.

public static bool BinarySearch(int[] array, int target)
{
    int left = 0;
    int right = array.Length - 1;

    while (left <= right)
    {
        int middle = left + (right - left) / 2;

        if (array[middle] == target)
        {
            return true;
        }

        if (array[middle] < target)
        {
            left = middle + 1;
        }
        else
        {
            right = middle - 1;
        }
    }

    return false;
}

Теперь давайте проанализируем производительность этих алгоритмов, используя нотацию Big O.

Алгоритм 1 — O(n) линейного времени

Это означает, что время выполнения Алгоритма 1 растет линейно с размером входных данных. Если размер массива удваивается, алгоритм выполняется примерно в два раза дольше. «n» представляет размер ввода.

В нашем примере выше, чтобы найти цель номер 21, потребовалось 8 итераций.

Алгоритм 2 — логарифмическое время O(log n)

Это указывает на то, что время выполнения алгоритма 2 логарифмически растет с размером входных данных. По мере увеличения массива время выполнения алгоритма увеличивается, но гораздо медленнее, чем линейный рост.

Важно отметить, что нотация Big O фокусируется на наихудшем сценарии, описывая верхнюю границу производительности алгоритма. Это помогает нам понять, как алгоритм масштабируется по мере увеличения размера входных данных, но может не давать точных сведений о фактическом времени выполнения.

Как правило, мы хотим выбирать алгоритмы с меньшей сложностью Big O (например, O (1), O (log n), O (n)), поскольку они более эффективны для больших входных данных.

В нашем примере выше, чтобы найти цель номер 21, потребовалось всего 4 итерации.

Вот другие часто используемые обозначения Big O

O(1) — постоянное время

Это означает, что время выполнения алгоритма или потребность в пространстве не зависят от размера входных данных. Он работает одинаково, независимо от размера входных данных.

Примеры включают доступ к элементу массива или выполнение основных арифметических операций.

public static void ConstantAlgorithm(int[] array)
{
    int firstElement = array[0];
}

O(n²) — квадратичное время

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

Вложенные циклы типичны для квадратичной временной сложности.

public static void QuadraticAlgorithm(int[] array)
{
    int lenght = array.Length;
    for (int i = 0; i < lenght; i++)
    {
        for (int j = 0; j < lenght; j++)
        {
            // Do some stuff
        }
    }
}

O(2^n) — экспоненциальное время

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

Он быстро становится неэффективным, и его следует избегать, если это возможно.

Пример включает рекурсивный Фибоначчи.

public static  int Fibonacci(int n)
{
    if (n <= 1)
    {
        return n;
    }

    return Fibonacci(n - 1) + Fibonacci(n - 2);
}

В приведенном выше коде функция Фибоначчи рекурсивно вызывает себя дважды с входными данными n-1 и n-2, пока не достигнет базовых случаев, когда n равно 0 или 1. Количество рекурсивных вызовов и операций растет экспоненциально по мере увеличения n, что приводит к к экспоненциальной временной сложности.

Интерпретация нотации Big O

При анализе алгоритмов мы ориентируемся на доминирующий термин или термин с наибольшей скоростью роста. Например, если алгоритм имеет временную сложность O(n² + n), мы игнорируем член n и классифицируем его как O(n²).

Последние мысли

Когда мы пишем код, мы хотим, чтобы он был быстрым и использовал как можно меньше ресурсов. Нотация Big O позволяет нам сравнивать различные алгоритмы и определять, какой из них более эффективен для решения конкретной задачи.

Спасибо за прочтение!

В своих статьях на Medium я делюсь мыслями о веб-разработке, советами по карьере и последними тенденциями в области технологий. Присоединяйтесь ко мне, когда мы вместе исследуем эти захватывающие темы. Давайте учиться, расти и творить вместе!

👏Похлопайте, чтобы статья распространилась

Ещё статьи на тему Программирование, карьера и технические тренды.

🔔Подписывайтесь на меня в Средних | Твиттер | ЛинкедИн