Базово про O(n*log(n))

В этом уроке ты
  • Вспомнишь, что такое логарифм.
  • Узнаешь, в каких случаях возникает сложность O(n*log(n)).
  • Научишься оценивать сортировки по времени и памяти.
Что такое log(n)

Логарифм log_b(N) отвечает на вопрос: «Сколько раз нужно разделить N на b, чтобы в результате получить 1?». b - это основание логарифма.

Для log_2(N): если N = 8 , то деления 8 → 4 → 2 → 1 занимают 3 шага (log_2(8)=3).

Обычно в записи O(log(n)) не указывают основание логарифма, потому что при асимптотической оценке важна только сама форма зависимости, а основание — константа.
Место n*log(n) в оценке сложности

n*log(n) называют линейно-логарифмической сложностью. По скорости работы она расположена между O(n) (линейной) и O(n*n) (квадратичной). Чтобы понять, как сильно n*log(n) отличается от n, взгляни на таблицу:

n Число операций в O(n*log(n)) Число операций в O(n)
16 = 2^4 16 * 4 = 64 16
1024 = 2^10 1024 * 10 = 10240 1024
65536 = 2^16 65536 * 16 = 1048576 65536
4294967296 = 2^32 4294967296 * 32 = 137438953472 4294967296
Хотя n*log(n) растёт быстрее, чем n, на практике такая сложность всё равно считается достаточно быстрой и ближе к O(n), чем к O(n*n).
Где встречается оценка O(n*log(n))

Чаще всего такая оценка возникает в алгоритмах сортировки:

using System;
using System.Collections.Generic;

public class Solution {
    static Solution() {
        List<int> nums = new List<int> {1, 5, 2, 3, 5, 1, 2, 3};
        // Сортировка за O(n*log(n))
        nums.Sort();
        // [1, 1, 2, 2, 3, 3, 5, 5]
        Console.WriteLine(string.Join(" ", nums));
    }
}

Также сложность O(n*log(n)) встречается и в других алгоритмах, но в этом курсе мы их рассматривать не будем, так как они редко встречаются на практике.

Сортировки

Сортировка — это упорядочивание набора данных (чисел, слов или других объектов) по определённому правилу (например, по возрастанию или убыванию).

Пример: если мы отсортируем массив [4,2,5,6,1] по возрастанию, получим [1,2,4,5,6].

Время и память сортировок

Существует множество алгоритмов сортировки, и каждый из них лучше работает для тех или иных данных. Чаще всего сортировки имеют оценку по времени O(n*log(n)), а по памяти — O(1), O(log(n)) или O(n).

Ниже примеры популярных сортировок, которые встроены в разные языки программирования:

Алгоритм Худший случай Средний случай Лучший случай Память
Introsort O(n*log(n)) Θ(n*log(n)) O(n*log(n)) O(log(n))
Timsort O(n*log(n)) Θ(n*log(n)) O(n) O(n)
Quick Sort O(n*n) Θ(n*log(n)) O(n*log(n)) O(log n) в среднем, O(n) в худшем
Heap Sort O(n*log(n)) Θ(n*log(n)) O(n*log(n)) O(1)

Такие оценки по времени и памяти связаны с внутренним устройством каждого алгоритма. Мы не будем подробно разбирать их в этом курсе. Важно лишь помнить, что большинство эффективных сортировок имеют время работы порядка O(n*log(n)).

Дополнительное задание: узнай, какая сортировка используется в языке программирования, которым ты пользуешься, и выясни, какова её оценка по времени и памяти.
Главные выводы
  1. Чаще всего сортировка оценивается по времени как O(n*log(n)), а по памятиO(n), O(log(n)) или O(1) в зависимости от конкретного алгоритма.
  2. Оценка O(n*log(n)) чаще всего возникает при использовании сортировок.
  3. Логарифм log_b(N) отвечает на вопрос: “Сколько раз нужно разделить N на b, чтобы получить 1?”