Znajdź „dziurę” na liście liczb


14

Jaki jest najszybszy sposób na znalezienie pierwszej (najmniejszej) liczby całkowitej, która nie istnieje na danej liście nieposortowanych liczb całkowitych (i która jest większa niż najmniejsza wartość na liście)?

Moje prymitywne podejście polega na ich sortowaniu i przeglądaniu listy, czy jest lepszy sposób?


6
@Jodrell Myślę, że sortowanie nieskończonego postępu byłoby trudne ;-)
maple_shaft

3
@maple_shaft zgodził się, może chwilę potrwać.
Jodrell,

4
Jak zdefiniujesz najpierw nieposortowaną listę?
Jodrell

1
Właśnie zdałem sobie sprawę, że to prawdopodobnie należy do StackOverflow, ponieważ tak naprawdę nie jest to problem koncepcyjny.
JasonTrue

2
@JasonTrue Z FAQ If you have a question about… •algorithm and data structure conceptsjest na temat IMHO.
wałek klonowy

Odpowiedzi:


29

Zakładając, że masz na myśli „liczbę całkowitą”, kiedy mówisz „liczba”, możesz użyć bitvectora o rozmiarze 2 ^ n, gdzie n jest liczbą elementów (powiedzmy, że twój zakres obejmuje liczby całkowite od 1 do 256, wtedy możesz użyć 256- bit lub 32 bajt, bitvector). Gdy natrafisz na liczbę całkowitą w pozycji n swojego zakresu, ustaw n-ty bit.

Kiedy skończysz wyliczanie zbioru liczb całkowitych, iterujesz po bitach w swoim bitvektorze, szukając pozycji dowolnego zestawu bitów 0. Teraz pasują one do pozycji n brakujących liczb całkowitych.

Jest to O (2 * N), dlatego O (N) i prawdopodobnie bardziej wydajna pamięć niż sortowanie całej listy.


6
Cóż, jako bezpośrednie porównanie, jeśli wszystkie dodatnie 32-bitowe liczby całkowite bez znaku, ale 1, możesz rozwiązać problem braku liczby całkowitej w około pół gigabajta pamięci. Jeśli zamiast tego posortujesz, będziesz musiał użyć ponad 8 gigabajtów pamięci. Sortowanie, z wyjątkiem szczególnych przypadków takich jak ten (twoja lista jest sortowana, gdy masz już bitvector), prawie zawsze wynosi n log n lub gorzej, więc z wyjątkiem przypadków, w których stała przewyższa złożoność kosztów, podejście liniowe wygrywa.
JasonTrue

1
Co jeśli nie znasz zakresu a priori?
Blrfl

2
Jeśli masz liczbę całkowitą typu danych, Blrfl, z pewnością znasz maksymalne zakresy zakresu, nawet jeśli nie masz wystarczającej ilości informacji, aby zawęzić zakres. Jeśli zdarzy ci się wiedzieć, że jest to mała lista, ale nie znasz dokładnego rozmiaru, sortowanie może być prostszym rozwiązaniem.
JasonTrue

1
Lub wykonaj kolejną pętlę najpierw na liście, aby znaleźć najmniejszy i największy element. Następnie można przydzielić tablicę dokładnego rozmiaru o najmniejszej wartości jako przesunięcie podstawowe. Nadal na).
Zabezpiecz

1
@JPatrick: Nie praca domowa, biznes, ukończyłem CS lata temu :).
Fabian Zeindl

4

Jeśli najpierw posortujesz całą listę, gwarantujesz najgorszy czas działania. Ponadto wybór algorytmu sortowania ma kluczowe znaczenie.

Oto jak podejdę do tego problemu:

  1. Użyj sortowania sterty , koncentrując się na najmniejszych elementach na liście.
  2. Po każdej zamianie sprawdź, czy masz lukę.
  3. Jeśli znajdziesz lukę, to return: Znalazłeś swoją odpowiedź.
  4. Jeśli nie znajdziesz przerwy, kontynuuj zamianę.

Oto wizualizacja rodzaju sterty .


Jedno pytanie, jak rozpoznać „najmniejsze” elementy listy?
Jodrell

4

Aby być ezoterycznym i „sprytnym”, w specjalnym przypadku tablicy mającej tylko jedną „dziurę” możesz wypróbować rozwiązanie oparte na XOR:

  • Określ zasięg swojej tablicy; odbywa się to poprzez ustawienie zmiennej „max” i „min” na pierwszy element tablicy, a następnie dla każdego elementu, jeśli ten element jest mniejszy niż min lub większy niż maksimum, ustaw min lub maks na Nowa wartość.
  • Jeśli zasięg jest o jeden mniejszy niż liczność zbioru, jest tylko jeden „otwór”, więc możesz użyć XOR.
  • Zainicjuj zmienną całkowitą X na zero.
  • Dla każdej liczby całkowitej od min. Do maks. Włącznie, XOR tę wartość z X i zapisz wynik w X.
  • Teraz XOR każdą liczbę całkowitą w tablicy z X, przechowując każdy kolejny wynik do X jak poprzednio.
  • Kiedy skończysz, X ​​będzie wartością twojej „dziury”.

To będzie działać w przybliżeniu 2N czas podobny do rozwiązania bitvector, ale wymaga mniej miejsca w pamięci dla dowolnego N> sizeof (int). Jeśli jednak tablica ma wiele „otworów”, X będzie „sumą” wszystkich otworów, która będzie trudna lub niemożliwa do rozdzielenia na rzeczywiste wartości otworów. W takim przypadku powracasz do innej metody, takiej jak podejście „przestawne” lub „bitvector” z innych odpowiedzi.

Można to również powtórzyć, używając czegoś podobnego do metody przestawnej, aby jeszcze bardziej zmniejszyć złożoność. Zmień kolejność tablic w oparciu o punkt obrotu (który będzie maksimum lewej strony i min prawej strony; ustalenie maksimum i min pełnej tablicy podczas obracania będzie trywialne). Jeśli lewa strona czopa ma jeden lub więcej otworów, cofnij się tylko na tę stronę; w przeciwnym razie wróć na drugą stronę. W dowolnym momencie, w którym możesz ustalić, że jest tylko jeden otwór, użyj metody XOR, aby go znaleźć (co powinno być ogólnie tańsze niż kontynuowanie obracania się aż do zbioru dwóch elementów ze znanym otworem, który jest podstawowym przypadkiem algorytm czystego obrotu).


To absurdalnie sprytne i niesamowite! Czy możesz teraz wymyślić sposób na zrobienie tego ze zmienną liczbą dziur? :-D

2

Jaki zakres liczb napotkasz? Jeśli ten zakres nie jest bardzo duży, możesz rozwiązać ten problem za pomocą dwóch skanów (czas liniowy O (n)), używając tablicy z tyloma elementami, ile masz liczb, wymieniając przestrzeń na czas. Możesz znaleźć zakres dynamicznie za pomocą jednego skanu. Aby zmniejszyć ilość miejsca, możesz przypisać 1 bit do każdej liczby, co daje 8 wartości pamięci na bajt.

Inną opcją, która może być lepsza we wczesnych scenariuszach i byłaby insitu zamiast kopiowania pamięci, jest zmodyfikowanie sortowania wyboru, aby wyjść wcześniej, jeśli min znaleziony w przebiegu skanowania nie jest o 1 większy niż ostatnia znaleziona min.


1

Nie, nie bardzo. Ponieważ jakikolwiek jeszcze nie skanowany numer zawsze może być tym, który wypełnia daną „dziurę”, nie można uniknąć skanowania każdego numeru przynajmniej raz, a następnie porównywania go z możliwymi sąsiadami. Prawdopodobnie możesz przyspieszyć, budując drzewo binarne lub tak dalej, a następnie przesuwając je od lewej do prawej, aż do znalezienia dziury, ale jest to zasadniczo taka sama złożoność czasowa jak sortowanie, ponieważ sortuje. I prawdopodobnie nie wymyślisz niczego szybciej niż Timsort .


1
Czy mówisz, że przeglądanie listy to ta sama złożoność czasu co sortowanie?
wałek klonowy

@maple_shaft: Nie, mówię, budowanie drzewa binarnego z przypadkowych danych, a następnie przechodzenie od lewej do prawej jest równoważne sortowaniu, a następnie przechodzeniu od małego do dużego.
pillmuncher

1

Większość pomysłów to nie tylko sortowanie. Wersja bitvector to zwykły Bucketsort. Wspomniano także o rodzaju sterty. Zasadniczo sprowadza się do wyboru właściwego algorytmu sortowania, który zależy od wymagań czasowych / przestrzennych, a także od zasięgu i liczby elementów.

Moim zdaniem użycie struktury sterty jest prawdopodobnie najbardziej ogólnym rozwiązaniem (sterty zasadniczo zapewniają najmniejsze elementy skutecznie bez pełnego sortowania).

Możesz również przeanalizować podejścia, które najpierw znajdują najmniejsze liczby, a następnie skanują każdą liczbę całkowitą większą niż ta. Lub znajdziesz 5 najmniejszych liczb, mając nadzieję, że będą miały lukę.

Wszystkie te algorytmy mają swoją siłę w zależności od charakterystyki wejściowej i wymagań programu.


0

Rozwiązanie, które nie wykorzystuje dodatkowej pamięci lub nie przyjmuje szerokości (32 bitów) liczb całkowitych.

  1. W jednym przejściu liniowym znajdź najmniejszą liczbę. Nazwijmy to „min”. O (n) złożoność czasu.

  2. Wybierz losowy element przestawny i wykonaj partycję w stylu Quicksort.

  3. Jeśli element przestawny zakończył się w pozycji = („pivot” - „min”), to powtórz po prawej stronie partycji, w przeciwnym razie powtórz po lewej stronie partycji. Chodzi o to, że jeśli od początku nie ma żadnych otworów, oś obrotu znajdowałaby się w („pivot” - „min”) pozycji, więc pierwszy otwór powinien leżeć po prawej stronie przegrody i odwrotnie.

  4. Walizka podstawowa to tablica 1 elementu, a otwór znajduje się między tym elementem a następnym.

Oczekiwana złożoność całkowitego czasu pracy wynosi O (n) (8 * n ze stałymi), a najgorszym przypadkiem jest O (n ^ 2). Analiza złożoności czasowej dla podobnego problemu można znaleźć tutaj .


0

Wydaje mi się, że wpadłem na coś, co powinno działać ogólnie i wydajnie, jeśli masz gwarancję, że nie będziesz mieć duplikatów * (jednak powinno być rozszerzalne na dowolną liczbę dziur i dowolny zakres liczb całkowitych).

Pomysł tej metody jest podobny do szybkiego sortowania, polegającego na tym, że wokół niej znajduje się czop i przegroda, a następnie nawiercane po bokach z otworem. Aby zobaczyć, które strony mają otwór, znajdujemy najniższą i najwyższą liczbę i porównujemy je z osią obrotu i liczbą wartości po tej stronie. Załóżmy, że oś obrotu to 17, a minimalna liczba to 11. Jeśli nie ma otworów, powinno być 6 liczb (11, 12, 13, 14, 15, 16, 17). Jeśli jest ich 5, wiemy, że po tej stronie jest dziura i możemy po tej stronie wrócić, aby ją znaleźć. Mam problem z wyjaśnieniem tego bardziej precyzyjnie, więc weźmy przykład.

15 21 10 13 18 16 22 23 24 20 17 11 25 12 14

Sworzeń:

10 13 11 12 14 |15| 21 18 16 22 23 24 20 17 25

15 jest osią obrotu oznaczoną potokami ( ||). Po lewej stronie osi obrotu znajduje się 5 cyfr, takich jak powinno być (15–10), i 9 po prawej stronie, gdzie powinno być 10 (25–15). Tak więc powracamy po prawej stronie; zauważymy, że poprzednia granica wynosiła 15 w przypadku, gdy otwór przylega do niej (16).

[15] 18 16 17 20 |21| 22 23 24 25

Teraz po lewej stronie są 4 liczby, ale powinno być 5 (21–16). Wracamy więc tam i ponownie zanotujemy poprzednią granicę (w nawiasach).

[15] 16 17 |18| 20 [21]

Lewa strona ma poprawne 2 liczby (18–16), ale prawa ma 1 zamiast 2 (20–18). W zależności od naszych warunków końcowych moglibyśmy porównać liczbę 1 z dwiema stronami (18, 20) i zobaczyć, czy brakuje 19 lub powtórzyć jeszcze raz:

[18] |20| [21]

Lewa strona ma rozmiar zero, z odstępem między czopem (20) a poprzednim obrzeżem (18), więc 19 to otwór.

*: Jeśli są duplikaty, prawdopodobnie możesz użyć zestawu skrótów, aby usunąć je w czasie O (N), zachowując ogólną metodę O (N), ale może to zająć więcej czasu niż użycie innej metody.


1
Nie sądzę, żeby OP wspominał o istnieniu tylko jednej dziury. Dane wejściowe to nieposortowana lista liczb - mogą to być cokolwiek. Z opisu nie wynika jasno, jak określić, ile liczb powinno „być”.
Caleb

@caleb Nie ma znaczenia, ile jest otworów, po prostu nie ma duplikatów (które można usunąć w O (N) za pomocą zestawu skrótów, choć w praktyce może to mieć więcej narzutu niż w przypadku innych metod). Próbowałem poprawić opis, zobacz, czy jest lepszy.
Kevin,

To nie jest liniowe, IMO. To bardziej jak (logN) ^ 2. Na każdym kroku przestawiasz podzbiór kolekcji, na której ci zależy (połowa poprzedniej podtablicy, którą zidentyfikowałeś jako posiadającą pierwszą „dziurę”), a następnie wskakujesz w lewą stronę, jeśli ma „dziurę”, lub prawa strona, jeśli lewa nie. (logN) ^ 2 jest wciąż lepszy niż liniowy; jeśli N wzrośnie dziesięciokrotnie, przyjmujesz tylko rząd 2 (log (N) -1) + 1 dodatkowy krok.
KeithS

@Keith - niestety, musisz spojrzeć na wszystkie liczby na każdym poziomie, aby je przestawić, więc zajmie to około n + n / 2 + n / 4 + ... = 2n (technicznie, 2 (nm)) porównań .
Kevin
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.