Ciekawe pytanie do wywiadu, którego używa mój kolega:
Załóżmy, że otrzymujesz bardzo długą, nieposortowaną listę 64-bitowych liczb całkowitych bez znaku. Jak znaleźć najmniejszą nieujemną liczbę całkowitą, która nie występuje na liście?
KONTROLA: Teraz, gdy zaproponowano oczywiste rozwiązanie polegające na sortowaniu, czy możesz to zrobić szybciej niż O (n log n)?
DALSZE INFORMACJE: Twój algorytm musi działać na komputerze z, powiedzmy, 1 GB pamięci
WYJAŚNIENIE: lista znajduje się w pamięci RAM, chociaż może zużywać jej dużo. Rozmiar listy zostanie podany z góry, powiedzmy N.
Odpowiedzi:
Jeśli struktura danych może być zmutowana na miejscu i obsługuje dostęp swobodny, możesz to zrobić w czasie O (N) i O (1) dodatkowej przestrzeni. Po prostu przejrzyj tablicę sekwencyjnie i dla każdego indeksu zapisz wartość w indeksie do indeksu określonego przez wartość, rekurencyjnie umieszczając dowolną wartość w tym miejscu na swoim miejscu i odrzucając wartości> N. Następnie ponownie przejdź przez tablicę w poszukiwaniu miejsca gdzie wartość nie pasuje do indeksu - to najmniejsza wartość spoza tablicy. Daje to co najwyżej 3N porównań i wykorzystuje tylko kilka wartości wartych tymczasowej przestrzeni.
źródło
Oto proste
O(N)
rozwiązanie, które wykorzystujeO(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.N
.N
wartości logicznych, zainicjowaną dla wszystkichfalse
.X
na liście, jeśliX
jest mniejsza niżN
, ustawX'th
element tablicy natrue
.0
, szukając pierwszego elementufalse
. Jeśli znajdziesz pierwszyfalse
w indeksieI
, toI
jest odpowiedź. W przeciwnym razie (tj. Gdy wszystkie elementy sątrue
) odpowiedź brzmiN
.W praktyce „tablica
N
wartości logicznych” byłaby prawdopodobnie zakodowana jako „mapa bitowa” lub „zestaw bitów” reprezentowana jako tablica abyte
lubint
. Zwykle zajmuje to mniej miejsca (w zależności od języka programowania) i pozwala nafalse
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ówneN
... 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 natrue
, a krok 4 mówi nam, że pierwsza „brakująca” liczba toN
.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; czyliO(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:
Xmax - Xmin
liczników, gdzieXmax
jest największą liczbą na liście iXmin
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))
.Xmax
iXmin
.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.
źródło
bool[]
pomocą mapy bitowej lub za pomocą mapy bitowej, nie ma znaczenia dla ogólnego rozwiązania.Ponieważ OP określił teraz, że oryginalna lista jest przechowywana w pamięci RAM, a komputer ma tylko, powiedzmy, 1 GB pamięci, zamierzam wyjść na skraj i przewidzieć, że odpowiedź wynosi zero.
1 GB pamięci RAM oznacza, że lista może zawierać maksymalnie 134 217 728 numerów. Ale jest 2 64 = 18 446 744 073 709 551 616 możliwych liczb. Zatem prawdopodobieństwo, że zero znajduje się na liście, wynosi 1 do 137.438.953.472.
Natomiast moje szanse na porażenie piorunem w tym roku wynoszą 1 na 700 000. A moje szanse na trafienie przez meteoryt wynoszą około 1 na 10 bilionów. Więc jestem około dziesięć razy bardziej prawdopodobne, że zostanę napisany w czasopiśmie naukowym z powodu mojej przedwczesnej śmierci przez ciało niebieskie, niż odpowiedź niezerowa.
źródło
Jak wskazano w innych odpowiedziach, możesz zrobić sortowanie, a następnie po prostu skanować, aż znajdziesz lukę.
Możesz zwiększyć złożoność algorytmiczną do O (N) i zachować miejsce O (N), używając zmodyfikowanego QuickSort, w którym eliminujesz partycje, które nie są potencjalnymi kandydatami do wypełnienia luki.
Oszczędza to dużą liczbę obliczeń.
źródło
Aby zilustrować jedną z pułapek
O(N)
myślenia, otoO(N)
algorytm wykorzystującyO(1)
przestrzeń.źródło
Ponieważ wszystkie liczby mają 64 bity, możemy na nich zastosować sortowanie radix , czyli O (n). Sortuj je, a następnie skanuj, aż znajdziesz to, czego szukasz.
jeśli najmniejsza liczba to zero, przeszukaj do przodu, aż znajdziesz przerwę. Jeśli najmniejsza liczba nie jest zerem, odpowiedź wynosi zero.
źródło
Aby uzyskać metodę efektywną przestrzennie, a wszystkie wartości są różne, możesz to zrobić w
O( k )
czasie i przestrzeniO( k*log(N)*N )
. Zajmuje mało miejsca i nie wymaga przenoszenia danych, a wszystkie operacje są elementarne (dodawanie odejmowania).U = N; L=0
k
regiony. Lubię to:0->(1/k)*(U-L) + L
,0->(2/k)*(U-L) + L
,0->(3/k)*(U-L) + L
...0->(U-L) + L
count{i}
) znajduje się w każdym regionie. (N*k
kroki)h
), który nie jest pełny. To znaczycount{h} < upper_limit{h}
. (k
kroki)h - count{h-1} = 1
masz odpowiedźU = count{h}; L = count{h-1}
można to poprawić za pomocą haszowania (dzięki Nicowi za ten pomysł).
k
regiony. Lubię to:L + (i/k)->L + (i+1/k)*(U-L)
inc count{j}
za pomocąj = (number - L)/k
(if L < number < U)
h
), który nie ma w sobie k elementówcount{h} = 1
h jest twoją odpowiedziąU = maximum value in region h
L = minimum value in region h
To się pojawi
O(log(N)*N)
.źródło
U-L < k
Po prostu posortuję je, a następnie przeglądam sekwencję, aż znajdę lukę (w tym przerwę na początku między zerem a pierwszą liczbą).
Jeśli chodzi o algorytm, zrobiłoby to coś takiego:
Oczywiście, jeśli masz dużo więcej pamięci niż CPU, możesz utworzyć maskę bitową wszystkich możliwych 64-bitowych wartości i po prostu ustawić bity dla każdej liczby na liście. Następnie poszukaj pierwszego 0-bitowego w tej masce bitowej. To zmienia go w operację O (n) pod względem czasu, ale dość cholernie kosztowną pod względem wymagań dotyczących pamięci :-)
Wątpię, czy mógłbyś poprawić O (n), ponieważ nie widzę sposobu, aby to zrobić, który nie wymaga spojrzenia na każdą liczbę przynajmniej raz.
Algorytm dla tego byłby następujący:
źródło
Posortuj listę, spójrz na pierwszy i drugi element i zacznij wspinać się w górę, aż pojawi się luka.
źródło
Możesz to zrobić w czasie O (n) i O (1) dodatkowej przestrzeni, chociaż ukryty czynnik jest dość duży. Nie jest to praktyczny sposób rozwiązania problemu, ale może być interesujący.
Dla każdej 64-bitowej liczby całkowitej bez znaku (w porządku rosnącym) iteruj po liście, aż znajdziesz docelową liczbę całkowitą lub dojdziesz do końca listy. Jeśli dojdziesz do końca listy, docelową liczbą całkowitą jest najmniejsza liczba całkowita, której nie ma na liście. Jeśli dojdziesz do końca 64-bitowych liczb całkowitych, każda 64-bitowa liczba całkowita znajduje się na liście.
Tutaj jest to funkcja Pythona:
Ta funkcja jest celowo nieefektywna, aby utrzymać ją O (n). Zwróć szczególną uwagę, że funkcja sprawdza docelowe liczby całkowite nawet po znalezieniu odpowiedzi. Jeśli funkcja zwróciłaby się zaraz po znalezieniu odpowiedzi, liczba uruchomień zewnętrznej pętli byłaby ograniczona rozmiarem odpowiedzi, która jest ograniczona przez n. Ta zmiana spowodowałaby, że czas wykonywania O (n ^ 2) byłby dużo szybszy.
źródło
Dziękuję egon, swilden i Stephenowi C za inspirację. Po pierwsze, znamy granice wartości celu, ponieważ nie może być ona większa niż rozmiar listy. Ponadto lista o rozmiarze 1 GB może zawierać maksymalnie 134217728 (128 * 2 ^ 20) 64-bitowych liczb całkowitych.
Hashing part
Proponuję użyć haszowania, aby radykalnie zmniejszyć naszą przestrzeń wyszukiwania. Najpierw pierwiastek kwadratowy z wielkości listy. W przypadku listy 1 GB to N = 11 586. Skonfiguruj tablicę liczb całkowitych o rozmiarze N. Powtarzaj listę i weź pierwiastek kwadratowy * z każdej liczby znalezionej jako hash. W swojej tabeli skrótów zwiększ licznik dla tego skrótu. Następnie wykonaj iterację w swojej tabeli skrótów. Pierwszy znaleziony zasobnik, który nie jest równy maksymalnemu rozmiarowi, definiuje nową przestrzeń wyszukiwania.
Część bitmapy
Teraz skonfiguruj zwykłą mapę bitową równą rozmiarowi nowej przestrzeni wyszukiwania i ponownie przejrzyj listę źródeł, wypełniając bitmapę, gdy znajdziesz każdą liczbę w swojej przestrzeni wyszukiwania. Kiedy skończysz, pierwszy nieustawiony bit w twojej mapie bitowej da ci odpowiedź.
Zostanie to zakończone w czasie O (n) i przestrzeni O (sqrt (n)).
(* Możesz użyć czegoś w rodzaju przesunięcia bitowego, aby zrobić to znacznie wydajniej, i po prostu odpowiednio dostosuj liczbę i rozmiar wiader.)
źródło
Cóż, jeśli na liście liczb brakuje tylko jednej liczby, najłatwiejszym sposobem znalezienia brakującej liczby jest zsumowanie serii i odjęcie każdej wartości z listy. Ostateczna wartość to brakująca liczba.
źródło
źródło
Moglibyśmy użyć tablicy haszującej do przechowywania liczb. Gdy wszystkie liczby zostaną wykonane, uruchom licznik od 0, aż znajdziemy najniższą. Dość dobry hash będzie haszował i będzie przechowywany w stałym czasie oraz będzie pobierany w stałym czasie.
Najgorszy przypadek, jeśli
n
w tablicy znajdują się elementy i{0, 1, ... n-1}
w takim przypadku odpowiedź zostanie uzyskana pod adresemn
, nadal ją zachowującO(n)
.źródło
Oto moja odpowiedź napisana w Javie:
Podstawowy pomysł: 1- Zapętlaj się przez tablicę, wyrzucając zduplikowane liczby dodatnie, zerowe i ujemne, jednocześnie sumując resztę, uzyskując również maksymalną liczbę dodatnią i zachowaj unikalne liczby dodatnie na mapie.
2- Oblicz sumę jako max * (max + 1) / 2.
3- Znajdź różnicę między sumami obliczonymi w krokach 1 i 2
4- Zapętl ponownie od 1 do minimum [sumy różnicy, maks.] I zwróć pierwszą liczbę, której nie ma na mapie wypełnionej w kroku 1.
źródło
Jak sprytnie zauważył Stephen C, odpowiedzią musi być liczba mniejsza niż długość tablicy. Wtedy znalazłbym odpowiedź za pomocą wyszukiwania binarnego. To optymalizuje najgorszy przypadek (więc ankieter nie może złapać cię na patologicznym scenariuszu „co by było, gdyby”). W wywiadzie zwróć uwagę, że robisz to, aby zoptymalizować się pod kątem najgorszego przypadku.
Sposób korzystania z wyszukiwania binarnego polega na odjęciu szukanej liczby od każdego elementu tablicy i sprawdzeniu wyników ujemnych.
źródło
Podoba mi się podejście „zgadnij zero”. Jeśli liczby byłyby losowe, zero jest wysoce prawdopodobne. Jeśli „egzaminator” ustawił nielosową listę, dodaj jedną i zgadnij ponownie:
Najgorszym przypadkiem jest n * N gdzie n = N, ale w praktyce n jest bardzo prawdopodobne, że będzie małą liczbą (np. 1)
źródło
Nie jestem pewien, czy dostałem pytanie. Ale jeśli dla listy 1, 2, 3, 5, 6 i brakującą liczbą jest 4, to brakującą liczbę można znaleźć w O (n) przez: (n + 2) (n + 1) / 2- (n + 1) nie / 2
EDYCJA: przepraszam, myślę, że myślałem zbyt szybko ostatniej nocy. W każdym razie drugą część należy właściwie zastąpić sumą (listą), czyli miejscem, w którym występuje O (n). Formuła ujawnia ideę: dla n kolejnych liczb całkowitych suma powinna wynosić (n + 1) * n / 2. Jeśli brakuje liczby, suma byłaby równa sumie (n + 1) kolejnych liczb całkowitych minus brakująca liczba.
Dziękuję za zwrócenie uwagi na fakt, że myślę o środkowych fragmentach.
źródło
Dobra robota Ants Aasma! Myślałem o odpowiedzi przez około 15 minut i samodzielnie wymyśliłem odpowiedź w podobnym tonie myślenia do twojego:
m reprezentuje "bieżące maksymalne możliwe wyjście, biorąc pod uwagę to, co wiem o pierwszych wejściach i i nie zakładając nic więcej o wartościach aż do wejścia na m-1".
Ta wartość m zostanie zwrócona tylko wtedy, gdy (a [i], ..., a [m-1]) jest permutacją wartości (i, ..., m-1). Zatem jeśli a [i]> = m lub jeśli a [i] <i lub jeśli a [i] == a [a [i]] wiemy, że m to niewłaściwe wyjście i musi być co najmniej o jeden element niższe. Zatem zmniejszając m i zamieniając a [i] na a [m] możemy powtórzyć.
Jeśli to nie jest prawda, ale a [i]> i wtedy wiedząc, że a [i]! = A [a [i]] wiemy, że zamiana a [i] na a [a [i]] zwiększy liczbę elementów na swoim miejscu.
W przeciwnym razie a [i] musi być równe i, w którym to przypadku możemy inkrementować i, wiedząc, że wszystkie wartości do tego indeksu włącznie są równe ich indeksowi.
Dowód, że nie może to wejść w nieskończoną pętlę, pozostaje jako ćwiczenie dla czytelnika. :)
źródło
Dafny fragment z odpowiedziami pokazy mrówki dlaczego algorytm w miejscu może zakończyć się niepowodzeniem.
requires
Warunek opisuje, że wartości poszczególnych pozycji nie może wykraczać poza granice tablicy.Wklej kod do walidatora z
forall ...
klauzulą i bez niej , aby zobaczyć błąd weryfikacji. Drugi błąd jest wynikiem tego, że weryfikator nie jest w stanie ustalić warunku zakończenia pętli Pass 1. Udowodnienie tego należy do kogoś, kto lepiej rozumie narzędzie.źródło
Oto odpowiedź w Javie, która nie modyfikuje danych wejściowych i używa czasu O (N) i N bitów oraz niewielkiego stałego narzutu pamięci (gdzie N to rozmiar listy):
źródło
Otrzymałem 100% za powyższe rozwiązanie.
źródło
1) Filtruj negatyw i zero
2) Sortuj / wyraźne
3) Odwiedź tablicę
Złożoność : O (N) lub O (N * log (N))
używając Java8
źródło
Unordered_set może służyć do przechowywania wszystkich liczb dodatnich, a następnie możemy iterować od 1 do długości unordered_set i zobaczyć pierwszą liczbę, która nie występuje.
źródło
Rozwiązanie za pomocą podstawowego javascript
var a = [1, 3, 6, 4, 1, 2]; function findSmallest(a) { var m = 0; for(i=1;i<=a.length;i++) { j=0;m=1; while(j < a.length) { if(i === a[j]) { m++; } j++; } if(m === 1) { return i; } } } console.log(findSmallest(a))
Mam nadzieję, że to pomoże komuś.
źródło
W przypadku Pythona nie jest to najbardziej wydajne, ale poprawne
źródło
źródło
to może pomóc:
źródło