Dlaczego ktoś miałby używać set zamiast unordered_set?


145

Wprowadzamy C ++ 0x, unordered_setktóry jest dostępny w boostwielu innych miejscach. Rozumiem, że unordered_setjest to tabela skrótów ze O(1)złożonością wyszukiwania. Z drugiej strony setto nic innego jak drzewo o log(n)złożoności wyszukiwania. Dlaczego, u licha, ktoś miałby używać setzamiast unordered_set? tj. czy jest już taka potrzeba set?


22
Twoje pytanie zasadniczo dotyczy tego, czy jest już potrzebne drzewo.
Vinko Vrsalovic

2
Myślę, że w pierwszej linijce wyraźnie powiedziałem, że jest to jakoś głupie pytanie. Coś mi brakowało i teraz dostałem odpowiedź :)
AraK

2
Prawdziwym powodem jest to, że rzeczy nie są tak czarno-białe, jak się wydaje. Pomiędzy nimi jest dużo szarości i innych kolorów. Musisz pamiętać, że te kontenery to narzędzia. Czasami wydajność nie jest kluczowa, a wygoda ma znacznie większe znaczenie. Gdyby wszyscy szukali najbardziej wydajnego rozwiązania, nigdy nie
używalibyśmy

(Dlaczego, do licha, ktokolwiek miałby używać ogólnej nazwy dla implementacji / interfejsu z obietnicami wykraczającymi poza te, które sugeruje ta nazwa, tworząc niezręczną sytuację dla osób bez?)
Greybeard

Odpowiedzi:


219

Kiedy dla kogoś, kto chce iterować elementy zestawu, kolejność ma znaczenie.


Czy jest uporządkowany zgodnie z kolejnością reklamową, czy według rzeczywistego porównania przy użyciu operatorów < >?
SomethingSomething

2
Domyślnie jest sortowany przy użyciu std :: less; możesz to zmienić i podać własny operator porównania. cplusplus.com/reference/set/set
moonshadow

Lub czasami, gdy chcesz tylko iterować, nawet jeśli kolejność nie ma znaczenia.
mfnx

319

Nieuporządkowane zestawy muszą płacić za średni czas dostępu O (1) na kilka sposobów:

  • setzużywa mniej pamięci niż unordered_setprzechowywanie tej samej liczby elementów.
  • W przypadku niewielkiej liczby elementów wyszukiwania w a setmogą być szybsze niż wyszukiwania w unordered_set.
  • Mimo, iż wiele operacji szybciej w średnim przypadku dla unordered_set, często są one gwarantowane mieć lepsze najgorszym przypadku zawiłości dla set(na przykładinsert ).
  • To set sortuje elementy jest przydatne, jeśli chcesz uzyskać do nich dostęp w kolejności.
  • Można leksykograficznie porównać różne setsz <, <=, >i >=. unordered_setnie są wymagane do obsługi tych operacji.


9
+1, wszystkie doskonałe punkty. Ludzie często przeoczają fakt, że tabele skrótów mają średni czas dostępu O (1) , co oznacza, że ​​czasami mogą mieć duże opóźnienia. To rozróżnienie może być ważne w przypadku systemów czasu rzeczywistego.
j_random_hacker

Dobre punkty, jednak tutaj ( en.cppreference.com/w/cpp/container/unordered_set/operator_cmp ) stwierdzono, że możemy porównać unordered_sets.
Michiel uit het Broek

5
Zdefiniuj „małą liczbę elementów”
Sunjay Varma

4
@SunjayVarma zwykle 100 elementów to dobre odcięcie między nimi. W razie wątpliwości nic nie może zastąpić testowania obu testów w Twoim konkretnym przypadku użycia.
Nate

3
@MichieluithetBroek Podawane jest tylko porównanie równości, a nie zamawianie ( <).
lisyarus

26

Zawsze, gdy wolisz drzewo od tabeli skrótów.

Na przykład tabele skrótów mają w najgorszym przypadku wartość „O (n)”. O (1) to przeciętny przypadek. W najgorszym przypadku drzewa są „O ( log n)”.


18
/ Zrównoważone / drzewa mają wartość O (ln n) w najgorszym przypadku. Możesz otrzymać O (n) drzew (zasadniczo połączone listy).
strager

5
Jeśli potrafisz napisać w miarę inteligentną funkcję haszującą, prawie zawsze możesz uzyskać O (1) perf z tablicy haszującej. Jeśli nie możesz napisać takiej funkcji haszującej, jeśli chcesz iterować „po kolei” po swoim zestawie, powinieneś użyć drzewa. Ale nie powinieneś używać drzewa, ponieważ boisz się „O (n) najgorszego przypadku”.
Justin L.

6
stager: Aby być pedantycznym, tak. Jednak mówimy o zestawie w C ++, który jest zwykle implementowany jako zrównoważone drzewo wyszukiwania binarnego . Powinniśmy byli określić rzeczywistą operację, aby mówić o złożoności. W tym kontekście jest oczywiste, że mówimy o wyszukiwaniu.
Mehrdad Afshari

1
Justin L: To tylko jeden z powodów, dla których wolisz drzewo. Podstawą mojej odpowiedzi jest pierwsza linijka. Zawsze , gdy wolisz strukturę danych w postaci drzewa od tabeli skrótów. Istnieje wiele przypadków, w których drzewa są preferowane niż tabele skrótów. Tabele z skrótami są szczególnie słabe w takich rzeczach jak „przecięcia zakresów”.
Mehrdad Afshari

2
Drzewa stl są prawie powszechnie stosowanymi czerwono-czarnymi drzewami, zaawansowanym drzewem samobalansującym. Naprawdę są przypadki, w których O (n) szukanie w gorszym przypadku jest nie do przyjęcia. Usługa internetowa, która zapewnia i interfejs do przechowywania wartości użytkownika, nie powinna używać mapy skrótów, ponieważ złośliwy użytkownik może skutecznie utworzyć DoS, przechowując specjalnie spreparowane wartości. Krytyczne, wrażliwe na czas systemy mogą również nie pozwalać na wyszukiwanie O (n), kontrolę ruchu lotniczego itp. Chociaż ogólnie masz rację, używaj map skrótów domyślnie i przełączaj wersję drzewa tylko wtedy, gdy masz rzeczywistą potrzebę.
deft_code

14

Użyj zestawu, gdy:

  1. Potrzebujemy uporządkowanych danych (odrębnych elementów).
  2. Musielibyśmy wydrukować / uzyskać dostęp do danych (w posortowanej kolejności).
  3. Potrzebujemy poprzednika / następcy elementów.

Użyj unordered_set, gdy:

  1. Musimy zachować zestaw odrębnych elementów i nie jest wymagana żadna kolejność.
  2. Potrzebujemy dostępu do pojedynczego elementu, tj. Bez przechodzenia.

Przykłady:

zestaw:

Wejście: 1, 8, 2, 5, 3, 9

Wyjście: 1, 2, 3, 5, 8, 9

Unordered_set:

Wejście: 1, 8, 2, 5, 3, 9

Wyjście: 9 3 1 8 2 5 (może ta kolejność, na którą ma wpływ funkcja skrótu)

Głównie różnica:

wprowadź opis obrazu tutaj

Uwaga: (w niektórych przypadkach setjest to wygodniejsze) na przykład użycie vectorjako klucza

set<vector<int>> s;
s.insert({1, 2});
s.insert({1, 3});
s.insert({1, 2});

for(const auto& vec:s)
    cout<<vec<<endl;   // I have override << for vector
// 1 2
// 1 3 

Powodem, dla którego vector<int>może być kluczem setbo vectorręcznym operator<.

Ale jeśli używasz unordered_set<vector<int>>, musisz utworzyć funkcję skrótu dla vector<int>, ponieważ wektor nie ma funkcji skrótu, więc musisz zdefiniować taką:

struct VectorHash {
    size_t operator()(const std::vector<int>& v) const {
        std::hash<int> hasher;
        size_t seed = 0;
        for (int i : v) {
            seed ^= hasher(i) + 0x9e3779b9 + (seed<<6) + (seed>>2);
        }
        return seed;
    }
};

vector<vector<int>> two(){
    //unordered_set<vector<int>> s; // error vector<int> doesn't  have hash function
    unordered_set<vector<int>, VectorHash> s;
    s.insert({1, 2});
    s.insert({1, 3});
    s.insert({1, 2});

    for(const auto& vec:s)
        cout<<vec<<endl;
    // 1 2
    // 1 3
}

widać, że w niektórych przypadkach unordered_setjest to bardziej skomplikowane.

Cytowane głównie z: https://www.geeksforgeeks.org/set-vs-unordered_set-c-stl/ https://stackoverflow.com/a/29855973/6329006


6

Ponieważ std :: set jest częścią Standard C ++, a unordered_set nie jest. C ++ 0x NIE jest standardem, podobnie jak Boost. Dla wielu z nas przenośność jest niezbędna, a to oznacza trzymanie się standardów.


2
Jeśli dobrze go rozumiem, nie pyta, dlaczego ludzie nadal używają zestawu. Informuje się o C ++ 0x.
Johannes Schaub - litb

2
Może. Myślałem, że wszyscy wiedzą, że tablice skrótów i drzewa rozwiązują różne problemy.

21
Cóż, teraz to standard (zajęło to tylko kilka lat)
Clayton Hughes

6

Rozważmy algorytmy linii przebiegu. Algorytmy te całkowicie zawiodłyby w przypadku tablic mieszających, ale działają pięknie ze zrównoważonymi drzewami. Aby dać ci konkretny przykład algorytmu linii losowania, rozważ algorytm fortuny. http://en.wikipedia.org/wiki/Fortune%27s_algorithm


1
Myślę, że takie odniesienie jest zbyt złożone, biorąc pod uwagę pytanie. (Musiałem to sprawdzić)
hektorpalny

3

Jeszcze jedno, oprócz tego, o czym wspomniały już inne osoby. A oczekiwane zamortyzowany złożoność do wprowadzania elementu do unordered_set O (1), co jakiś czas, a następnie będzie się O (N), ponieważ wymaga mieszającego stołowych restrukturyzowany (liczby segmentów musi zmienić) - nawet „dobra” funkcja skrótu. Podobnie jak wstawianie elementu do wektora wymaga od czasu do czasu O (n), ponieważ podstawowa tablica musi zostać ponownie przydzielona.

Wstawienie do zestawu zawsze zajmuje najwyżej O (log n). Może to być preferowane w niektórych aplikacjach.


3

Przepraszam, jeszcze jedna rzecz, na którą warto zwrócić uwagę w przypadku posortowanej nieruchomości:

Jeśli chcesz zakres danych w kontenerze, na przykład: Zapisałeś czas w zestawie , a chcesz czas od 01.01.2013 do 01.01.2014.

Dla unordered_set jest to niemożliwe.

Oczywiście ten przykład byłby bardziej przekonujący dla przypadków użycia między mapą a unordered_map .


3

g++ 6.4 Stdlibc ++ benchmark uporządkowanego a nieuporządkowanego zestawu

Testowałem tę dominującą implementację Linux C ++, aby zobaczyć różnicę:

wprowadź opis obrazu tutaj

Pełne szczegóły i analiza testu porównawczego zostały podane pod adresem: Jaka jest podstawowa struktura danych STL w C ++? i nie będę ich tutaj powtarzał.

„BST” oznacza „przetestowano z, std::seta„ mapa skrótów ”oznacza„ przetestowano z std::unordered_set. „Sterta” jest, dla std::priority_queuektórej przeanalizowałem: Heap vs Binary Search Tree (BST)

Krótkie podsumowanie:

  • wykres wyraźnie pokazuje, że w tych warunkach wstawianie haszmap było zawsze dużo szybsze, gdy jest więcej niż 100 tys. elementów, a różnica rośnie wraz ze wzrostem liczby elementów

    Koszt tego przyspieszenia polega na tym, że nie jesteś w stanie efektywnie poruszać się po kolei.

  • krzywe wyraźnie sugerują, że zamówiony produkt std::setjest oparty na BST i oparty na std::unordered_sethashmap. W odpowiedzi referencyjnej dodatkowo potwierdziłem, że przez krok GDB debugowanie kodu.

Podobne pytanie dla mapvs unordered_map: Czy jest jakaś przewaga używania map nad unordered_map w przypadku trywialnych kluczy?


1

Z drugiej strony, powiedziałbym, że wygodnie jest mieć rzeczy w związku, jeśli chcesz przekonwertować je na inny format.

Możliwe jest również, że chociaż dostęp do niego jest szybszy, czas potrzebny na zbudowanie indeksu lub pamięci używanej podczas tworzenia i / lub uzyskiwania dostępu do niego jest dłuższy.


+1, notacja Big Oh ukrywa stałe czynniki, a dla typowych rozmiarów problemów często to stałe czynniki mają największe znaczenie.
j_random_hacker

1

Jeśli chcesz, aby rzeczy były posortowane, użyj set zamiast unordered_set. unordered_set jest używany nad zestawem, gdy kolejność przechowywania nie ma znaczenia.


1

Chociaż ta odpowiedź może być opóźniona o 10 lat, warto na to zwrócić uwagę std::unordered_set ma również wady bezpieczeństwa.

Jeśli funkcja skrótu jest przewidywalna (jest to zwykle przypadek, chyba że stosuje środki zaradcze, takie jak losowa sól), atakujący mogą ręcznie tworzyć dane, które powodują kolizje hash i powodują, że wszystkie wstawienia i wyszukiwania zajmują O (n) czasu .

Można to wykorzystać do bardzo skutecznych i eleganckich ataków typu „odmowa usługi”.

Wiele (większość?) Implementacji języków, które wewnętrznie używają map skrótów, napotkało to:

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.