C++

Алгоритми сортування та аналіз складності

Big O нотація та аналіз складності алгоритмів. Детальний розбір сортувань: бульбашкою, вибіркою, вставками та швидкого сортування. Покрокові трасування, порівняння та практика.

Навіщо вчити сортування?

Сортування — одна з найбільш фундаментальних і досліджуваних задач в інформатиці. Не тому що воно рідко зустрічається в реальному житті (навпаки — воно всюди: ранжування у пошуку, стрічки соціальних мереж, ціни в інтернет-магазині), а тому що пошук ефективного способу впорядкувати дані навчив людство думати про складність алгоритмів.

Кожен алгоритм сортування — це маленький філософський трактат: «Яку ціну ми платимо за порядок?» Різні алгоритми відповідають на це питання по-різному. Один витрачає менше порівнянь, інший — менше переміщень, третій краще працює на майже-відсортованих даних. Щоб порівнювати algorithms чесно, нам потрібна єдина мова вимірювання — і ця мова називається Big O.

Big O нотація: мова складності

Проблема вимірювання

Як виміряти швидкість алгоритму? Перше, що спадає на думку — засікти час виконання на комп'ютері. Але це поганий підхід: час залежить від процесора, оперативної пам'яті, поточного навантаження системи, компілятора, оптимізацій. Один і той самий алгоритм на старому ноутбуці займе 5 секунд, на сервері — 0.1 секунди. Як же порівнювати?

Відповідь: вимірювати не час, а кількість операцій — і залежність цієї кількості від розміру вхідних даних.

Big O нотація (нотація «О велике») — математична мова для опису цієї залежності. Вона показує, як зростає час (або пам'ять) алгоритму при збільшенні розміру вхідних даних n — у найгіршому випадку.

Big O описує порядок зростання, відкидаючи константи та менш значущі доданки. Нас цікавить не точна кількість операцій, а те як вона змінюється зі зростанням n. 100n і n — обидва O(n), бо при пересуванні від тис. до млн. елементів і той, і інший збільшаться у 1000 разів.

Основні класи складності

Розглянемо основні класи Big O від найшвидшого до найповільнішого:

Loading diagram...
graph LR
    A["O(1)\nКонстантна"] --> B["O(log n)\nЛогарифмічна"]
    B --> C["O(n)\nЛінійна"]
    C --> D["O(n log n)\nЛінійно-логарифмічна"]
    D --> E["O(n²)\nКвадратична"]
    E --> F["O(2ⁿ)\nЕкспоненційна"]

    style A fill:#22c55e,stroke:#16a34a,color:#ffffff
    style B fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style C fill:#f59e0b,stroke:#b45309,color:#ffffff
    style D fill:#f97316,stroke:#ea580c,color:#ffffff
    style E fill:#ef4444,stroke:#dc2626,color:#ffffff
    style F fill:#7c3aed,stroke:#6d28d9,color:#ffffff

O(1) — Константна складність. Кількість операцій не залежить від розміру вхідних даних. Незалежно від того, скільки елементів у масиві — дія завжди займає однаковий час.

int firstElement = arr[0];  // Завжди одна операція

O(log n) — Логарифмічна складність. З кожною операцією задача вдвічі зменшується. Бінарний пошук у відсортованому масиві: шукаємо серед 1 000 000 елементів — потрібно не більше 20 порівнянь (бо 2²⁰ ≈ 1 000 000).

O(n) — Лінійна складність. Кількість операцій прямо пропорційна розміру вхідних даних. Пошук у невідсортованому масиві: в найгіршому випадку потрібно перевірити кожен елемент.

for (int i = 0; i < n; i++) { /* одна операція */ }  // O(n)

O(n log n) — Лінійно-логарифмічна складність. «Золотий стандарт» порівняльного сортування. Швидке сортування та сортування злиттям. Значно ефективніший за O(n²) на великих даних.

O(n²) — Квадратична складність. Два вкладені цикли на одних і тих самих даних. Прийнятна на малих масивах (до кількох тисяч елементів), але катастрофічна на великих: мільйон елементів → 10¹² операцій.

for (int i = 0; i < n; i++)
    for (int j = 0; j < n; j++) { /* одна операція */ }  // O(n²)

Наочне порівняння: що відбувається при n = 1 000 000?

Big OКількість операційЧас (≈1 млрд ops/сек)
O(1)1миттєво
O(log n)≈ 20миттєво
O(n)1 000 000≈ 1 мс
O(n log n)≈ 20 000 000≈ 20 мс
O(n²)1 000 000 000 000≈ 17 хвилин
O(2ⁿ)10³⁰⁰ ⁰⁰⁰довше, ніж існування Всесвіту

Ця таблиця пояснює, чому вибір алгоритму критично важливий. Різниця між O(n log n) та O(n²) — це різниця між «20 мілісекунд» та «17 хвилин» на одному мільйоні елементів.

Best, Average, Worst case

Big O описує лише найгірший (worst case) сценарій. Але алгоритми мають ще два «обличчя»:

🟢 Best Case (Ω)

Найкращий сценарій: вхідні дані у найзручнішій формі. Наприклад, масив вже відсортований.

🟡 Average Case (Θ)

Середній сценарій: дані у «типовому» стані. Найбільш реалістична оцінка для практики.

🔴 Worst Case (O)

Найгірший сценарій: дані у найнезручнішій формі. Гарантія верхньої межі часу.

Для сортування бульбашкою:

  • Best case (вже відсортований масив): O(n) — одна перевірка, без обмінів
  • Average case: O(n²)
  • Worst case (масив відсортований у зворотному порядку): O(n²)

Сортування бульбашкою (Bubble Sort)

Ідея алгоритму

Назва — від аналогії з бульбашками повітря у воді: що легша (менша) бульбашка — то швидше вона «спливає» вгору. В масиві «важкі» (великі) елементи «тонуть» вниз (до кінця), а «легкі» (малі) — «спливають» вгору (до початку).

Принцип: порівнюємо два сусідніх елементи. Якщо лівий більший за правий — міняємо їх місцями. Повторюємо для всіх пар. За один прохід (pass) найбільший елемент гарантовано опиняється на своєму місці — в кінці масиву. Потім повторюємо для решти (без останнього елемента, він вже на місці).

Покрокове трасування

Масив: {5, 3, 8, 1, 2}

Прохід 1 (порівнюємо пари, найбільший — на кінець):

КрокДіяМасив
Початок{5, 3, 8, 1, 2}
0 vs 1: 5 > 3 → обмінswap(5,3){3, 5, 8, 1, 2}
1 vs 2: 5 < 8 → без обміну{3, 5, 8, 1, 2}
2 vs 3: 8 > 1 → обмінswap(8,1){3, 5, 1, 8, 2}
3 vs 4: 8 > 2 → обмінswap(8,2){3, 5, 1, 2, **8**}

8 зайняла своє місце ✅

Прохід 2 (без останнього):

КрокДіяМасив
0 vs 1: 3 < 5 → без обміну{3, 5, 1, 2, 8}
1 vs 2: 5 > 1 → обмінswap(5,1){3, 1, 5, 2, 8}
2 vs 3: 5 > 2 → обмінswap(5,2){3, 1, 2, **5**, 8}

5 на місці ✅

Прохід 3:

КрокДіяМасив
0 vs 1: 3 > 1 → обмінswap(3,1){1, 3, 2, 5, 8}
1 vs 2: 3 > 2 → обмінswap(3,2){1, 2, **3**, 5, 8}

3 на місці ✅

Прохід 4:

КрокДіяМасив
0 vs 1: 1 < 2 → без обміну{**1**, **2**, 3, 5, 8}

Масив відсортований ✅ {1, 2, 3, 5, 8}

Реалізація

BubbleSort.cpp
#include <iostream>

using namespace std;

int main()
{
    const int SIZE = 5;
    int arr[SIZE] = {5, 3, 8, 1, 2};

    // Зовнішній цикл: N-1 проходів
    for (int pass = 0; pass < SIZE - 1; pass++)
    {
        bool swapped = false;  // Прапор для оптимізації

        // Внутрішній цикл: -pass, бо хвіст вже відсортований
        for (int i = 0; i < SIZE - 1 - pass; i++)
        {
            if (arr[i] > arr[i + 1])
            {
                int temp = arr[i];
                arr[i] = arr[i + 1];
                arr[i + 1] = temp;
                swapped = true;
            }
        }

        // Якщо жодного обміну — масив вже відсортований!
        if (!swapped)
        {
            break;
        }
    }

    for (int i = 0; i < SIZE; i++)
    {
        cout << arr[i] << " ";
    }
    cout << "\n";

    return 0;
}

Зверніть на оптимізацію з прапором swapped (рядки 13, 24, 28–31). Якщо за весь прохід жодного обміну не відбулося — масив вже відсортований, і ми виходимо достроково. Завдяки цьому best case стає O(n) замість O(n²): для вже відсортованого масиву потрібен лише один перевірочний прохід.

Аналіз складності

Складність
Best caseO(n) — один прохід без обмінів
Average caseO(n²)
Worst caseO(n²) — зворотньо відсортований масив
Пам'ятьO(1) — сортування на місці

Коли використовувати: майже ніколи в продакшн-коді. Bubble sort — навчальний алгоритм. Єдиний реальний сценарій: перевірити, чи масив вже відсортований (з оптимізацією прапором).


Сортування вибіркою (Selection Sort)

Ідея алгоритму

Уявіть, що перед вами лежить купа іграшок різного розміру. Ви хочете розкласти їх від найменшої до найбільшої. Природний підхід: знаходите найменшу — кладете на перше місце. Потім знаходите найменшу з тих, що залишились — на друге місце. І так далі.

Принцип: ділимо масив на дві частини — «відсортовану» (ліворуч) та «невідсортовану» (праворуч). На кожному кроці знаходимо мінімальний елемент у невідсортованій частині та ставимо його на відповідне місце, міняючи з першим невідсортованим елементом.

Ключова відмінність від bubble sort: мінімум порядків обміну. В bubble sort обміни відбуваються часто. В selection sort — рівно N-1 обмін за весь алгоритм. Для ситуацій, де запис у пам'ять дорогий — це перевага.

Покрокове трасування

Масив: {5, 3, 8, 1, 2}

Крок 0: Шукаємо min у {5, 3, 8, 1, 2}
        min = 1 (індекс 3)
        Міняємо arr[0] і arr[3]:
        {1, 3, 8, 5, 2}
        ↑ відсортована частина

Крок 1: Шукаємо min у {3, 8, 5, 2}
        min = 2 (індекс 4)
        Міняємо arr[1] і arr[4]:
        {1, 2, 8, 5, 3}
        ↑↑ відсортована частина

Крок 2: Шукаємо min у {8, 5, 3}
        min = 3 (індекс 4)
        Міняємо arr[2] і arr[4]:
        {1, 2, 3, 5, 8}
        ↑↑↑ відсортована частина

Крок 3: Шукаємо min у {5, 8}
        min = 5 (індекс 3) — вже на місці, але все одно "обмінюємо"
        {1, 2, 3, 5, 8} ✅

Всього обмінів: 4 (рівно N-1). Для порівняння — bubble sort на тому самому масиві зробив би значно більше.

Реалізація

SelectionSort.cpp
#include <iostream>

using namespace std;

int main()
{
    const int SIZE = 5;
    int arr[SIZE] = {5, 3, 8, 1, 2};

    for (int step = 0; step < SIZE - 1; step++)
    {
        // Знаходимо індекс мінімального елемента
        // у невідсортованій частині [step .. SIZE-1]
        int minIndex = step;

        for (int i = step + 1; i < SIZE; i++)
        {
            if (arr[i] < arr[minIndex])
            {
                minIndex = i;
            }
        }

        // Якщо мінімум — не вже поточний елемент, міняємо місцями
        if (minIndex != step)
        {
            int temp = arr[step];
            arr[step] = arr[minIndex];
            arr[minIndex] = temp;
        }
    }

    for (int i = 0; i < SIZE; i++)
    {
        cout << arr[i] << " ";
    }
    cout << "\n";

    return 0;
}

Деталі реалізації:

  • Рядок 14: minIndex = step — спочатку вважаємо, що мінімум — перший невідсортований елемент.
  • Рядок 16: Внутрішній цикл починається з step + 1 — елементи до step вже на своїх місцях.
  • Рядки 22–28: Перевірка if (minIndex != step) перед обміном — якщо мінімум вже на правильному місці, обмін не потрібен.

Аналіз складності

Складність
Best caseO(n²) — пошук мінімуму відбувається завжди
Average caseO(n²)
Worst caseO(n²)
Кількість обмінівO(n) — завжди не більше N-1
Пам'ятьO(1) — сортування на місці

Важлива особливість: на відміну від bubble sort, selection sort не виграє від вже відсортованих даних — він все одно виконує всі порівняння. Але через мінімальну кількість обмінів він може бути швидшим за bubble sort на масивах, де операція письма у пам'ять повільніша за читання.


Сортування вставками (Insertion Sort)

Ідея алгоритму

Уявіть, як ви тримаєте карти в руці під час карткової гри. Отримавши нову карту, ви вставляєте її на правильне місце серед вже відсортованих карт: знаходите позицію, де нова карта «менша за наступну» та «більша за попередню» — і вставляєте туди.

Принцип: поділяємо масив на «вже відсортовану» ліву частину та «ще не опрацьовану» праву. Беремо перший невідсортований елемент — поточний (key). Зсуваємо всі елементи відсортованої частини, що більші за нього, на одну позицію вправо — «звільняємо місце». Вставляємо поточний елемент на правильну позицію.

Insertion sort особливо ефективний на майже відсортованих даних: якщо масив майже правильний, елементи вставляються практично без зсувів.

Покрокове трасування

Масив: {5, 3, 8, 1, 2}

Початок: [5] | {3, 8, 1, 2}
         ↑ відсортована частина (з одного елемента)

Крок 1: key = 3 (arr[1])
        Відсортована: {5}
        5 > 3 → зсуваємо 5 вправо: {_, 5, 8, 1, 2}
        Вставляємо key=3 на позицію 0: {3, 5} | {8, 1, 2}

Крок 2: key = 8 (arr[2])
        Відсортована: {3, 5}
        5 < 8 → зупиняємось одразу
        Вставляємо key=8 на позицію 2: {3, 5, 8} | {1, 2}

Крок 3: key = 1 (arr[3])
        Відсортована: {3, 5, 8}
        8 > 1 → зсуваємо 8: {3, 5, _, 8, 2}
        5 > 1 → зсуваємо 5: {3, _, 5, 8, 2}
        3 > 1 → зсуваємо 3: {_, 3, 5, 8, 2}
        Початок масиву → вставляємо key=1: {1, 3, 5, 8} | {2}

Крок 4: key = 2 (arr[4])
        Відсортована: {1, 3, 5, 8}
        8 > 2 → зсуваємо: {1, 3, 5, _, 8}
        5 > 2 → зсуваємо: {1, 3, _, 5, 8}
        3 > 2 → зсуваємо: {1, _, 3, 5, 8}
        1 < 2 → зупиняємось
        Вставляємо key=2 на позицію 1: {1, 2, 3, 5, 8} ✅

Реалізація

InsertionSort.cpp
#include <iostream>

using namespace std;

int main()
{
    const int SIZE = 5;
    int arr[SIZE] = {5, 3, 8, 1, 2};

    for (int step = 1; step < SIZE; step++)
    {
        int key = arr[step];  // Поточний елемент для вставки
        int j = step - 1;     // Останній елемент відсортованої частини

        // Зсуваємо вправо елементи, що більші за key
        while (j >= 0 && arr[j] > key)
        {
            arr[j + 1] = arr[j];  // Зсуваємо на одну позицію вправо
            j--;
        }

        // j+1 — правильна позиція для key
        arr[j + 1] = key;
    }

    for (int i = 0; i < SIZE; i++)
    {
        cout << arr[i] << " ";
    }
    cout << "\n";

    return 0;
}

Ключові рядки:

  • Рядок 10: Зовнішній цикл починається з 1 (не з 0): масив з одного елемента вже «відсортований».
  • Рядок 12: key = arr[step] — зберігаємо копію поточного елемента, бо він буде перезаписаний при зсувах.
  • Рядок 16: while (j >= 0 && arr[j] > key) — умови зупинки: або вийшли за початок масиву, або знайшли елемент менший за key.
  • Рядок 18: arr[j + 1] = arr[j] — зсув без тимчасової змінної! Ми просто котимо елементи вправо, ніби котимо кулю по похилій поверхні.
  • Рядок 22: arr[j + 1] = keyj після циклу вказує на перший елемент, що менший або рівний key. Тому key іде на позицію j+1.

Аналіз складності

Складність
Best caseO(n) — масив вже відсортований, while не виконується
Average caseO(n²)
Worst caseO(n²) — зворотньо відсортований масив
Кількість обмінівO(n²) у найгіршому, O(n) у найкращому
Пам'ятьO(1) — сортування на місці

Де insertion sort виграє: на малих масивах (до 15–30 елементів) та на майже відсортованих даних insertion sort часто швидший за quicksort — через менші константні накладні витрати. Тому багато реальних бібліотечних реалізацій quicksort перемикаються на insertion sort для малих підмасивів.


Швидке сортування (Quick Sort)

Ідея алгоритму: «розділяй і володарюй»

Quicksort — один з найефективніших алгоритмів сортування загального призначення та центральний приклад стратегії «розділяй і володарюй» (divide and conquer). Ідея: замість того, щоб вирішувати одну велику задачу, ми розбиваємо її на менші підзадачі аж до тривіального випадку.

Принцип:

  1. Обираємо опорний елемент (pivot) — будь-який елемент масиву.
  2. Розбиваємо (partition) масив на дві частини: ліворуч — всі елементи, що менші або рівні pivot, праворуч — всі, що більші.
  3. Рекурсивно сортуємо ліву та праву частини.
  4. Базовий випадок рекурсії: підмасив з 0 або 1 елемента вже «відсортований».
Рекурсія (recursion) — виклик функцією самої себе. Детально цю концепцію ми розберемо в окремому розділі. Для розуміння quicksort достатньо інтуїтивного уявлення: функція sort(arr) розбиває масив на дві частини та викликає sort(ліва_частина) і sort(права_частина).

Операція partition: серце алгоритму

Partition — найважливіша та найхитріша частина quicksort. Розберемо варіант Lomuto partition scheme — простіший для розуміння:

Масив: {3, 6, 8, 10, 1, 2, 1}
Pivot: arr[right] = 1 (останній елемент)

i = -1  (« межа меншого» — inicially перед масивом)

j = 0: arr[0]=3. 3 > 1 → нічого
j = 1: arr[1]=6. 6 > 1 → нічого
j = 2: arr[2]=8. 8 > 1 → нічого
j = 3: arr[3]=10. 10 > 1 → нічого
j = 4: arr[4]=1. 1 <= 1 → i++, swap(arr[0], arr[4])
        {1, 6, 8, 10, 3, 2, 1}, i=0
j = 5: arr[5]=2. 2 > 1 → нічого
j = 6: (pivot, не входить у цикл)

Фінальний swap(arr[i+1], arr[right]): swap(arr[1], arr[6])
{1, 1, 8, 10, 3, 2, 6}
       ↑ pivot на своєму місці (індекс 1)

Після partition: всі елементи ліворуч від pivot ≤ pivot, праворуч — > pivot. Pivot на фінальній позиції.

Покрокове трасування quicksort

Масив: {5, 3, 8, 1, 2}, pivot = останній елемент

quickSort({5, 3, 8, 1, 2})
  pivot = 2
  partition: {1, 2, 8, 3, 5}
              ↑  ↑
         ліва  pivot  права
              |
  Ліва: {1} → вже відсортовано (1 елемент)
  Права: {8, 3, 5}
    quickSort({8, 3, 5})
      pivot = 5
      partition: {3, 5, 8}
                     ↑
                   pivot
      Ліва: {3} → відсортовано
      Права: {8} → відсортовано

Фінальний масив: {1, 2, 3, 5, 8} ✅

Реалізація

QuickSort.cpp
#include <iostream>

using namespace std;

// Допоміжна функція: міняє два елементи місцями
void swap(int& a, int& b)
{
    int temp = a;
    a = b;
    b = temp;
}

// Partition: розташовує pivot на правильній позиції,
// ліворуч — менші, праворуч — більші.
// Повертає індекс pivot.
int partition(int arr[], int low, int high)
{
    int pivot = arr[high];  // Обираємо останній елемент як pivot
    int i = low - 1;        // i — індекс останнього "малого" елемента

    for (int j = low; j < high; j++)
    {
        if (arr[j] <= pivot)
        {
            i++;
            swap(arr[i], arr[j]);
        }
    }

    // Ставимо pivot на своє місце
    swap(arr[i + 1], arr[high]);

    return i + 1;  // Повертаємо індекс pivot
}

// Рекурсивне сортування підмасиву arr[low..high]
void quickSort(int arr[], int low, int high)
{
    if (low < high)  // Базовий випадок: 0 або 1 елемент — вже відсортовано
    {
        int pivotIndex = partition(arr, low, high);

        // Рекурсивно сортуємо ліву та праву частини
        quickSort(arr, low, pivotIndex - 1);
        quickSort(arr, pivotIndex + 1, high);
    }
}

int main()
{
    const int SIZE = 7;
    int arr[SIZE] = {10, 7, 8, 9, 1, 5, 3};

    quickSort(arr, 0, SIZE - 1);

    for (int i = 0; i < SIZE; i++)
    {
        cout << arr[i] << " ";
    }
    cout << "\n";

    return 0;
}

Розберемо архітектуру:

  • swap (рядки 6–11): Допоміжна функція з параметрами-посиланнями (int& a). Посилання дозволяє змінити оригінальні значення, а не копії. Детально посилання розберемо у окремому розділі.
  • partition (рядки 16–32): Схема Lomuto. Змінна i — межа «малих» елементів. Якщо елемент менший за pivot (arr[j] <= pivot), збільшуємо i та ставимо його у зону малих.
  • quickSort (рядки 35–45): Рекурсивна функція. Умова if (low < high) — базовий випадок: підмасив із 0 або 1 елемента рекурсії не потребує.
  • Рядок 51: quickSort(arr, 0, SIZE - 1) — запуск зі всім масивом.

Вибір pivot та його вплив

Вибір pivot може кардинально вплинути на ефективність:

Аналіз складності

Складність
Best caseO(n log n) — pivot завжди ділить масив навпіл
Average caseO(n log n) — у середньому розбивка досить рівна
Worst caseO(n²) — pivot завжди найменший або найбільший
Пам'ятьO(log n) — стек рекурсивних викликів

Чому quicksort часто найшвидший на практиці (незважаючи на O(n²) у найгіршому випадку)?

  1. Кеш-дружній: quicksort працює з підмасивами послідовно, що відповідає принципу локальності у кеші процесора.
  2. Малі константи: порівняно з merge sort, quicksort не потребує додаткової пам'яті для злиття.
  3. Рідкісний worst case: при рандомізованому pivot або медіані трьох — O(n²) вкрай малоймовірний.

Порівняння алгоритмів

Зведена таблиця

АлгоритмBestAverageWorstПам'ятьСтабільний?
Bubble SortO(n)O(n²)O(n²)O(1)✅ Так
Selection SortO(n²)O(n²)O(n²)O(1)❌ Ні
Insertion SortO(n)O(n²)O(n²)O(1)✅ Так
Quick SortO(n log n)O(n log n)O(n²)O(log n)❌ Ні
Стабільне сортування (stable sort) зберігає відносний порядок рівних елементів. Наприклад, якщо у масиві є два елементи зі значенням 5, стабільне сортування гарантує, що їх взаємний порядок не зміниться. Це важливо при сортуванні об'єктів за кількома ключами.

Коли що обирати?

🫧 Bubble Sort

  • Лише для навчання
  • Перевірка «чи масив відсортований» (з оптимізацією прапором)
  • Ніколи у продакшн-коді

🎯 Selection Sort

  • Коли операцій запису у пам'ять має бути мінімум
  • Маленькі масиви (до ~20 елементів)
  • Коли потрібно точно знати, що буде не більше N обмінів

🃏 Insertion Sort

  • Майже відсортовані масиви
  • Маленькі масиви (до 15–30 елементів)
  • Онлайн-сортування (елементи надходять по одному)
  • Фінальна стадія у гібридних алгоритмах (Timsort, Introsort)

⚡ Quick Sort

  • Загальний випадок: великі масиви довільних даних
  • Коли важлива практична швидкість, а не теоретична гарантія
  • З рандомізованим pivot — безпечно для більшості сценаріїв
  • Основа STL std::sort

Масштаб: як різниться час на практиці?

Для n = 10 000 елементів (орієнтовні значення):

АлгоритмОперацій (приблизно)
Insertion Sort (впорядкований вхід)≈ 10 000
Quick Sort≈ 130 000
Insertion Sort (випадковий вхід)≈ 25 000 000
Bubble Sort≈ 50 000 000
Selection Sort≈ 50 000 000

Навіть 10 000 елементів вже показують суттєву різницю між O(n log n) та O(n²).


Практичні завдання

Рівень 1 — Базовий

Рівень 2 — Логічний

Рівень 3 — Творчий

Підсумок

📌 Big O

Описує зростання кількості операцій залежно від n у найгіршому випадку. Константи відкидаються. O(n log n) >> O(n²) практично.

📌 Bubble Sort O(n²)

Порівняння сусідів, «спливання» максимуму. Оптимізація-прапором → O(n) у best case. Лише для навчання.

📌 Selection Sort O(n²)

Пошук мінімуму, обмін на початок. Мінімум N-1 обмінів за весь алгоритм. Завжди O(n²).

📌 Insertion Sort O(n²)

Вставка поточного елемента на правильне місце серед відсортованих. O(n) на майже відсортованих даних. Ефективний для малих масивів.

📌 Quick Sort O(n log n)

Розділяй і володарюй. Pivot ділить масив на менші/більші. O(n log n) average, O(n²) worst. Рандомізація усуває найгірший випадок.

📌 Вибір алгоритму

Малі/майже-відсортовані → Insertion. Мінімум обмінів → Selection. Загальний випадок → Quicksort. Навчання → Bubble.
Copyright © 2026