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

Определение

Линейно-логарифмическая сложность возникает когда алгоритм комбинирует перебор всех элементов (n) с логарифмическим разделением задачи (log n).

Формула

Характеристики

  • Оптимальная сложность для сортировки сравнениями
  • Медленнее чем O(n), но быстрее чем O(n²)
  • Типична для алгоритмов “разделяй и властвуй”
  • Хорошо масштабируется

Визуализация Merge Sort

Исходный массив: [38, 27, 43, 3]

Разделение:
            [38, 27, 43, 3]        <- уровень 0
           /              \
      [38, 27]          [43, 3]     <- уровень 1
       /    \            /    \
     [38]  [27]        [43]   [3]    <- уровень 2

Слияние:
     [38]  [27]        [43]   [3]    <- уровень 2
       \    /            \    /
      [27, 38]          [3, 43]     <- уровень 1
           \              /
            [3, 27, 38, 43]         <- уровень 0

Количество уровней: log₂(4) = 2
Операций на каждом уровне: n = 4
Итого: n × log n = 4 × 2 = 8 операций

Дерево разделения для n=8

Уровень 0:  [8 элементов]                          <- 8 операций
                  /            \
Уровень 1:     [4]            [4]                   <- 8 операций
              /   \          /   \
Уровень 2:  [2]   [2]      [2]   [2]                <- 8 операций
            / \   / \      / \   / \
Уровень 3: [1][1][1][1]  [1][1][1][1]               <- 8 операций

Высота дерева: log₂(8) = 3
Работа на каждом уровне: n = 8
Итого: 3 × 8 = 24 операции = 8 × log₂(8)

Сравнение производительности

Размер (n)nn log₂ n
101033100
10010066410,000
1,0001,0009,9661,000,000
10,00010,000132,877100,000,000

График сложности

Операции
      |
100K  |                                    ●  O(n²)
      |                                  ●
 75K  |                                ●
      |                              ●
 50K  |                            ●
      |                          ●
 25K  |                      ● ●
      |                 ● ●         O(n log n)
 10K  |          ● ● ●
      |    ● ● ●  O(n)
   0  +----------------------------------------------->
        10   100    1K     10K    100K           n

Почему O(n log n) оптимальна для сортировки?

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

Интуиция:

  • Нужно сделать log n уровней разделения
  • На каждом уровне обработать все n элементов
  • Итого: n × log n операций

Практическое применение

  • Эффективные алгоритмы сортировки
  • Построение индексов в базах данных
  • Операции с приоритетными очередями
  • Некоторые задачи на графах
  • Обработка больших объемов данных

Алгоритмы с O(n log n)

АлгоритмЛучший случайСредний случайХудший случай
Merge SortO(n log n)O(n log n)O(n log n)
Quick SortO(n log n)O(n log n)O(n²)
Heap SortO(n log n)O(n log n)O(n log n)
Tim SortO(n)O(n log n)O(n log n)

Важно помнить

✅ Оптимально для сортировки сравнениями ✅ Хорошо масштабируется на больших данных ✅ Стабильная производительность ❌ Медленнее линейных алгоритмов ❌ Требует дополнительной памяти (Merge Sort)

Примеры кода

1. Сортировка слиянием (Merge Sort)

function mergeSort(arr) {
  // Базовый случай
  if (arr.length <= 1) {
    return arr;
  }
 
  // Разделяем массив пополам
  const mid = Math.floor(arr.length / 2);
  const left = arr.slice(0, mid);
  const right = arr.slice(mid);
 
  // Рекурсивно сортируем половины
  return merge(mergeSort(left), mergeSort(right));
}
 
function merge(left, right) {
  const result = [];
  let i = 0, j = 0;
 
  // Сливаем два отсортированных массива
  while (i < left.length && j < right.length) {
    if (left[i] < right[j]) {
      result.push(left[i++]);
    } else {
      result.push(right[j++]);
    }
  }
 
  // Добавляем оставшиеся элементы
  return result.concat(left.slice(i)).concat(right.slice(j));
}
 
const arr = [38, 27, 43, 3, 9, 82, 10];
console.log(mergeSort(arr)); // [3, 9, 10, 27, 38, 43, 82]

2. Быстрая сортировка (Quick Sort)

function quickSort(arr) {
  if (arr.length <= 1) {
    return arr;
  }
 
  const pivot = arr[Math.floor(arr.length / 2)];
  const left = arr.filter(x => x < pivot);
  const middle = arr.filter(x => x === pivot);
  const right = arr.filter(x => x > pivot);
 
  return [...quickSort(left), ...middle, ...quickSort(right)];
}
 
const arr = [10, 7, 8, 9, 1, 5];
console.log(quickSort(arr)); // [1, 5, 7, 8, 9, 10]

3. Пирамидальная сортировка (Heap Sort)

function heapSort(arr) {
  const n = arr.length;
 
  // Построение кучи
  for (let i = Math.floor(n / 2) - 1; i >= 0; i--) {
    heapify(arr, n, i);
  }
 
  // Извлечение элементов из кучи
  for (let i = n - 1; i > 0; i--) {
    [arr[0], arr[i]] = [arr[i], arr[0]];
    heapify(arr, i, 0);
  }
 
  return arr;
}
 
function heapify(arr, n, i) {
  let largest = i;
  const left = 2 * i + 1;
  const right = 2 * i + 2;
 
  if (left < n && arr[left] > arr[largest]) {
    largest = left;
  }
 
  if (right < n && arr[right] > arr[largest]) {
    largest = right;
  }
 
  if (largest !== i) {
    [arr[i], arr[largest]] = [arr[largest], arr[i]];
    heapify(arr, n, largest);
  }
}

4. Сортировка + проход

function findDuplicatesEfficient(arr) {
  // Сортировка - O(n log n)
  const sorted = [...arr].sort((a, b) => a - b);
 
  // Один проход - O(n)
  const duplicates = [];
  for (let i = 1; i < sorted.length; i++) {
    if (sorted[i] === sorted[i - 1] && 
        !duplicates.includes(sorted[i])) {
      duplicates.push(sorted[i]);
    }
  }
 
  // Итого: O(n log n) + O(n) = O(n log n)
  return duplicates;
}