Quelle est la différence entre HashSet.Union
contre HashSet.Unionwith
quand je combine 2 hashsets.
J'essaye de combiner comme ça:
HashSet<EngineType> enginesSupportAll = _filePolicyEvaluation.EnginesSupportAll;
enginesSupportAll = enginesSupportAll != null ? new HashSet<EngineType>(engines.Union(enginesSupportAll)) : enginesSupportAll;
quelle est la meilleure méthode pour cet exemple et pourquoi?
Eh bien, ce n'est pas HashSet.Union
mais Enumerable.Union
, vous utilisez donc une méthode d'extension LINQ qui fonctionne avec tout type de IEnumerable<>
tandis que HashSet.UnionWith
est une véritable méthode HashSet
qui modifie l'instance actuelle.
Union
renvoie un IEnumerable<TSource>
UnionWith
est void
, il modifie l'instance actuelle de HashSet
UnionWith
est légèrement plus efficace car il peut être optimiséSi vous ne voulez prendre en charge aucun type de séquence dans votre méthode, donc HashSet
est fixe et vous pouvez le modifier, utilisez-le, sinon utilisez l'extension LINQ. Si vous créez l'instance HashSet
juste à cet effet, cela n'a pas vraiment d'importance et je préférerais que LINQ soit plus flexible et puisse enchaîner ma requête.
Étant donné HashSet<T>
[~ # ~] un [~ # ~] et HashSet<T>
[~ # ~] b [~ # ~] il y a quatre façons de [~ # ~] a [~ # ~] ∪ [~ # ~] b [~ # ~]:
new HashSet<t>(A.Union(B))
HashSet<T&>(IEnumerable<T>)
et Enumerable.Union<T>(IEnumerable<T>, IEnumerable<T>)
)A.UnionWith(B)
HashSet<T> C = new HashSet<T>(A); C.UnionWith(B);
new HashSet<t>(A.Concat(B))
Enumerable.Concat<T>(IEnumerable<T>, IEnumerable<T>)
)Chacun a ses avantages et désavantages:
HashSet
tandis que 2 et 3 sont des instructions ou des blocs d'instructions.from x in setofSetsA as IEnumerable<HashSet<T>> from y in setOfSetsB as IEnumerable<HashSet<T>> select x.UnionWith(y)
ne fonctionnera pas puisque UnionWith
renvoie void.Coût de calcul:
A.UnionWith(B)
(≈ O ((log (| A∪B |) - log (| A |)) * | A∪B |) + O (| B |))
≤
HashSet<T> C = new HashSet<T>(A); C.UnionWith(B);
(≈ O ((log (| A∪B |) - log (| A |)) * | A∪B |) + O (| A | + | B |))
≤
HashSet<T>(A.Concat(B))
(≈ O (log (| A∪B |) * | A∪B |) + O (| A | + | B |))
≤
HashSet<T>(A.Union(B))
(≈ 2 * O (log (| A∪B |) * | A∪B |) + O (| A | + | B | + | A∪B |))
La section suivante se penche sur la source de référence pour voir la base de ces estimations de performance.
HashSet<T>
Dans les options d'union 1, 3 et 4, le constructeur HashSet<T>(IEnumerable<T>, IEqualityComparer<T>)
est utilisé pour créer un HashSet<T>
À partir d'un IEnumerable<T>
. Si le IEnumerable<T>
Passé a une propriété Count
—i.e. s'il s'agit d'un ICollection<T>
-, cette propriété est utilisée pour définir la taille du nouveau HashSet
:
int suggestedCapacity = 0; ICollection<T> coll = collection as ICollection<T>; if (coll != null) { suggestedCapacity = coll.Count; } Initialize(suggestedCapacity);
La méthode [Count()][10]
n'est jamais appelée. Ainsi, si le nombre de IEnumerable
peut être récupéré sans effort, il est utilisé pour réserver la capacité; sinon, le HashSet
grandit et se réalloue lorsque de nouveaux éléments sont ajoutés.
Dans l'option 1 A.Union(B)
et l'option 4 A.Concat(B)
ne sont pas ICollection<T>
Donc le HashSet
créé va grandir et réaffecter quelques-uns (≈ log (| A∪B |)) fois. L'option 3 peut utiliser le Count
de [~ # ~] a [~ # ~].
Le constructeur appelle UnionWith
pour remplir le nouveau HashSet
vide:
this.UnionWith(collection);
UnionWith(IEnumerable<T>)
itère sur les éléments du IEnumerable<T>
passé en argument et appelle AddIfNotPresent(T)
pour chacun.
AddIfNotPresent(T)
insère des éléments et garantit que les doublons ne sont jamais insérés dans l'ensemble.HashSet<T>
Est implémenté comme un tableau d'emplacements, m_slots
, et un tableau de compartiments, m_buckets
. Un compartiment contient juste un index int
dans le tableau m_slots
. Par compartiment, les Slot
s dans m_slots
Forment une liste liée avec un index vers le Slot
suivant dans m_slots
.
AddIfNotPresent(T)
saute dans le compartiment correct puis parcourt sa liste chaînée pour vérifier si l'élément est déjà présent:
for (int i = m_buckets[hashCode % m_buckets.Length] - 1; i >= 0; i = m_slots[i].next) { if (m_slots[i].hashCode == hashCode && m_comparer.Equals(m_slots[i].value, value)) { return false; } }
Ensuite, un index gratuit est trouvé et un emplacement est réservé. Tout d'abord, la liste des emplacements libres, m_freelist
, est vérifiée. Lorsqu'il n'y a pas d'emplacements dans la liste libre, l'emplacement suivant vide dans le tableau m_slots
Est utilisé. Plus de capacité est réservée (via IncreaseCapacity()
) s'il n'y a pas de slots dans la liste libre et pas de slots vides:
int index; if (m_freeList >= 0) { index = m_freeList; m_freeList = m_slots[index].next; } else { if (m_lastIndex == m_slots.Length) { IncreaseCapacity(); // this will change during resize bucket = hashCode % m_buckets.Length; } index = m_lastIndex; m_lastIndex++; }
AddIfNotPresent(T)
a trois opérations qui demandent un certain calcul: l'appel à object.GetHashCode()
, appelant object.Equals(object)
quand il est une collision et IncreaseCapacity()
. L'ajout réel d'un élément n'entraîne que le coût de la mise en place de certains pointeurs et de certains pouces.
Lorsque HashSet<T>
Doit IncreaseCapacity()
la capacité est au moins doublée. Nous pouvons donc conclure qu'en moyenne un HashSet<T>
Est rempli à 75%. Si les hachages sont répartis uniformément, l'espérance d'une collision de hachage est également de 75%.
SetCapacity(int, bool)
, appelé par IncreaseCapacity()
, est le plus cher: il alloue de nouveaux tableaux, il copie l'ancien tableau d'emplacement dans le nouveau tableau, et il recalcule le listes de compartiments:
Slot[] newSlots = new Slot[newSize]; if (m_slots != null) { Array.Copy(m_slots, 0, newSlots, 0, m_lastIndex); } ... int[] newBuckets = new int[newSize]; for (int i = 0; i < m_lastIndex; i++) { int bucket = newSlots[i].hashCode % newSize; newSlots[i].next = newBuckets[bucket] - 1; newBuckets[bucket] = i + 1; } m_slots = newSlots; m_buckets = newBuckets;
Les options 1 et 4 (new HashSet<T>(A.Union(B))
) entraîneront un peu plus d'appels à IncreaseCapacity()
. Le coût —sans le coût de A.Union(B)
ou A.Concat(B)
— est d'environ O (log (| A∪B |) * | A∪B | ) .
Alors que lorsque vous utilisez l'option 2 (A.UnionWith(B)
) ou l'option 3 (HashSet<T> C = new HashSet<T>(A); C.UnionWith(B)
), nous obtenons une "remise" de log (| A |) sur le coût: O ((log (| A∪B |) - log (| A |)) * | A∪B |) . Il est payant (légèrement) d'utiliser le plus grand ensemble comme cible dans laquelle l'autre est fusionné.
Enumerable<T>.Union(IEnumerable<T>)
Enumerable<T>.Union(IEnumerable<T>)
est implémentée via UnionIterator<T>(IEnumerable<T>, IEnumerable<T>, IEqualityComparer<T>)
.
Le UnionIterator
utilise Set<T>
—une classe interne dans Enumerable.cs
- qui est très similaire à HashSet<T>
. UnionIterator
paresseusement Add(T)
s éléments de [~ # ~] a [~ # ~] et [~ # ~] b [~ # ~] à ce Set<T>
et yields
les éléments s'ils peuvent être ajoutés. Le travail se fait dans Find(T, bool)
qui est similaire à HashSet<T>.AddIfNotPresent(T)
. Vérifiez si l'élément est déjà présent:
int hashCode = InternalGetHashCode(value); for (int i = buckets[hashCode % buckets.Length] - 1; i >= 0; i = slots[i].next) { if (slots[i].hashCode == hashCode && comparer.Equals(slots[i].value, value)) return true; }
Trouvez un index gratuit et réservez un emplacement:
int index; if (freeList >= 0) { index = freeList; freeList = slots[index].next; } else { if (count == slots.Length) Resize(); index = count; count++; } int bucket = hashCode % buckets.Length; slots[index].hashCode = hashCode; slots[index].value = value; slots[index].next = buckets[bucket] - 1; buckets[bucket] = index + 1;
Resize()
est similaire à IncreaseCapacity()
. La plus grande différence entre les deux est que Resize()
n'utilise pas nombre premier pour le nombre de compartiments, donc avec une mauvaise GetHashCode()
il y a une chance légèrement plus élevée pour les collisions. Code de Resize()
:
int newSize = checked(count * 2 + 1); int[] newBuckets = new int[newSize]; Slot[] newSlots = new Slot[newSize]; Array.Copy(slots, 0, newSlots, 0, count); for (int i = 0; i < count; i++) { int bucket = newSlots[i].hashCode % newSize; newSlots[i].next = newBuckets[bucket] - 1; newBuckets[bucket] = i + 1; } buckets = newBuckets; slots = newSlots;
Le coût de performance de A.Union(B)
n'est pas significativement différent de celui de HashSet<T> C = new HashSet<T>(); C.UnionWith(A); C.UnionWith(B);
. Dans l'option 1 (new HashSet<T>(A.Union(B))
), le même HashSet
est créé deux fois, ce qui entraîne un 2 * O très coûteux (log (| A∪B |) * (| A∪B |)). L'option 4 résulte de la connaissance de l'implémentation de HashSet<T>(IEnumerable<T>)
et Enumerable.Union(IEnumerable<T>, IEnumerable<T>)
. Il évite la A.Union(B)
redondante conduisant à un coût de O (log (| A∪B |) * | A∪B |).