Szybki algorytm wyszukiwania posortowanej tablicy liczb zmiennoprzecinkowych w celu znalezienia pary liczb zmiennopozycyjnych w nawiasach wejściowych

10

Mam tablicę liczb zmiennoprzecinkowych, posortowanych od najmniejszej do największej, i muszę być w stanie wybrać najbliższy zmiennoprzecinkowy większy lub mniejszy od przekazanej wartości wejściowej. Ta wartość wejściowa niekoniecznie występuje jako wartość w tablicy.

Naiwnym podejściem byłoby proste przeszukiwanie liniowe tablicy. Może to wyglądać tak:

void FindClosestFloatsInArray( float input, std::vector<float> array, 
                               float *min_out, float *max_out )
{
    assert( input >= array[0] && input < array[ array.size()-1 ] );
    for( int i = 1; i < array.size(); i++ )
    {
        if ( array[i] >= input )
        {
            *min = array[i-1];
            *max = array[i];
        }
    }
}

Ale oczywiście w miarę powiększania się tablicy, będzie to coraz wolniejsze.

Czy ktoś ma pomysł na algorytm, który pozwoliłby mi znaleźć te dane bardziej optymalnie? Już przeszedłem na wyszukiwanie binarne, które nieco poprawiło sytuację, ale wciąż jest o wiele wolniejsze niż bym chciał, a ponieważ tak naprawdę nie szukam konkretnej wartości, która istnieje w tablicy, nigdy nie może się zakończyć wcześnie.

Więcej informacji: Wartości zmiennoprzecinkowe w tablicy niekoniecznie są rozmieszczone równomiernie (tzn. Tablica może składać się z wartości „1.f, 2.f, 3.f, 4.f, 100.f, 1200.f , 1203.f, 1400.f ”.

Robię tę operację setki tysięcy razy, ale mogę wykonać dowolne przetwarzanie wstępne na tablicy pływaków, jeśli poprawi to czas wyszukiwania. Absolutnie mogę zmienić, aby użyć czegoś innego niż wektor do ich przechowywania, jeśli to pomoże.

Trevor Powell
źródło
Co sprawia, że ​​myślisz, że twoje wyszukiwanie binarne nie może zakończyć się wcześniej? Na pewno możesz po prostu przetestować elementy w i i i + 1, aby sprawdzić, czy obejmują one wartość docelową, i zakończyć, jeśli tak się dzieje?
Paul R
Alternatywnie mogłem przetestować elementy w punktach i oraz i-1, aby sprawdzić, czy obejmują one wartość docelową. Musiałbym również sprawdzić, czy „i” było> = array.size () - 1, aby uniknąć wykonywania testu, i czy było to <= 0, więc mogłem uniknąć wykonywania testu ... to w rzeczywistości dużo dodatkowe warunki warunkowe do wykonania na każdym kroku, w celu sprawdzenia wczesnego wyjścia. Wyobrażam sobie, że znacznie spowolnią algorytm, choć przyznam się, że jeszcze go nie profilowałem.
Trevor Powell,
3
Nie musi być tak skomplikowane - jeśli twoja tablica ma rozmiar N, musisz po prostu potraktować ją tak, jakby miała rozmiar N - 1. W ten sposób zawsze jest poprawny element w i + 1. binarne wyszukiwanie elementu N - 1 dla elementu i, który jest mniejszy od wartości docelowej, przy czym element i + 1 jest większy niż wartość docelowa.
Paul R

Odpowiedzi:

11

Kod w pytaniu (wyszukiwanie liniowe), jak słusznie zauważyłeś, zwolni się w przypadku dużych tablic pływających. Technicznie jest to O (n), gdzie n jest liczbą wartości zmiennoprzecinkowych w tablicy.

Ogólnie rzecz biorąc, najlepszym sposobem na znalezienie wartości w uporządkowanej tablicy jest pewnego rodzaju rekurencyjne wyszukiwanie drzewa (np. Wyszukiwanie binarne), w którym to przypadku można osiągnąć czas wyszukiwania O (log n) w liczbie elementów w twojej tablicy. O (log n) jest znacznie lepsze niż O (n) dla dużych wartości n.

Dlatego moim sugerowanym podejściem byłoby proste wyszukiwanie binarne tablicy , tj .:

  1. Ustaw indeksy liczb całkowitych min / maks, aby pokryć całą tablicę zmiennoprzecinkową
  2. sprawdzić wartość pośrodku zakresu przy indeksie mid = (min + max / 2) względem wartości wyszukiwania x
  3. jeśli x jest niższy od tej wartości, ustaw maks. na środkową wartość, w przeciwnym razie ustaw min. na średnią
  4. powtarzaj (2-4), aż znajdziesz prawidłową wartość

Jest to algorytm O (log n), który powinien być wystarczająco szybki dla prawie wszystkich sytuacji. Intuicyjnie działa poprzez zmniejszenie o połowę zakresu, który ma być przeszukiwany na każdym kroku, aż do znalezienia właściwej wartości.

Naprawdę trudno jest przeforsować proste wyszukiwanie binarne, więc jeśli już poprawnie to zaimplementowałeś, możesz być już prawie optymalny. Jeśli jednak znasz rozkłady danych i / lub masz ograniczony zakres wartości odnośników (x), możesz spróbować jeszcze innych, bardziej zaawansowanych sztuczek:

  • Wiadro - twórz wiadra (np. Dla każdego przedziału między dwiema liczbami całkowitymi), z których każdy zawiera mniejszą posortowaną listę wartości zmiennoprzecinkowych między dwiema liczbami całkowitymi graniczącymi oraz dwie wartości bezpośrednio poniżej i bezpośrednio powyżej każdego zakresu. Następnie możesz rozpocząć wyszukiwanie w (trunc (x) +0,5). To powinno dać ci dobre przyspieszenie, jeśli wybierzesz odpowiednio duże wiadra (to skutecznie zwiększa współczynnik rozgałęzienia drzewa .....). Jeśli liczby całkowite nie działają dla Ciebie, możesz wypróbować segmenty o innej precyzji w punkcie stałym (np. Wielokrotności 1/16).
  • Mapowanie bitów - jeśli zakres możliwych wartości odnośników jest wystarczająco mały, możesz spróbować stworzyć dużą tablicę odnośników indeksowaną wartością bitową x. Będzie to O (1), ale możesz potrzebować dużo pamięci, która będzie bardzo nieprzyjazna dla twojej pamięci podręcznej ... więc używaj jej ostrożnie. Jest to szczególnie nieprzyjemne, ponieważ szukasz wartości zmiennoprzecinkowych, więc możesz potrzebować kilku GB, aby uwzględnić wszystkie mniej znaczące bity ......
  • Zaokrąglanie i mieszanie - tabele skrótów prawdopodobnie nie są najlepszą strukturą danych dla tego problemu, ale jeśli możesz przeżyć utratę dokładności, mogą one działać - wystarczy zaokrąglić najniższe bity wartości wyszukiwania i użyć skrótu, aby bezpośrednio wyszukać poprawna wartość. Będziesz musiał eksperymentować z właściwym kompromisem między wielkością mapy a precyzją, a także upewnić się, że wszystkie możliwe wartości skrótu są wypełnione, więc może to być nieco trudne ......
  • Balansowanie drzew - twoje idealne drzewo powinno mieć 50% szansy na przejście w lewo lub w prawo. Jeśli więc utworzysz drzewo na podstawie rozkładu wartości odnośników (x), możesz zoptymalizować drzewo, aby uzyskać odpowiedzi przy minimalnej liczbie testów. Jest to prawdopodobnie dobre rozwiązanie, jeśli wiele wartości w tablicy pływającej jest bardzo blisko siebie, ponieważ pozwoli ci to zbyt często przeszukiwać te gałęzie.
  • Drzewa krytyczne - nadal są to drzewa (więc nadal O (log n) ...), ale w niektórych przypadkach: jednak aby przekształcenia działały, trzeba by było przekonwertować zmiennoprzecinkowe na format o stałym punkcie.

Jednak, chyba że znajdujesz się w wyjątkowej sytuacji, prawdopodobnie zaleciłbym trzymanie się prostego wyszukiwania binarnego. Powody:

  • jest o wiele łatwiejszy do wdrożenia
  • jest bardzo szybki w większości przypadków
  • dodatkowe koszty bardziej złożonych podejść (np. wyższe zużycie pamięci / nacisk pamięci podręcznej) często przewyższają niewielkie zyski teoretyczne
  • będzie bardziej odporny na przyszłe zmiany w dystrybucji danych ....
mikera
źródło
1

Wydaje się to dość proste:

Wykonaj binarne wyszukiwanie pływaka, który chcesz powiązać - czas O (log n).

Następnie element po jego lewej stronie jest dolną granicą, a element po prawej to górna granica.

Ankit Soni
źródło
0

Oczywistą odpowiedzią jest przechowywanie pływaków na drzewie . Obsługa „poprzednich” i „następnych” operacji jest banalna w drzewie. Więc po prostu zrób „następną” wartość, a następnie zrób „poprzednią” wartość, którą znajdziesz w pierwszym kroku.

David Schwartz
źródło
1
Jest to zasadniczo to samo, co wyszukiwanie binarne.
kevin cline
-1

Ten artykuł („wyszukiwanie sublogarytmiczne bez zwielokrotnienia”) może być interesujący; zawiera nawet kod źródłowy. Dla celów porównania można traktować liczbę zmiennoprzecinkową jako liczbę całkowitą o tym samym wzorze bitowym; był to jeden z celów projektowych standardu zmiennoprzecinkowego IEEE.

zvrba
źródło