Oto proste O(N)
rozwiązanie, które wykorzystuje O(N)
przestrzeń. Zakładam, że ograniczamy listę wejściową do liczb nieujemnych i chcemy znaleźć pierwszą nieujemną liczbę, której nie ma na liście.
- Znajdź długość listy; powiedzmy, że tak
N
.
- Przydziel tablicę
N
wartości logicznych, zainicjowaną dla wszystkich false
.
- Dla każdej liczby
X
na liście, jeśli X
jest mniejsza niż N
, ustaw X'th
element tablicy na true
.
- Przeszukaj tablicę, zaczynając od indeksu
0
, szukając pierwszego elementu false
. Jeśli znajdziesz pierwszy false
w indeksie I
, to I
jest odpowiedź. W przeciwnym razie (tj. Gdy wszystkie elementy są true
) odpowiedź brzmi N
.
W praktyce „tablica N
wartości logicznych” byłaby prawdopodobnie zakodowana jako „mapa bitowa” lub „zestaw bitów” reprezentowana jako tablica a byte
lub int
. Zwykle zajmuje to mniej miejsca (w zależności od języka programowania) i pozwala na false
szybsze wykonanie pierwszego skanowania .
Oto jak / dlaczego działa algorytm.
Załóżmy, że N
liczby na liście nie są różne lub że co najmniej jedna z nich jest większa niż N
. Oznacza to, że w zakresie musi znajdować się co najmniej jedna liczba, 0 .. N - 1
której nie ma na liście. Zatem problem znalezienia najmniejszej brakującej liczby musi zatem sprowadzić się do problemu znalezienia najmniejszej brakującej liczby mniejszej niżN
. Oznacza to, że nie musimy śledzić liczb, które są większe lub równe N
... ponieważ nie będą one odpowiedzią.
Alternatywą dla poprzedniego akapitu jest to, że lista jest permutacją liczb z 0 .. N - 1
. W tym przypadku krok 3 ustawia wszystkie elementy tablicy na true
, a krok 4 mówi nam, że pierwsza „brakująca” liczba to N
.
Złożoność obliczeniowa algorytmu O(N)
charakteryzuje się stosunkowo małą stałą proporcjonalności. Wykonuje dwa liniowe przejścia przez listę lub tylko jeden przebieg, jeśli długość listy zaczyna się od. Nie ma potrzeby reprezentowania całej listy w pamięci, więc asymptotyczne użycie pamięci algorytmu jest potrzebne do reprezentowania tablicy wartości logicznych; czyli O(N)
bity.
(Z drugiej strony algorytmy, które opierają się na sortowaniu w pamięci lub partycjonowaniu, zakładają, że można przedstawić całą listę w pamięci. W formie pytania wymagałoby to O(N)
64-bitowych słów).
@Jorn komentuje, że kroki od 1 do 3 są odmianą sortowania zliczania. W pewnym sensie ma rację, ale różnice są znaczące:
- Sortowanie według liczenia wymaga tablicy (co najmniej)
Xmax - Xmin
liczników, gdzie Xmax
jest największą liczbą na liście i Xmin
najmniejszą liczbą na liście. Każdy licznik musi być w stanie reprezentować N stanów; tj. zakładając reprezentację binarną, musi mieć liczbę całkowitą (przynajmniej) ceiling(log2(N))
.
- Aby określić rozmiar tablicy, sortowanie zliczające musi wykonać wstępne przejście przez listę, aby określić
Xmax
i Xmin
.
- Dlatego też minimalna wymagana ilość miejsca w najgorszym przypadku to
ceiling(log2(N)) * (Xmax - Xmin)
bity.
Z kolei algorytm przedstawiony powyżej po prostu wymaga N
bitów w najgorszych i najlepszych przypadkach.
Jednak ta analiza prowadzi do intuicji, że gdyby algorytm przeszedł przez listę początkowo szukając zera (i licząc elementy listy, jeśli to konieczne), dałby szybszą odpowiedź, nie wykorzystując w ogóle spacji, gdyby znalazł zero. Zdecydowanie warto to zrobić, jeśli istnieje duże prawdopodobieństwo znalezienia przynajmniej jednego zera na liście. A to dodatkowe przejście nie zmienia ogólnej złożoności.
EDYCJA: Zmieniłem opis algorytmu, aby używał „tablicy wartości logicznych”, ponieważ ludzie najwyraźniej uznali mój oryginalny opis za pomocą bitów i bitmap za mylący.