Ось просте O(N)
рішення, яке використовує O(N)
простір. Я припускаю, що ми обмежуємо список введення невід’ємними числами і що ми хочемо знайти перше невід’ємне число, якого немає у списку.
- Знайдіть довжину списку; припустимо, це так
N
.
- Виділіть масив
N
булевих значень , ініціалізованих для всіх false
.
- Для кожного числа
X
у списку, якщо X
воно менше N
, встановіть для X'th
елемента масиву значення true
.
- Скануйте масив, починаючи з індексу
0
, шукаючи перший елемент, який є false
. Якщо ви знайдете перший false
за індексом I
, тоді I
це відповідь. Інакше (тобто коли всі елементи є true
) відповідь є N
.
На практиці "масив N
булевих символів", мабуть, буде кодований як "растрове зображення" або "бітсет", представлене як byte
або int
масив. Зазвичай це використовує менше місця (залежно від мови програмування) і дозволяє сканувати перше, false
щоб зробити його швидше.
Ось як / чому працює алгоритм.
Припустимо, що N
числа у списку не відрізняються, або що одне або кілька з них більше ніж N
. Це означає, що в діапазоні повинно бути принаймні одне число 0 .. N - 1
, якого немає в списку. Тож проблема пошуку найменшого відсутнього числа, отже, повинна зводитися до задачі пошуку найменшого відсутнього числа, менше ніжN
. Це означає, що нам не потрібно відстежувати числа, більші чи рівні N
..., оскільки вони не будуть відповіддю.
Альтернативою попередньому абзацу є те, що список є перестановкою чисел з 0 .. N - 1
. У цьому випадку на кроці 3 встановлюються всі елементи масиву true
, а на кроці 4 повідомляється, що першим «відсутнім» числом є N
.
Обчислювальна складність алгоритму має O(N)
відносно невелику константу пропорційності. Він робить два лінійні проходи через список або лише один прохід, якщо відомо, що довжина списку починається з. Немає необхідності представляти утримання всього списку в пам'яті, тому використання асимптотичної пам'яті алгоритму - це саме те, що потрібно для представлення масиву булевих значень; тобто O(N)
біти.
(На відміну від цього, алгоритми, які покладаються на сортування в пам’яті або розділення, передбачають, що ви можете представити весь список у пам’яті. У тому вигляді, в якому було задано питання, для цього знадобляться O(N)
64-розрядні слова.)
@Jorn коментує, що кроки з 1 по 3 є варіацією підрахунку сортування. У певному сенсі він має рацію, але відмінності суттєві:
- Для сортування підрахунку потрібен масив (принаймні)
Xmax - Xmin
лічильників, де Xmax
є найбільше число у списку та Xmin
найменше число у списку. Кожен лічильник повинен мати можливість представляти N держав; тобто припускаючи двійкове представлення, воно повинно мати цілочисельний тип (принаймні) ceiling(log2(N))
біти.
- Щоб визначити розмір масиву, підрахунку сортування потрібно зробити початковий прохід через список, щоб визначити
Xmax
і Xmin
.
- Отже, мінімальна потреба в найгіршому випадку - це
ceiling(log2(N)) * (Xmax - Xmin)
біти.
На відміну від цього, алгоритм, представлений вище, просто вимагає N
біти в гірших і найкращих випадках.
Однак цей аналіз приводить до інтуїції, що якщо алгоритм зробив початковий прохід через список, шукаючи нуль (і підраховуючи елементи списку, якщо потрібно), він дав би швидшу відповідь, не використовуючи пробілу, якщо знайшов нуль. Безумовно, варто це зробити, якщо є велика ймовірність знайти хоча б один нуль у списку. І цей додатковий пропуск не змінює загальної складності.
РЕДАКТУВАТИ: Я змінив опис алгоритму на використання "масиву булевих символів", оскільки люди, мабуть, визнали мій оригінальний опис за допомогою бітів та растрових зображень незрозумілим.