середняalgorithmsbig-ocomplexityperformanceinterviewoptimization

Big O Notation - Як розрахувати складність алгоритмів

Детальний розібір Big O notation з практичними прикладами на JavaScript для розуміння часової та просторової складності

Big O Notation - Як розрахувати складність алгоритмів

Вступ

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

🧠 Інтуїтивне розуміння - Життєві аналогії

Перш ніж переходити до коду, давайте зрозуміємо Big O через звичайні життєві ситуації:

📖 Аналогія з пошуком у книзі

Уявіть, що ви шукаєте слово в різних типах книг:

O(1) - Константна складність

🎯 Пошук у словнику з закладками
Ви знаєте, що слово "Apple" на сторінці 15
Незалежно від того, 100 чи 10,000 сторінок у словнику - 
ви одразу переходите на сторінку 15

O(log n) - Логарифмічна складність

📚 Пошук у відсортованому словнику
1. Відкриваєте книгу посередині
2. Якщо ваше слово має бути раніше - беріть ліву половину
3. Якщо пізніше - праву половину  
4. Повторюєте з новою половиною

Кожен крок зменшує область пошуку вдвічі!

O(n) - Лінійна складність

📑 Пошук у невідсортованій книзі
Читаєте сторінку за сторінкою, доки не знайдете
Якщо книга в 2 рази більша - шукатимете в 2 рази довше

O(n²) - Квадратична складність

🔍 Порівняння кожного слова з кожним
Для кожного слова у книзі порівнюєте його з усіма іншими словами
Якщо в книзі 100 слів - зробите 10,000 порівнянь!

🏪 Аналогія з походом у магазин

O(1) - Ви знаєте, де лежить хліб, і йдете прямо туди O(log n) - Питаєте працівника, він каже "2 поверх, ліва секція", потім "3 ряд" O(n) - Обходите всі ряди підряд, доки не знайдете
O(n²) - Для кожного товару перевіряєте всі інші товари на схожість

🔢 Візуальне розуміння з маленькими числами

Давайте подивимося, як зростає час виконання для різних алгоритмів:

Розмір масиву (n)O(1)O(log n)O(n)O(n log n)O(n²)O(2ⁿ)
1101012
2112244
412481616
81382464256
1614166425665,536
10001101,00010,0001,000,000💥

Що це означає?

  • ✅ O(1): Завжди 1 операція - супер!
  • ✅ O(log n): Навіть для 1000 елементів - лише 10 операцій
  • ⚠️ O(n): 1000 елементів = 1000 операцій - прийнятно
  • ⚠️ O(n log n): 1000 елементів = 10,000 операцій - ще ОК
  • ❌ O(n²): 1000 елементів = 1,000,000 операцій - повільно!
  • 💀 O(2ⁿ): Вже при 16 елементах = 65,536 операцій - жахливо!

Основні поняття

Що таке Big O?

Big O описує найгірший випадок роботи алгоритму. Воно показує верхню межу того, наскільки повільно може працювати ваш код при збільшенні даних.

// Приклад: пошук елемента в масиві
function findElement(arr, target) {
    for (let i = 0; i < arr.length; i++) {
        if (arr[i] === target) {
            return i;
        }
    }
    return -1;
}

// Найкращий випадок: O(1) - елемент на першій позиції
// Найгірший випадок: O(n) - елемент в кінці або відсутній
// Big O завжди описує найгірший випадок: O(n)

Типи складності

НотаціяНазваОписПриклад
O(1)КонстантнаЧас не залежить від розміруДоступ до елемента масиву
O(log n)ЛогарифмічнаЧас зростає логарифмічноБінарний пошук
O(n)ЛінійнаЧас пропорційний розміруІтерація по масиву
O(n log n)Лінійно-логарифмічнаКомбінація лінійної та логарифмічноїЕфективне сортування
O(n²)КвадратичнаЧас пропорційний квадрату розміруВкладені цикли
O(2ⁿ)ЕкспоненціальнаЧас подвоюється з кожним новим елементомРекурсія Фібоначчі

Як розрахувати Big O - Покрокова інструкція

Крок 1: Знайдіть домінуючі операції

function example1(arr) {
    let sum = 0;           // O(1)
    
    for (let i = 0; i < arr.length; i++) {    // O(n)
        sum += arr[i];     // O(1)
    }
    
    console.log(sum);      // O(1)
    return sum;            // O(1)
}

// Результат: O(1) + O(n) + O(1) + O(1) + O(1) = O(n)
// Домінує O(n), тому загальна складність O(n)

Крок 2: Ігноруйте константи

function example2(arr) {
    // Перший цикл: O(n)
    for (let i = 0; i < arr.length; i++) {
        console.log(arr[i]);
    }
    
    // Другий цикл: O(n)  
    for (let i = 0; i < arr.length; i++) {
        console.log(arr[i] * 2);
    }
}

// O(n) + O(n) = O(2n) = O(n)
// Константи ігноруються

Крок 3: Аналізуйте вкладені цикли

function example3(arr) {
    for (let i = 0; i < arr.length; i++) {        // O(n)
        for (let j = 0; j < arr.length; j++) {    // O(n)
            console.log(arr[i], arr[j]);          // O(1)
        }
    }
}

// O(n) * O(n) * O(1) = O(n²)

🤔 Метод "Думання вголос" - Як аналізувати крок за кроком

Давайте навчимося аналізувати код, думаючи вголос як справжній програміст:

Приклад 1: Простий код

function simple(arr) {
    console.log(arr[0]);        // ← Що тут відбувається?
    console.log(arr[1]);        // ← Що тут відбувається?
    console.log("Hello");       // ← Що тут відбувається?
}

💭 Думання вголос:

"Хм, що тут відбувається? Дивлюся на кожен рядок:

  • Рядок 1: Виводжу перший елемент - це завжди одна операція, незалежно від розміру масиву
  • Рядок 2: Виводжу другий елемент - теж одна операція
  • Рядок 3: Виводжу текст - одна операція

Всього: 3 операції. Якщо масив має 10 елементів - 3 операції. Якщо 1000 елементів - все одно 3 операції! Це означає O(1) - константна складність."

Приклад 2: Один цикл

function oneLoop(arr) {
    for (let i = 0; i < arr.length; i++) {    // ← Скільки раз це виконається?
        console.log(arr[i]);                  // ← Що відбувається всередині?
    }
}

💭 Думання вголос:

"Бачу цикл. Питаю себе: скільки раз він виконається?

  • Цикл йде від 0 до arr.length
  • Якщо масив має 5 елементів, цикл виконається 5 разів
  • Якщо масив має 100 елементів, цикл виконається 100 разів
  • Всередині циклу лише одна операція console.log

Висновок: кількість операцій = розмір масиву = n Це O(n) - лінійна складність"

Приклад 3: Два цикли підряд

function twoLoops(arr) {
    // Перший цикл
    for (let i = 0; i < arr.length; i++) {
        console.log("First:", arr[i]);
    }
    
    // Другий цикл  
    for (let i = 0; i < arr.length; i++) {
        console.log("Second:", arr[i]);
    }
}

💭 Думання вголос:

"Два цикли, але вони один за одним, не вкладені.

  • Перший цикл: n операцій
  • Другий цикл: n операцій
  • Разом: n + n = 2n операцій

Але в Big O ми відкидаємо константи! 2n = O(n). Два цикли підряд - все одно O(n)"

Приклад 4: Вкладені цикли

function nestedLoops(arr) {
    for (let i = 0; i < arr.length; i++) {           // ← Зовнішній цикл
        for (let j = 0; j < arr.length; j++) {       // ← Внутрішній цикл
            console.log(arr[i], arr[j]);             // ← Що тут?
        }
    }
}

💭 Думання вголос:

"Ось тут цікаво! Цикл всередині циклу.

  • Зовнішній цикл виконується n разів
  • Для КОЖНОЇ ітерації зовнішнього циклу, внутрішній цикл виконується n разів
  • Якщо масив має 3 елементи:
    • i=0: внутрішній цикл 3 рази (j=0,1,2)
    • i=1: внутрішній цикл 3 рази (j=0,1,2)
    • i=2: внутрішній цикл 3 рази (j=0,1,2)
    • Всього: 3 × 3 = 9 операцій

Загальна формула: n × n = n² Це O(n²) - квадратична складність"

🎯 Простий алгоритм розпізнавання

Ось простий спосіб швидко визначити складність:

🔍 Крок 1: Рахуйте цикли

// Немає циклів → O(1)
function constant() {
    return 5 + 3;
}

// Один цикл → O(n)  
function linear(arr) {
    for (item of arr) { }
}

// Два цикли підряд → O(n)
function stillLinear(arr) {
    for (item of arr) { }  // n операцій
    for (item of arr) { }  // + n операцій = 2n = O(n)
}

// Цикл в циклі → O(n²)
function quadratic(arr) {
    for (item1 of arr) {      // n разів
        for (item2 of arr) {  // × n разів = n²
        }
    }
}

// Три вкладені цикли → O(n³)
function cubic(arr) {
    for (item1 of arr) {
        for (item2 of arr) {
            for (item3 of arr) {
            }
        }
    }
}

🔍 Крок 2: Ділення навпіл = log n

// Якщо на кожному кроці ділите задачу навпіл → O(log n)
function binarySearch() {
    // Кожна ітерація: розмір задачі / 2
    // n → n/2 → n/4 → n/8 → ... → 1
}

🔍 Крок 3: Рекурсія з двома викликами = 2ⁿ

// Якщо кожен виклик породжує 2 нових → O(2ⁿ)
function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n-1) + fibonacci(n-2);  // 2 виклики!
}

📝 Тренування: Розберіть самостійно

Спробуйте визначити складність цих функцій:

Завдання 1:

function mystery1(arr) {
    let sum = 0;
    for (let i = 0; i < arr.length; i++) {
        sum += arr[i];
    }
    return sum;
}
💡 Підказка Один цикл від 0 до n
✅ Відповідь O(n) - лінійна складність. Один цикл, який виконується n разів.

Завдання 2:

function mystery2(arr) {
    for (let i = 0; i < arr.length; i++) {
        for (let j = i + 1; j < arr.length; j++) {
            if (arr[i] > arr[j]) {
                console.log("Found pair");
            }
        }
    }
}
💡 Підказка Цикл в циклі, але внутрішній починається з i+1
✅ Відповідь O(n²) - квадратична складність. Хоча внутрішній цикл не завжди виконується n разів, у найгіршому випадку маємо приблизно n²/2 операцій, що дає O(n²).

Завдання 3:

function mystery3(arr) {
    if (arr.length <= 1) return arr;
    
    const mid = Math.floor(arr.length / 2);
    const left = mystery3(arr.slice(0, mid));
    const right = mystery3(arr.slice(mid));
    
    return merge(left, right);  // merge() працює за O(n)
}
💡 Підказка Ділимо масив навпіл на кожному рівні рекурсії
✅ Відповідь O(n log n). Рівнів рекурсії log n (ділимо навпіл), на кожному рівні робимо O(n) роботи.

Практичні приклади з JavaScript

O(1) - Константна складність

// Доступ до елемента масиву
function getFirst(arr) {
    return arr[0];  // Завжди O(1), незалежно від розміру масиву
}

// Додавання до кінця масиву
function addToEnd(arr, element) {
    arr.push(element);  // O(1) в середньому
}

// Доступ до властивості об'єкта
function getProperty(obj, key) {
    return obj[key];  // O(1) для об'єктів та Map
}

O(log n) - Логарифмічна складність

// Бінарний пошук в відсортованому масиві
function binarySearch(arr, target) {
    let left = 0;
    let right = arr.length - 1;
    
    while (left <= right) {
        const mid = Math.floor((left + right) / 2);
        
        if (arr[mid] === target) {
            return mid;
        } else if (arr[mid] < target) {
            left = mid + 1;     // Відкидаємо половину масиву
        } else {
            right = mid - 1;    // Відкидаємо половину масиву  
        }
    }
    
    return -1;
}

// На кожній ітерації розмір задачі зменшується вдвічі
// Тому складність O(log n)

O(n) - Лінійна складність

// Пошук максимального елемента
function findMax(arr) {
    let max = arr[0];
    
    for (let i = 1; i < arr.length; i++) {  // O(n)
        if (arr[i] > max) {
            max = arr[i];
        }
    }
    
    return max;
}

// Фільтрація масиву
function filterEven(arr) {
    const result = [];
    
    for (const num of arr) {  // O(n)
        if (num % 2 === 0) {
            result.push(num);  // O(1)
        }
    }
    
    return result;
}

O(n log n) - Лінійно-логарифмічна складність

// Сортування злиттям (Merge Sort)
function mergeSort(arr) {
    if (arr.length <= 1) {
        return arr;
    }
    
    const mid = Math.floor(arr.length / 2);
    const left = mergeSort(arr.slice(0, mid));    // O(log n) рівнів рекурсії
    const right = mergeSort(arr.slice(mid));      // O(log n) рівнів рекурсії
    
    return merge(left, right);  // O(n) на кожному рівні
}

function merge(left, right) {
    const result = [];
    let i = 0, j = 0;
    
    while (i < left.length && j < right.length) {  // O(n)
        if (left[i] <= right[j]) {
            result.push(left[i++]);
        } else {
            result.push(right[j++]);
        }
    }
    
    return result.concat(left.slice(i)).concat(right.slice(j));
}

// O(log n) рівнів * O(n) робота на рівень = O(n log n)

O(n²) - Квадратична складність

// Bubble Sort
function bubbleSort(arr) {
    const n = arr.length;
    
    for (let i = 0; i < n - 1; i++) {           // O(n)
        for (let j = 0; j < n - i - 1; j++) {   // O(n)
            if (arr[j] > arr[j + 1]) {
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];  // O(1)
            }
        }
    }
    
    return arr;
}

// Пошук дублікатів (наївний підхід)
function findDuplicates(arr) {
    const duplicates = [];
    
    for (let i = 0; i < arr.length; i++) {      // O(n)
        for (let j = i + 1; j < arr.length; j++) {  // O(n)
            if (arr[i] === arr[j]) {
                duplicates.push(arr[i]);
            }
        }
    }
    
    return duplicates;
}

O(2ⁿ) - Експоненціальна складність

// Наївна рекурсія Фібоначчі
function fibonacci(n) {
    if (n <= 1) {
        return n;
    }
    
    return fibonacci(n - 1) + fibonacci(n - 2);
    // Кожен виклик породжує 2 нових виклики
    // Глибина рекурсії n, тому O(2ⁿ)
}

// Оптимізована версія з мемоізацією O(n)
function fibonacciMemo(n, memo = {}) {
    if (n <= 1) return n;
    if (memo[n]) return memo[n];
    
    memo[n] = fibonacciMemo(n - 1, memo) + fibonacciMemo(n - 2, memo);
    return memo[n];
}

Просторова складність

Просторова складність описує, скільки додаткової пам'яті використовує алгоритм.

// O(1) просторова складність
function sumArray(arr) {
    let sum = 0;  // Константний об'єм пам'яті
    
    for (const num of arr) {
        sum += num;
    }
    
    return sum;
}

// O(n) просторова складність  
function reverseArray(arr) {
    const reversed = [];  // Масив розміром n
    
    for (let i = arr.length - 1; i >= 0; i--) {
        reversed.push(arr[i]);
    }
    
    return reversed;
}

// O(log n) просторова складність (рекурсивний стек)
function binarySearchRecursive(arr, target, left = 0, right = arr.length - 1) {
    if (left > right) return -1;
    
    const mid = Math.floor((left + right) / 2);
    
    if (arr[mid] === target) return mid;
    if (arr[mid] < target) {
        return binarySearchRecursive(arr, target, mid + 1, right);
    }
    return binarySearchRecursive(arr, target, left, mid - 1);
}
// Глибина рекурсії log n, кожен виклик займає O(1) пам'яті

Аналіз складних випадків

Приклад 1: Аналіз зі зменшенням розміру

function mysteryFunction(arr) {
    if (arr.length <= 1) return arr;
    
    const mid = Math.floor(arr.length / 2);
    
    // Обробляємо тільки половину масиву
    return mysteryFunction(arr.slice(0, mid));
}

// T(n) = T(n/2) + O(1)
// Це дає нам O(log n) часову складність
// Але slice() створює новий масив, тому O(n) просторова складність

Приклад 2: Амортизована складність

class DynamicArray {
    constructor() {
        this.data = new Array(1);
        this.size = 0;
        this.capacity = 1;
    }
    
    push(element) {
        if (this.size === this.capacity) {
            this.resize();  // O(n) в найгіршому випадку
        }
        
        this.data[this.size] = element;  // O(1)
        this.size++;
    }
    
    resize() {
        const newCapacity = this.capacity * 2;
        const newData = new Array(newCapacity);
        
        for (let i = 0; i < this.size; i++) {
            newData[i] = this.data[i];
        }
        
        this.data = newData;
        this.capacity = newCapacity;
    }
}

// Поодинокий push(): O(1) амортизована складність
// Хоча іноді resize() дає O(n), це відбувається рідко

Поради для співбесід

1. Структурований підхід до аналізу

function analyzeComplexity(func) {
    // 1. Визначте базові операції
    // 2. Знайдіть цикли та рекурсію  
    // 3. Проаналізуйте, як зростає робота з розміром даних
    // 4. Врахуйте найгірший випадок
    // 5. Спростіть, прибравши константи
}

2. Покрокове пояснення

// Поганий приклад пояснення:
"Це O(n²) бо є два цикли"

// Хороший приклад пояснення:
"Зовнішній цикл виконується n разів. Для кожної ітерації зовнішнього циклу, 
внутрішній цикл також виконується до n разів. У найгіршому випадку 
ми виконуємо n * n = n² операцій, тому складність O(n²)"

3. Практичні поради

// Завжди запитуйте про обмеження:
function optimizeForConstraints(arr) {
    const n = arr.length;
    
    // Якщо n < 100: можна використати O(n²) алгоритм
    if (n < 100) {
        return bubbleSort(arr);
    }
    
    // Якщо n велике: потрібен O(n log n) алгоритм
    return mergeSort(arr);
}

Поширені помилки та пастки

Помилка 1: Плутанина з вбудованими методами

// НЕПРАВИЛЬНО: вважати що це O(n)
function badExample(arr) {
    for (const item of arr) {              // O(n)
        if (arr.includes(item * 2)) {      // includes() це O(n)!
            console.log(item);
        }
    }
}
// Насправді це O(n²)!

// ПРАВИЛЬНО: використати Set для O(1) пошуку
function goodExample(arr) {
    const set = new Set(arr);              // O(n) для створення
    
    for (const item of arr) {              // O(n)
        if (set.has(item * 2)) {           // has() це O(1)
            console.log(item);
        }
    }
}
// Це справді O(n)

Помилка 2: Ігнорування просторової складності

// Здається ефективним, але використовує O(n²) пам'яті
function createMatrix(n) {
    const matrix = [];
    
    for (let i = 0; i < n; i++) {
        const row = [];
        for (let j = 0; j < n; j++) {
            row.push(i * j);
        }
        matrix.push(row);
    }
    
    return matrix;  // O(n²) просторова складність
}

Помилка 3: Неврахування рекурсивного стеку

// Виглядає як O(n) часова складність
function recursiveSum(arr, index = 0) {
    if (index >= arr.length) return 0;
    
    return arr[index] + recursiveSum(arr, index + 1);
}

// Але просторова складність O(n) через стек викликів!
// Ітеративна версія мала б O(1) просторову складність

Практичні завдання для самоперевірки

Завдання 1: Визначте складність

function mystery1(arr) {
    let count = 0;
    
    for (let i = 0; i < arr.length; i++) {
        for (let j = i; j < arr.length; j++) {
            count++;
        }
    }
    
    return count;
}

// Ваша відповідь: ?
// Правильна відповідь: O(n²)
// Пояснення: i змінюється від 0 до n, j від i до n
// Загальна кількість ітерацій: n + (n-1) + (n-2) + ... + 1 = n(n+1)/2 = O(n²)

Завдання 2: Оптимізуйте функцію

// Повільна версія O(n²)
function hasDuplicatesSlow(arr) {
    for (let i = 0; i < arr.length; i++) {
        for (let j = i + 1; j < arr.length; j++) {
            if (arr[i] === arr[j]) {
                return true;
            }
        }
    }
    return false;
}

// Швидка версія O(n)
function hasDuplicatesFast(arr) {
    const seen = new Set();
    
    for (const item of arr) {
        if (seen.has(item)) {
            return true;
        }
        seen.add(item);
    }
    
    return false;
}

Висновки

Big O notation - це фундаментальний інструмент для:

  1. Аналізу ефективності коду
  2. Порівняння алгоритмів
  3. Прогнозування продуктивності на великих даних
  4. Успішного проходження технічних співбесід

Ключові принципи:

  • Аналізуйте найгірший випадок
  • Ігноруйте константи та менші терми
  • Враховуйте як часову, так і просторову складність
  • Думайте про масштабованість

Порядок зростання (від кращого до гіршого):

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

Пам'ятайте: розуміння Big O допомагає писати код, який залишається швидким навіть при зростанні обсягу даних. Це критично важливо для створення масштабованих додатків та успішного проходження технічних співбесід.

🚀 Швидкий чек-лист для початківців

✅ Перед аналізом коду запитайте себе:

  1. Чи є цикли?
    • Немає → O(1)
    • Один → O(n)
    • Вкладені → O(n²), O(n³)...
  2. Чи ділиться задача навпіл?
    • Так → O(log n)
  3. Чи є рекурсія з кількома викликами?
    • Так → можливо O(2ⁿ) або O(n!)
  4. Які вбудовані методи використовуються?
    • arr.includes(), arr.indexOf() → O(n)
    • Set.has(), Map.get() → O(1)
    • arr.sort() → O(n log n)

🎯 Швидкі шаблони розпізнавання:

// 🟢 O(1) - Константа
arr[0], obj.property, Math.max(a, b)

// 🟡 O(log n) - Логарифм  
binarySearch, balanced tree operations

// 🟡 O(n) - Лінійна
for (item of arr), arr.find(), arr.filter()

// 🟠 O(n log n) - Квазілінійна
mergeSort, quickSort (середній випадок)

// 🔴 O(n²) - Квадратична  
for + for (вкладені), bubbleSort

// 💀 O(2ⁿ) - Експоненціальна
fibonacci (наївна рекурсія)

🎮 Інтерактивні вправи для закріплення

Вправа 1: Гра "Швидке розпізнавання"

Подивіться на код та швидко скажіть складність:

// A)
function example(n) {
    return n * 2;
}

// B) 
function example(arr) {
    for (let i = 0; i < arr.length; i++) {
        console.log(arr[i]);
    }
}

// C)
function example(arr) {
    for (let i = 0; i < arr.length; i++) {
        for (let j = 0; j < arr.length; j++) {
            console.log(i, j);
        }
    }
}

// D)
function example(arr, target) {
    let left = 0, right = arr.length - 1;
    while (left <= right) {
        let mid = Math.floor((left + right) / 2);
        if (arr[mid] === target) return mid;
        if (arr[mid] < target) left = mid + 1;
        else right = mid - 1;
    }
}
🎯 Відповіді
  • A) O(1) - Одна операція незалежно від n
  • B) O(n) - Один цикл по масиву
  • C) O(n²) - Вкладені цикли
  • D) O(log n) - Бінарний пошук (ділення навпіл)

Вправа 2: Знайдіть помилки в аналізі

function processData(arr) {
    // Крок 1: Видалити дублікати  
    const unique = [];
    for (let i = 0; i < arr.length; i++) {
        if (!unique.includes(arr[i])) {  // ⚠️ Помилка тут!
            unique.push(arr[i]);
        }
    }
    
    // Крок 2: Відсортувати
    unique.sort();  // O(n log n)
    
    return unique;
}

❌ Неправильний аналіз: "Це O(n log n) через сортування"

✅ Правильний аналіз: Це O(n²)!

  • includes() всередині циклу: O(n) × O(n) = O(n²)
  • Сортування: O(n log n)
  • Домінує O(n²)

🔧 Як виправити:

function processDataOptimized(arr) {
    // O(n) замість O(n²)
    const unique = [...new Set(arr)];
    
    // O(n log n)
    unique.sort();
    
    return unique;
}
// Загальна складність: O(n log n)

Вправа 3: Реальні сценарії

Яку складність мають ці операції у вашому додатку?

// Сценарій 1: Автокомпліт у пошуку
function autocomplete(query, dictionary) {
    return dictionary.filter(word => 
        word.startsWith(query)  // O(k) де k - довжина слова
    );
}
// Відповідь: O(n×k) де n - розмір словника, k - середня довжина слова

// Сценарій 2: Рендерінг списку
function renderUserList(users) {
    return users.map(user => ({
        ...user,  // O(p) де p - кількість властивостей
        avatar: generateAvatar(user.id)  // O(1)
    }));
}
// Відповідь: O(n×p) де n - кількість користувачів

// Сценарій 3: Валідація форми
function validateForm(formData) {
    const errors = {};
    
    for (const field in formData) {        // O(f) полів
        if (formData[field].length === 0) {
            errors[field] = "Required";
        }
    }
    
    return errors;
}
// Відповідь: O(f) де f - кількість полів форми

🛠️ Практичні поради для розробки

1. Коли не варто хвилюватися про Big O

// ✅ Для невеликих даних (< 1000 елементів) O(n²) може бути OK
function smallDataSort(arr) {
    if (arr.length < 100) {
        return bubbleSort(arr);  // O(n²) але простіший код
    }
    return mergeSort(arr);       // O(n log n) для великих даних
}

// ✅ Одноразові операції
function initializeApp() {
    // O(n²) при ініціалізації - не проблема
    for (let config of configs) {
        for (let plugin of plugins) {
            if (plugin.supports(config)) {
                plugin.configure(config);
            }
        }
    }
}

2. Коли Big O критично важливо

// ❌ Це буде повільно для великих даних!
function searchInRealTime(query, largeDataset) {
    return largeDataset.filter(item => 
        item.title.includes(query) ||      // O(n×m)
        item.description.includes(query)   // O(n×m)  
    );
}

// ✅ Краще використати індекси
class SearchIndex {
    constructor(dataset) {
        this.index = new Map();  // Попередньо створений індекс
        // Індексація: O(n×m) один раз при створенні
    }
    
    search(query) {
        return this.index.get(query) || [];  // O(1) пошук!
    }
}

3. Швидкі перевірки в коді

// 🚨 Червоні прапорці - ці конструкції потребують уваги:

// Вкладені цикли
for (...) {
    for (...) {  // ⚠️ Можливо O(n²)
    }
}

// Array методи в циклах  
for (item of array) {
    if (array.includes(something)) {  // ⚠️ O(n²)
    }
}

// Рекурсія без мемоізації
function recursive(n) {
    return recursive(n-1) + recursive(n-2);  // ⚠️ Можливо O(2ⁿ)
}

// Створення об'єктів в циклах
for (item of array) {
    result.push({...item, extra: value});  // ⚠️ Копіювання може бути дорогим
}

🎓 Заключні поради

Запам'ятайте головне:

  1. Почніть з простого - рахуйте цикли
  2. Практикуйтеся щодня - аналізуйте код, який пишете
  3. Не паніикуйте - Big O стає інтуїтивним з часом
  4. Думайте про користувача - швидкий код = краща користувацька взаємодія

Коли сумніваєтесь:

  • Спробуйте з маленькими числами (n=1,2,3,4)
  • Подумайте: "що станеться, якщо даних стане в 10 разів більше?"
  • Шукайте домінуючи операції (найповільніші частини)

Успіхів у вивченні алгоритмів! 🚀