Яка складність бінарного пошуку?
Коротка відповідь
- Час: O(log n) у середньому і найгіршому випадках; O(1) у найкращому (якщо шуканий елемент - одразу в середині).
- Пам'ять: O(1) для ітеративної реалізації; O(log n) для рекурсивної (через стек викликів).
- Кількість порівнянь: приблизно ⌈log2 n⌉ (у межах ⌈log2 n⌉...⌈log2 n⌉+1).
Детальний розбір
Передумови застосування
- Дані відсортовані за ключем порівняння.
- Є випадковий доступ до елементів за O(1) (наприклад, масив/динамічний масив).
- Компаратор задає строгий порядок і транзитивний (без суперечностей).
Чому O(log n)
На кожному кроці бінарний пошук ділить поточний діапазон навпіл і відкидає одну з половин. Після k кроків залишається не більше n / 2^k елементів. Зупинка відбувається, коли залишилося 1 або 0 елементів, тобто n / 2^k ≤ 1 ⇒ k ≥ ⌈log2 n⌉. Тому час - O(log n).
Випадки за часом
- Найкращий випадок: O(1) - шукане значення одразу в середині.
- Середній випадок: O(log n) - усереднення за рівномірним розподілом цільового індексу.
- Найгірший випадок: O(log n) - потрібна максимально можлива кількість кроків до звуження діапазону до одиниці.
Пам'ять
Ітеративна реалізація використовує сталу кількість змінних - O(1). Рекурсивна додає вкладеність викликів глибиною близько ⌈log2 n⌉ - O(log n) за пам'яттю.
Код (JavaScript)
// Ітеративний бінарний пошук: індекс знайденого елемента або -1
function binarySearch(arr, target) {
let left = 0;
let right = arr.length - 1;
while (left <= right) {
// Безпечне обчислення середини (захищає від переповнення в мовах з 32-бітними int)
const mid = left + ((right - left) >> 1);
if (arr[mid] === target) return mid;
if (arr[mid] < target) left = mid + 1; else right = mid - 1;
}
return -1;
}
// lowerBound: перший індекс i, де arr[i] >= target
function lowerBound(arr, target) {
let left = 0, right = arr.length; // напівінтервал [left, right)
while (left < right) {
const mid = left + ((right - left) >> 1);
if (arr[mid] < target) left = mid + 1; else right = mid;
}
return left;
}
// upperBound: перший індекс i, де arr[i] > target
function upperBound(arr, target) {
let left = 0, right = arr.length;
while (left < right) {
const mid = left + ((right - left) >> 1);
if (arr[mid] <= target) left = mid + 1; else right = mid;
}
return left;
}
// Перший індекс входження target або -1 (для масивів з повторами)
function firstOccurrence(arr, target) {
const i = lowerBound(arr, target);
return i < arr.length && arr[i] === target ? i : -1;
}
// Останній індекс входження target або -1
function lastOccurrence(arr, target) {
const j = upperBound(arr, target) - 1;
return j >= 0 && arr[j] === target ? j : -1;
}
// Позиція вставки для збереження сортування (якщо елемента немає)
function insertionIndex(arr, target) {
return lowerBound(arr, target);
}
// Приклади використання
const a = [1, 2, 4, 4, 5, 9, 12];
console.log(binarySearch(a, 5)); // 4
console.log(firstOccurrence(a, 4)); // 2
console.log(lastOccurrence(a, 4)); // 3
console.log(insertionIndex(a, 6)); // 5Часті помилки і нюанси
- Неправильні межі циклу: використовувати узгоджений напівінтервал [l, r) чи замкнутий [l, r] і відповідну умову зупинки.
- Обчислення середини як (l + r) / 2 може переповнюватися в мовах з 32-бітними цілими; безпечніше l + (r - l) / 2.
- Зациклення при неправильному оновленні меж (наприклад, r = mid замість r = mid - 1 у замкнутому інтервалі).
- Дублікати: базовий пошук повертає будь-який знайдений індекс; для першого/останнього входження використовуйте lowerBound/upperBound.
- Несортовані дані чи структури без O(1) доступу (наприклад, зв'язні списки) не підходять для класичного бінарного пошуку.
Порівняння з лінійним пошуком
Лінійний пошук - O(n). Бінарний - O(log n). Наприклад, при n = 1 000 000 бінарний пошук робить приблизно 20 кроків, тоді як лінійний може потребувати до мільйона порівнянь.
Підсумок
Бінарний пошук працює за O(log n) за часом і O(1)/O(log n) за пам'яттю (ітеративно/рекурсивно). Це досягається завдяки поетапному діленню діапазону пошуку навпіл.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.