Сортування — найбільш досліджувана задача в комп'ютерних науках.
Ефективне сортування лежить в основі баз даних, файлових систем,
пошукових систем, алгоритмів стиснення та динамічного програмування.
Розуміння, коли застосовувати сортування вставками O(n²) проти
сортування злиттям O(n log n) проти сортування підрахунком O(n) — і що
означають стабільність та in-place — необхідне для написання
продуктивного коду.
Сортування порівнянням визначає порядок виключно через порівняння
елементів (a < b?). Аргумент дерева рішень: • n! можливих
упорядкувань n елементів • Двійкове дерево рішень має висоту ≥
⌈log₂(n!)⌉ • За Стірлінгом: log₂(n!) ≈ n log₂ n - n log₂ e ≈ Θ(n log n)
Отже: БУДЬ-ЯКИЙ алгоритм сортування на основі порівнянь потребує
щонайменше Ω(n log n) порівнянь у найгіршому випадку. Сортування
злиттям і пірамідальне сортування досягають O(n log n) у найгіршому
випадку. Швидке сортування — O(n log n) у середньому, O(n²) у
найгіршому випадку. Означення: стабільне сортування зберігає відносний
порядок рівних елементів. Приклад: сортування за прізвищем має
залишити John Smith перед Jane Smith, якщо вони вже були в такому
порядку.
2. Прості сортування O(n²)
Сортування вставками
Найкраще підходить для: майже відсортованих даних,
малих масивів (n < 50), потокових даних (сортування в міру
надходження елементів). O(n) у найкращому випадку на відсортованому
вході.
functioninsertionSort(arr) {
for (let i = 1; i < arr.length; i++) {
const key = arr[i];
let j = i -1;
while (j >= 0&& arr[j] > key) {
arr[j +1] = arr[j];
j--;
}
arr[j +1] = key;
}
return arr;
}
Бульбашкове сортування
O(n²) у найгіршому/середньому випадку, O(n) у найкращому. Просте, але
рідко використовується в продакшені — сортування вставками має кращі
сталі множники для малих n.
Сортування вибором
Знаходить мінімальний елемент і ставить його на початок. Завжди O(n²).
Примітне мінімізацією перестановок (лише n перестановок
загалом) — корисне, коли записи дорогі (наприклад, флеш-пам'ять).
3. Сортування злиттям
Поділяй і володарюй: розбити навпіл, рекурсивно відсортувати кожну
половину, злити дві відсортовані половини.
Рекурентне співвідношення: T(n) = 2T(n/2) + O(n) За основною теоремою
(a=2, b=2, f(n)=n, n^log_b(a) = n^1): Випадок 2: T(n) = O(n log n)
Властивості: • O(n log n) гарантовано — найкращий, середній, найгірший
• Стабільне (рівні елементи зберігають відносний порядок) • НЕ in-place:
потребує O(n) допоміжної пам'яті для буфера злиття • Дружні до кешу
послідовні шаблони доступу Операція злиття: function merge(arr, left,
mid, right): copy arr[left..mid] to L[], arr[mid+1..right] to R[] i=0,
j=0, k=left while i < L.length and j < R.length: if L[i] <= R[j]:
arr[k++] = L[i++] else: arr[k++] = R[j++] copy remaining elements
Висхідне сортування злиттям уникає накладних витрат на
рекурсію, зливаючи серії розміром 1, 2, 4, 8, ... — використовується в
Arrays.sort у Java та Timsort у Python (який використовує
наявні відсортовані серії для O(n) на майже відсортованому вході).
4. Швидке сортування
Ідея: обрати опорний елемент (pivot); розбити масив так, щоб усі менші
елементи були ліворуч, а всі більші — праворуч; рекурсивно обробити
кожну сторону. T(n) = T(k) + T(n-1-k) + O(n) де k = ранг опорного
елемента Найкращий/середній випадок (опорний близький до медіани, k ≈
n/2): T(n) = 2T(n/2) + O(n) = O(n log n) Найгірший випадок (опорний
завжди мінімум або максимум, напр. відсортований вхід, поганий опорний):
T(n) = T(n-1) + O(n) = O(n²) Стратегії пом'якшення: • Медіана трьох:
порівняти перший, середній, останній елемент; узяти медіану як опорний
• Випадковий опорний: випадкова перестановка перед сортуванням або
випадковий вибір опорного → очікувано O(n log n) незалежно від входу •
Introsort (C++ std::sort): швидке сортування + перехід до
пірамідального, якщо глибина рекурсії перевищує 2 log n → гарантовано
O(n log n) у найгіршому випадку Розбиття (схема Ломуто): pivot =
arr[right] i = left - 1 for j = left to right-1: if arr[j] <= pivot:
i++, swap(arr[i], arr[j]) swap(arr[i+1], arr[right]) return i+1 Переваги
швидкого сортування: • O(1) додаткової пам'яті (in-place) • Чудова
поведінка з кешем (послідовний доступ) • Мала стала: ~у 2× швидше за
сортування злиттям на практиці для випадкових даних
5. Пірамідальне сортування
Використовує двійкову max-купу (батько ≥ нащадки). У масиві батько i =
⌊(i-1)/2⌋, нащадки: 2i+1, 2i+2. Алгоритм: 1. Побудувати max-купу з
масиву: O(n) (за допомогою buildHeap Флойда, а не n вставок) Почати від
останнього не-листка ⌊n/2⌋-1 до 0, просіюючи вниз кожен вузол. 2.
Багаторазово видобувати максимум (поміняти корінь з останнім,
зменшити купу, просіяти вниз): O(n log n) Операція просіювання вниз:
O(log n) — порівняти з нащадками, поміняти, якщо менший Властивості: •
O(n log n) у найгіршому випадку — гарантовано, на відміну від швидкого
сортування • In-place — O(1) пам'яті • НЕ стабільне (далекосяжні
перестановки порушують відносний порядок) • Погана поведінка з кешем
(шаблон випадкового доступу по масиву) • На практиці у 2–5× повільніше
за швидке сортування через промахи кешу Доведення O(n) для побудови
купи: ∑_{k=0}^{⌊log n⌋} ⌈n/2^(k+1)⌉ × k ≤ n ∑_{k=0}^∞ k/2^k = 2n = O(n)
6. Сортування за лінійний час
Уникаючи поелементних порівнянь і використовуючи структуру даних,
деякі алгоритми сортують за час O(n) — перемагаючи нижню межу
порівнянь Ω(n log n):
Сортування підрахунком — O(n + k)
Для цілих чисел у діапазоні [0, k]. Підрахувати входження, потім
обчислити префіксні суми, щоб визначити фінальні позиції. O(n + k) за
часом і пам'яттю. Стабільне. Практичне, коли k = O(n).
Порозрядне сортування — O(d·(n + k))
Сортувати спершу за найменш значущою цифрою (LSD-порозрядне
сортування), використовуючи стабільне сортування (підрахунком) на
кожній з d позицій цифр. Цифри в основі b: d = ⌈log_b(макс. значення)⌉.
Обрати b = n для O(n) загалом з b-цифровим поданням. Використовується в
сортуванні на GPU, розподілених системах.
Блочне сортування — O(n) у середньому
Для рівномірно розподілених дійсних чисел у [0,1). Створити n блоків, що
покривають [0,1); розкидати кожен елемент у його блок; відсортувати
кожен блок сортуванням вставками; об'єднати. Очікувано O(n), якщо вхід
рівномірний. Найгірший випадок O(n²), якщо всі елементи потрапляють в
один блок.
7. Порівняльна таблиця
Алгоритм Найкращий Середній Найгірший Пам'ять Стабільне In-place
─────────────────────────────────────────────────────────────────────
Бульбашкове O(n) O(n²) O(n²) O(1) Так Так Вибором O(n²) O(n²) O(n²) O(1)
Ні Так Вставками O(n) O(n²) O(n²) O(1) Так Так Злиттям O(n lg n) O(n lg
n) O(n lg n) O(n) Так Ні Швидке O(n lg n) O(n lg n) O(n²) O(lg n) Ні
Так Пірамідальне O(n lg n) O(n lg n) O(n lg n) O(1) Ні Так Timsort O(n)
O(n lg n) O(n lg n) O(n) Так Ні Підрахунком O(n+k) O(n+k) O(n+k) O(k)
Так Ні Порозрядне (LSD) O(dn) O(dn) O(dn) O(n+k) Так Ні Блочне O(n) O(n)
O(n²) O(n) Так Ні Коли що використовувати: n < 50: сортування
вставками (просте, швидке для малих n) Загальне призначення: Timsort
(Python, Java) / Introsort (C++) Потрібна стабільність: сортування
злиттям / Timsort Обмежена пам'ять: пірамідальне сортування / злиття
in-place Цілочисельні ключі: порозрядне / підрахунком Майже
відсортоване: сортування вставками / Timsort