Kiedy należy używać typu HashSet <T>?

134

Badam ten HashSet<T>typ, ale nie rozumiem, jakie miejsce zajmuje w kolekcjach.

Czy można go użyć do zastąpienia List<T>? Wyobrażam sobie, że działanie a HashSet<T>jest lepsze, ale nie widziałem indywidualnego dostępu do jego elementów.

Czy to tylko do wyliczenia?

Joan Venge
źródło

Odpowiedzi:

228

Ważna rzecz HashSet<T>jest w nazwie: to zestaw . Jedyne, co możesz zrobić z pojedynczym zestawem, to ustalić, jakie są jego elementy i sprawdzić, czy element jest członkiem.

Pytanie, czy możesz pobrać pojedynczy element (np. set[45]), Jest niezrozumieniem koncepcji zbioru. Nie ma czegoś takiego jak 45. element zestawu. Elementy w zestawie nie mają zamówienia. Zbiory {1, 2, 3} i {2, 3, 1} są identyczne pod każdym względem, ponieważ mają to samo członkostwo, a liczy się tylko członkostwo.

Iterowanie po a jest nieco niebezpieczne, HashSet<T>ponieważ narzuca porządek na elementach zestawu. Ta kolejność nie jest tak naprawdę właściwością zbioru. Nie powinieneś na tym polegać. Jeśli porządkowanie pozycji w kolekcji jest dla Ciebie ważne, ta kolekcja nie jest zestawem.

Zestawy są naprawdę ograniczone i mają unikalnych członków. Z drugiej strony są naprawdę szybkie.

Robert Rossney
źródło
1
Fakt, że framework zapewnia SortedSetstrukturę danych albo jest sprzeczny z tym, co mówisz o zamówieniu, które nie jest właściwością zbioru - albo wskazuje na nieporozumienie ze strony zespołu programistów.
Veverke,
10
Myślę, że bardziej poprawne jest stwierdzenie, że kolejność elementów w HashSetnie jest zdefiniowana, więc nie polegaj na kolejności iteratora. Jeśli iterujesz zestaw, ponieważ robisz coś przeciwko elementom w zestawie, nie jest to niebezpieczne, chyba że polegasz na czymkolwiek związanym z zamówieniem. A SortedSetma wszystkie właściwości rzędu HashSet plus , jednak SortedSetnie pochodzi z HashSet; przeformułowane, SortedSet jest uporządkowaną kolekcją odrębnych obiektów .
Kit
110

Oto prawdziwy przykład, w którym używam HashSet<string>:

Częścią mojego wyróżnienia składni dla plików UnrealScript jest nowa funkcja, która wyróżnia komentarze w stylu Doxygen . Muszę być w stanie stwierdzić, @czy \polecenie lub jest prawidłowe, aby określić, czy pokazać je w kolorze szarym (prawidłowe), czy czerwonym (nieprawidłowe). Mam HashSet<string>ze wszystkich poprawnych poleceń, więc za każdym razem, gdy uderzę w @xxxtoken w lexerze, używam validCommands.Contains(tokenText)jako mojego sprawdzenia poprawności O (1). Naprawdę nie obchodzi mnie nic poza istnieniem polecenia w zestawie prawidłowych poleceń. Spójrzmy na alternatywy, z którymi się spotkałem:

  • Dictionary<string, ?>: Jakiego typu użyć dla wartości? Wartość jest bez znaczenia, ponieważ zamierzam po prostu użyć ContainsKey. Uwaga: przed .NET 3.0 był to jedyny wybór dla wyszukiwań O (1) - HashSet<T>został dodany do 3.0 i rozszerzony do implementacji ISet<T>dla 4.0.
  • List<string>: Jeśli utrzymam posortowaną listę, mogę użyć BinarySearch, czyli O (log n) (nie widziałem tego faktu wspomnianego powyżej). Ponieważ jednak moja lista prawidłowych poleceń to stała lista, która nigdy się nie zmienia, nigdy nie będzie to bardziej odpowiednie niż po prostu ...
  • string[]: Ponownie, Array.BinarySearchdaje wydajność O (log n). Jeśli lista jest krótka, może to być najlepsza opcja. Zawsze ma mniej miejsca niż narzut HashSet, Dictionarylub List. Nawet BinarySearchw przypadku dużych zestawów nie jest to szybsze, ale w przypadku małych zestawów warto byłoby poeksperymentować. Mój ma jednak kilkaset pozycji, więc przekazałem to.
Sam Harwell
źródło
24

A HashSet<T>implementuje ICollection<T>interfejs:

public interface ICollection<T> : IEnumerable<T>, IEnumerable
{
    // Methods
    void Add(T item);
    void Clear();
    bool Contains(T item);
    void CopyTo(T[] array, int arrayIndex);
    bool Remove(T item);

    // Properties
   int Count { get; }
   bool IsReadOnly { get; }
}

A List<T>narzędzia IList<T>, która rozszerzaICollection<T>

public interface IList<T> : ICollection<T>
{
    // Methods
    int IndexOf(T item);
    void Insert(int index, T item);
    void RemoveAt(int index);

    // Properties
    T this[int index] { get; set; }
}

HashSet ma ustawioną semantykę, zaimplementowaną wewnętrznie za pomocą tablicy haszującej:

Zestaw to zbiór, który nie zawiera zduplikowanych elementów i którego elementy nie są w określonej kolejności.

Co zyskuje HashSet, jeśli utraci zachowanie indeksu / pozycji / listy?

Dodawanie i pobieranie elementów z HashSet jest zawsze wykonywane przez sam obiekt, a nie przez indeksator i blisko operacji O (1) (lista to O (1) dodawanie, O (1) pobieranie według indeksu, O (n) znajdowanie /usunąć).

Zachowanie HashSet można porównać do używania a Dictionary<TKey,TValue>, dodając / usuwając klucze jako wartości i ignorując same wartości słownikowe. Można by oczekiwać, że klucze w słowniku nie będą miały zduplikowanych wartości i to jest sedno części „Set”.

Kenan EK
źródło
14

Wydajność byłaby złym powodem, aby wybrać HashSet zamiast List. Zamiast tego, co lepiej oddaje twoje zamiary? Jeśli kolejność jest ważna, to Set (lub HashSet) jest niedostępny. Podobnie, jeśli dozwolone są duplikaty. Ale jest wiele okoliczności, w których nie dbamy o porządek i wolelibyśmy nie mieć duplikatów - i właśnie wtedy potrzebujesz zestawu.

Carl Manaster
źródło
21
Performance would be a bad reason to choose HashSet over List: Po prostu się z tobą nie zgadzam. To trochę powiedzenie, że wybór Dictionray zamiast dwóch list nie pomaga w wydajności. Spójrz na następujący artykuł
Oscar Mederos
11
@Oscar: Nie powiedziałem, że zestawy nie są szybsze - powiedziałem, że to zła podstawa do ich wyboru. Jeśli próbujesz przedstawić zamówioną kolekcję, zestaw po prostu nie zadziała i błędem byłoby próbowanie go włożyć; jeśli wybrana przez Ciebie kolekcja nie ma porządku, zestaw jest doskonały - i szybki. Ale ważne jest pierwsze pytanie: co próbujesz reprezentować?
Carl Manaster
2
Ale pomyśl o tym. Jeśli chcesz nadal sprawdzać, czy dane ciągi należą do jakiejś kolekcji 10000 ciągów, technicznie rzecz biorąc, string[].Containsi HashSet<string>.Containsrównie dobrze wyrażaj swoje zamiary; Powodem wyboru HashSet jest to, że będzie działać znacznie szybciej.
Casey,
12

HashSet to zestaw implementowany przez haszowanie. Zestaw to zbiór wartości, które nie zawierają zduplikowanych elementów. Wartości w zestawie również są zazwyczaj nieuporządkowane. Więc nie, zestaw nie może być użyty do zastąpienia listy (chyba że powinieneś był użyć zestawu w pierwszej kolejności).

Jeśli zastanawiasz się, do czego może się przydać zestaw: oczywiście wszędzie tam, gdzie chcesz pozbyć się duplikatów. Jako nieco wymyślony przykład, załóżmy, że masz listę 10.000 wersji projektów oprogramowania i chcesz dowiedzieć się, ile osób przyczyniło się do tego projektu. Możesz użyć a Set<string>i iterować po liście rewizji i dodać autora każdej rewizji do zestawu. Po zakończeniu iteracji rozmiar zestawu jest odpowiedzią, której szukałeś.

hrabia
źródło
Ale Set nie pozwala na pobieranie pojedynczych elementów? Jak zestaw [45]?
Joan Venge
2
W tym celu iterowałbyś po członkach zestawu. Inne typowe operacje to sprawdzenie, czy zestaw zawiera element lub pobranie rozmiaru zestawu.
hrabiego
11

HashSet byłby używany do usuwania zduplikowanych elementów w kolekcji IEnumerable. Na przykład,

List<string> duplicatedEnumrableStrings = new List<string> {"abc", "ghjr", "abc", "abc", "yre", "obm", "ghir", "qwrt", "abc", "vyeu"};
HashSet<string> uniqueStrings = new HashSet(duplicatedEnumrableStrings);

po uruchomieniu tych kodów uniqueStrings przechowuje {"abc", "ghjr", "yre", "obm", "qwrt", "vyeu"};

Thomas.Benz
źródło
6

Prawdopodobnie najczęstszym zastosowaniem hashetów jest sprawdzenie, czy zawierają one pewien element, który jest dla nich bliski operacji O (1) (zakładając wystarczająco silną funkcję haszującą), w przeciwieństwie do list, dla których sprawdzanie włączenia jest O ( n) (i posortowane zbiory, dla których jest to O (log n)). Więc jeśli wykonujesz wiele sprawdzeń, czy pozycja znajduje się na jakiejś liście, hahssets może oznaczać poprawę wydajności. Jeśli kiedykolwiek będziesz je iterować, nie będzie dużej różnicy (iteracja po całym zestawie to O (n), tak samo jak w przypadku list i hashsetów, które mają nieco więcej narzutu podczas dodawania elementów).

I nie, nie możesz zindeksować zestawu, co i tak nie miałoby sensu, ponieważ zestawy nie są uporządkowane. Jeśli dodasz jakieś elementy, zestaw nie zapamięta, który był pierwszy, a który drugi itd.

sepp2k
źródło
Jeśli tylko je iterujesz, metoda HashSet dodaje sporo pamięci w porównaniu z Listą.
SamuelWarren
5

HashSet<T>jest strukturą danych w środowisku .NET, która jest w stanie przedstawić zestaw matematyczny jako obiekt. W tym przypadku używa kodów skrótu ( GetHashCodewyniku każdego elementu) do porównania równości elementów zestawu.

Zestaw różni się od listy tym, że dopuszcza tylko jedno wystąpienie tego samego elementu w nim zawartego. HashSet<T>po prostu zwróci, falsejeśli spróbujesz dodać drugi identyczny element. Rzeczywiście, wyszukiwanie elementów jest bardzo szybkie ( O(1)czas), ponieważ wewnętrzna struktura danych jest po prostu haszowana.

Jeśli zastanawiasz się, którego użyć, pamiętaj, że użycie List<T>gdzie HashSet<T>jest właściwe nie jest największym błędem, chociaż może potencjalnie powodować problemy, gdy masz niepożądane zduplikowane elementy w swojej kolekcji. Co więcej, wyszukiwanie (pobieranie przedmiotów) jest znacznie bardziej wydajne - najlepiej O(1)(dla idealnego zasobnika) zamiast O(n)czasu - co jest dość ważne w wielu scenariuszach.

Noldorin
źródło
1
Dodanie istniejącego elementu do zestawu nie spowoduje zgłoszenia wyjątku. Add zwróci po prostu false. Ponadto: z technicznego punktu widzenia wyszukiwanie skrótów to O (n), a nie O (1), chyba że masz idealną funkcję haszującą. Oczywiście w praktyce założysz, że to O (1), chyba że funkcja haszująca jest naprawdę zła.
wrzesień
1
@ sepp2k: Tak, więc zwraca wartość logiczną ... Chodzi o to, że powiadamia Cię. A wyszukiwanie skrótu jest najgorszym przypadkiem, gdy O (n), jeśli zbierasz wiadro, jest okropne - ogólnie jest znacznie bliższe O (1).
Noldorin
4

List<T>służy do przechowywania uporządkowanych zestawów informacji. Jeśli znasz względną kolejność elementów listy, możesz uzyskać do nich dostęp w stałym czasie. Jednak aby określić, gdzie element znajduje się na liście lub sprawdzić, czy istnieje na liście, czas wyszukiwania jest liniowy. Z drugiej strony,HashedSet<T> nie gwarantuje porządku przechowywanych danych, a co za tym idzie zapewnia stały czas dostępu do ich elementów.

Jak sama nazwa wskazuje, HashedSet<T>implementuje strukturę danych semantykę zbioru . Struktura danych jest zoptymalizowana pod kątem implementacji operacji na zestawach (tj. Suma, Różnica, Przecięcie), czego nie można wykonać tak wydajnie w przypadku tradycyjnej implementacji listy.

Tak więc wybór typu danych do użycia naprawdę zależy od tego, co próbujesz zrobić z aplikacją. Jeśli nie obchodzi Cię kolejność elementów w kolekcji i chcesz tylko wyliczyć lub sprawdzić istnienie, użyj HashSet<T>. W przeciwnym razie rozważ użycie List<T>lub innej odpowiedniej struktury danych.

Steve Guidi
źródło
2
Kolejne zastrzeżenie: zestawy zazwyczaj zezwalają tylko na jedno wystąpienie elementu.
Steve Guidi
1

Krótko mówiąc - za każdym razem, gdy masz ochotę użyć słownika (lub słownika, w którym S jest własnością T), powinieneś rozważyć HashSet (lub HashSet + implementujący IEquatable na T, który równa się S)

Addys
źródło
5
O ile nie zależy Ci na kluczu, powinieneś skorzystać ze słownika.
Hardwareguy
1

W podstawowym zamierzonym scenariuszu HashSet<T>należy używać, gdy chcesz uzyskać bardziej szczegółowe operacje na dwóch kolekcjach niż zapewnia LINQ. Metody LINQ podoba Distinct, Union, Intersecti Exceptsą na tyle w większości przypadków, ale czasami może być konieczne kolejne operacje drobnoziarnistą i HashSet<T>zapewnia:

  • UnionWith
  • IntersectWith
  • ExceptWith
  • SymmetricExceptWith
  • Overlaps
  • IsSubsetOf
  • IsProperSubsetOf
  • IsSupersetOf
  • IsProperSubsetOf
  • SetEquals

Inną różnicą między HashSet<T>metodami LINQ i „nakładającymi się” jest to, że LINQ zawsze zwraca nowy IEnumerable<T>, a HashSet<T>metody modyfikują kolekcję źródłową.

c_buk
źródło