← Назад к вопросам

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

2.2 Middle🔥 131 комментариев
#Язык C++

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI30 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

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

Краткий ответ

Нотация Big O (O-нотация) — это математический способ описания того, как растёт время выполнения или потребление памяти алгоритма при увеличении размера входных данных. Она показывает верхнюю границу сложности алгоритма.

Базовая идея

О-нотация игнорирует константы и сосредоточивается только на главном факторе роста:

Линейный поиск в массиве из 1000 элементов:
Время выполнения примерно 1000 операций (100 операций за проверку)

О-нотация: O(n), где n = размер входа (1000)

Почему O(n), а не O(100n)?
Потому что константа 100 не важна для общей тенденции.
Если взять 2000 элементов, потребуется ~2000 операций.
Время растёт пропорционально n.

Основные нотации

1. O(1) — Константная сложность

int getFirst(const std::vector<int>& v) {
    return v[0];  // Одна операция, независимо от размера v
}

int getElementByKey(const std::map<int, int>& m, int key) {
    return m[key];  // O(log n) для карты, но часто пишут как O(1) для хеша
}

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

2. O(log n) — Логарифмическая сложность

int binarySearch(const std::vector<int>& sorted_v, int target) {
    int left = 0, right = sorted_v.size() - 1;
    
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (sorted_v[mid] == target) return mid;
        if (sorted_v[mid] < target) left = mid + 1;
        else right = mid - 1;
    }
    return -1;  // Не найдено
}
// O(log n), потому что каждый раз размер уменьшается в 2 раза

Пример: бинарный поиск, поиск в сбалансированном дереве.

Для массива из 1000 элементов нужно примерно 10 итераций.

3. O(n) — Линейная сложность

int linearSearch(const std::vector<int>& v, int target) {
    for (int i = 0; i < v.size(); ++i) {
        if (v[i] == target) return i;
    }
    return -1;
}
// O(n), потому что в худшем случае проверяем все элементы

Пример: простой поиск, подсчёт суммы элементов, перебор всех элементов.

4. O(n log n) — Линеарифмическая сложность

void mergeSort(std::vector<int>& v) {
    // Разделяй и властвуй: O(log n) слоёв
    // На каждом слое O(n) работы
    // Всего: O(n log n)
}

Пример: эффективные сортировки (merge sort, quick sort в среднем случае).

5. O(n²) — Квадратичная сложность

void bubbleSort(std::vector<int>& v) {
    for (int i = 0; i < v.size(); ++i) {           // O(n)
        for (int j = i + 1; j < v.size(); ++j) {   // O(n)
            if (v[i] > v[j]) std::swap(v[i], v[j]);
        }
    }
}  // Вложенные циклы: O(n²)

Пример: простые сортировки (bubble sort, selection sort, insertion sort), вложенные циклы по всему массиву.

6. O(2ⁿ) — Экспоненциальная сложность

int fibonacci_naive(int n) {
    if (n <= 1) return n;
    return fibonacci_naive(n-1) + fibonacci_naive(n-2);
    // Для каждого вызова создаются 2 новых вызова
    // Всего примерно 2ⁿ вызовов
}

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

7. O(n!) — Факториальная сложность

void generatePermutations(std::vector<int>& v) {
    // Перебор всех перестановок
    // Для n элементов: n! перестановок
}

Пример: перебор всех перестановок массива, некоторые переборные задачи.

Сравнение скоростей

Для n = 1000:

O(1)        →  1 операция
O(log n)    →  10 операций
O(n)        →  1,000 операций
O(n log n)  →  10,000 операций
O(n²)       →  1,000,000 операций
O(2ⁿ)       →  Больше чем атомов во вселенной!
O(n!)       →  Больше чем 2ⁿ в миллионы раз!

Заметьте, как быстро становится неприемлемо с экспоненциальной сложностью.

Как анализировать сложность

1. Простые операции — O(1):

x = 5;          // O(1)
x = y + z;      // O(1)
v[0] = 10;      // O(1)

2. Циклы — умножение:

for (int i = 0; i < n; ++i) {
    x = i * 2;  // O(1)
}
// Цикл повторяется n раз: O(n)

3. Вложенные циклы — умножение:

for (int i = 0; i < n; ++i) {           // n раз
    for (int j = 0; j < m; ++j) {       // m раз
        // O(1) операция
    }
}
// Всего: O(n * m) или O(n²) если m = n

4. Последовательные части — сложение:

// Часть 1: O(n)
for (int i = 0; i < n; ++i) {
    // ...
}

// Часть 2: O(n²)
for (int i = 0; i < n; ++i) {
    for (int j = 0; j < n; ++j) {
        // ...
    }
}

// Части 3: O(n log n)
std::sort(v.begin(), v.end());

// Общая сложность: O(n) + O(n²) + O(n log n) = O(n²)
// Берём наибольшую!

5. Рекурсия — сложно:

void foo(int n) {
    if (n <= 1) return;  // O(1)
    bar();               // O(n)
    foo(n/2);            // Вызываем сами себя
}
// Глубина рекурсии: O(log n)
// Работа на каждом уровне: O(n)
// Всего: O(n log n)

Виды анализа сложности

1. Лучший случай (Best Case)

int linearSearch(const std::vector<int>& v, int target) {
    for (int i = 0; i < v.size(); ++i) {
        if (v[i] == target) return i;  // Может вернуться сразу!
    }
    return -1;
}
// Лучший случай: O(1) — элемент в начале

2. Средний случай (Average Case)

// В среднем элемент где-то в середине
// O(n/2) = O(n)

3. Худший случай (Worst Case) — обычно это то, о чём говорят

// Элемент в конце или отсутствует
// O(n)

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

std::vector операции:

std::vector<int> v = {1, 2, 3, 4, 5};

v[0];                    // O(1) доступ
v.push_back(6);          // O(1) амортизированное
v.insert(v.begin(), 0);  // O(n) нужно сдвинуть все
v.erase(v.begin());      // O(n) нужно сдвинуть все
std::find(v.begin(), v.end(), 3);  // O(n) линейный поиск

std::map операции:

std::map<int, int> m;

m[5] = 10;               // O(log n) вставка/обновление
m.count(5);              // O(log n) поиск
m.erase(5);              // O(log n) удаление
std::find(m.begin(), m.end(), 5);  // O(n) линейный поиск по элементам!

Сортировка:

std::vector<int> v;

std::sort(v.begin(), v.end());       // O(n log n) быстрая сортировка
std::stable_sort(v.begin(), v.end()); // O(n log n) стабильная
std::partial_sort(v.begin(), v.begin() + 10, v.end());  // O(n log k)

Правило большого O

1. Игнорируем константы:

O(2n) = O(n)
O(n/2) = O(n)
O(100n) = O(n)

2. Игнорируем младшие члены:

O(n² + n) = O(n²)
O(n log n + n) = O(n log n)
O(n + 1000) = O(n)

3. Берём наибольший:

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

Заключение

Big O нотация — это инструмент для:

  • Сравнения эффективности алгоритмов
  • Предсказания поведения при большом размере входа
  • Выбора правильного алгоритма для задачи

Ключевые сложности в порядке улучшения:

O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ) < O(n!)

Чем меньше Big O, тем быстрее алгоритм для больших входов!

Что такое нотация «O» большое? | PrepBro