什么时候应该使用 HashSet<T> 类型?

Posted

技术标签:

【中文标题】什么时候应该使用 HashSet<T> 类型?【英文标题】:When should I use the HashSet<T> type? 【发布时间】:2010-11-17 20:39:22 【问题描述】:

我正在探索HashSet&lt;T&gt; 类型,但我不明白它在集合中的位置。

可以用它来代替List&lt;T&gt;吗?我认为HashSet&lt;T&gt; 的性能会更好,但我看不到个人对其元素的访问。

只用于枚举吗?

【问题讨论】:

【参考方案1】:

HashSet 是一个通过散列实现的set。集合是不包含重复元素的值的集合。集合中的值通常也是无序的。所以不,不能使用集合来替换列表(除非您首先应该使用集合)。

如果您想知道集合可能有什么用处:显然,您想在任何地方摆脱重复。作为一个稍微人为的例子,假设您有一个软件项目的 10.000 个修订的列表,并且您想找出有多少人为该项目做出了贡献。您可以使用Set&lt;string&gt; 并遍历修订列表并将每个修订的作者添加到集合中。完成迭代后,集合的大小就是您要寻找的答案。

【讨论】:

但是 Set 不允许检索单个元素?喜欢 set[45]? 为此,您将遍历集合中的成员。其他典型的操作是检查集合是否包含元素或获取集合的大小。【参考方案2】:

性能将是选择 HashSet 而不是 List 的一个不好的理由。相反,有什么能更好地捕捉您的意图?如果顺序很重要,那么 Set(或 HashSet)就出局了。如果允许重复,同样如此。但是在很多情况下,我们不关心顺序,我们宁愿没有重复 - 这就是你想要一个 Set 的时候。

【讨论】:

Performance would be a bad reason to choose HashSet over List:我只是不同意你的观点。这就是说选择一个字典而不是两个列表对性能没有帮助。看看the following article @Oscar:我并没有说套装并不快——我说那将是选择它们的糟糕基础。如果你试图代表一个有序的集合,那么一个集合根本就行不通,试图把它硬塞进去是错误的;如果您想要的收藏没有顺序,那么一套是完美的——而且速度很快。但重要的是第一个问题:你想代表什么? 但是想一想。如果您想继续检查给定的字符串是否是某个 10,000 个字符串集合的成员,从技术上讲,string[].ContainsHashSet&lt;string&gt;.Contains 可以很好地表达您的意图;选择 HashSet 的原因是它会运行得更快。【参考方案3】:

HashSet&lt;T&gt; 是 .NET 框架中的数据结构,能够将 mathematical set 表示为对象。在这种情况下,它使用哈希码(每个项目的GetHashCode 结果)来比较集合元素的相等性。

集合与列表的不同之处在于它只允许其中包含的相同元素出现一次。如果您尝试添加第二个相同的元素,HashSet&lt;T&gt; 只会返回 false。实际上,元素的查找非常快(O(1) 时间),因为内部数据结构只是一个哈希表。

如果您想知道使用哪个,请注意,使用 List&lt;T&gt;(其中 HashSet&lt;T&gt; 是合适的)并不是最大的错误,尽管它可能会在您的集合中有不受欢迎的重复项目时出现问题。更重要的是,查找(项目检索)效率更高 - 理想情况下是 O(1)(用于完美分桶)而不是 O(n) 时间 - 这在许多情况下都非常重要。

【讨论】:

将现有项目添加到集合中不会引发异常。 Add 只会返回 false。另外:从技术上讲,哈希查找是 O(n),而不是 O(1),除非你有一个完美的哈希函数。当然,在实践中你会假设它是 O(1),除非散列函数真的很糟糕。 @sepp2k:是的,所以它返回一个布尔值......关键是,它会通知你。并且散列查找是最坏的情况如果你正在分桶,O(n) 很糟糕 - 它通常更接近 O(1)。【参考方案4】:

List&lt;T&gt; 用于存储有序的信息集。如果您知道列表元素的相对顺序,则可以在恒定时间内访问它们。但是,要确定元素在列表中的位置或检查它是否存在于列表中,查找时间是线性的。另一方面,HashedSet&lt;T&gt; 不保证存储数据的顺序,因此为其元素提供恒定的访问时间。

顾名思义,HashedSet&lt;T&gt; 是实现set semantics 的数据结构。数据结构经过优化以实现集合操作(​​即 Union、Difference、Intersect),这是传统 List 实现无法高效完成的。

因此,选择使用哪种数据类型实际上取决于您尝试对应用程序执行的操作。如果您不关心元素在集合中的排序方式,而只想枚举或检查是否存在,请使用HashSet&lt;T&gt;。否则,请考虑使用List&lt;T&gt; 或其他合适的数据结构。

【讨论】:

另一个警告:集合通常只允许一个元素出现一次。【参考方案5】:

hashsets 最常见的用途可能是查看它们是否包含某个元素,这对它们来说接近 O(1) 操作(假设一个足够强的散列函数),而不是检查包含的列表是 O(n) (以及它是 O(log n) 的排序集)。因此,如果您进行大量检查,某个项目是否包含在某个列表中,hahssets 可能会提高性能。如果你只对它们进行迭代,则不会有太大区别(迭代整个集合是 O(n),与列表相同,哈希集在添加项目时会有更多开销)。

不,你不能索引一个集合,这无论如何都是没有意义的,因为集合不是有序的。如果你添加一些物品,套装将不记得哪个是第一个,哪个是第二个等等。

【讨论】:

如果只对它们进行迭代,那么与 List 相比,HashSet 方法会增加相当多的内存使用量。【参考方案6】:

简而言之 - 任何时候你想使用字典(或字典,其中 S 是 T 的属性),那么你应该考虑一个 HashSet(或 HashSet + 在 T 上实现 IEquatable,它等于 S)

【讨论】:

除非你关心密钥,否则你应该使用字典。【参考方案7】:

HashSet&lt;T&gt; 的重要之处就在名称中:它是一个集合。您可以对单个集合做的唯一事情是确定它的成员是什么,并检查一个项目是否是成员。

询问您是否可以检索单个元素(例如set[45])是对集合概念的误解。没有像集合的第 45 个元素这样的东西。集合中的项目没有排序。集合 1, 2, 3 和 2, 3, 1 在各个方面都是相同的,因为它们具有相同的成员资格,而成员资格才是最重要的。

迭代HashSet&lt;T&gt; 有点危险,因为这样做会对集合中的项目施加顺序。该顺序并不是该集合的真正属性。你不应该依赖它。如果集合中项目的排序对您很重要,那么该集合就不是集合。

集合非常有限,并且具有独特的成员。另一方面,它们真的很快。

【讨论】:

框架提供SortedSet 数据结构的事实与您所说的顺序不是集合的属性相矛盾 - 或者指出了开发团队的误解。 我觉得说HashSet里面的item的顺序没有定义比较正确,所以不要依赖迭代器的顺序。如果您因为对集合中的项目做某事而对集合进行迭代,那是危险的除非您依赖于与订单相关的任何事物。 SortedSet 具有 HashSet plus 顺序的所有属性,但是 SortedSet 不是从 HashSet 派生的;换个说法,SortedSet 是不同对象的有序集合【参考方案8】:

这是我使用HashSet&lt;string&gt; 的真实示例:

我的 UnrealScript 文件语法高亮显示的一部分是 highlights Doxygen-style comments 的新功能。我需要能够判断 @\ 命令是否有效,以确定是显示为灰色(有效)还是红色(无效)。我有一个所有有效命令的HashSet&lt;string&gt;,所以每当我在词法分析器中点击@xxx 标记时,我都会使用validCommands.Contains(tokenText) 作为我的O(1) 有效性检查。除了有效命令的set 中命令的存在 之外,我真的不关心任何事情。让我们看看我面临的替代方案:

Dictionary&lt;string, ?&gt;: 我用什么类型的值?该值没有意义,因为我将使用ContainsKey。注意:在 .NET 3.0 之前,这是 O(1) 查找的唯一选择 - 为 3.0 添加了 HashSet&lt;T&gt;,并扩展为在 4.0 中实现 ISet&lt;T&gt;List&lt;string&gt;:如果我保持列表排序,我可以使用BinarySearch,即 O(log n)(没有看到上面提到的这个事实)。但是,由于我的有效命令列表是一个永远不会改变的固定列表,所以这永远不会比简单地更合适...... string[]:同样,Array.BinarySearch 提供 O(log n) 性能。如果列表很短,这可能是性能最佳的选择。它的空间开销总是比HashSetDictionaryList 少。即使使用BinarySearch,对于大型系列来说也不会更快,但对于小型系列来说,它值得尝试。不过我的有几百件,所以我把它传递了。

【讨论】:

【参考方案9】:

HashSet&lt;T&gt; 实现了ICollection&lt;T&gt; 接口:

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; 

List&lt;T&gt; 实现 IList&lt;T&gt;,它扩展了 ICollection&lt;T&gt;

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 具有设置语义,通过内部哈希表实现:

集合是一个集合,不包含 重复元素,以及其元素 没有特别的顺序。

如果 HashSet 失去索引/位置/列表行为,它会获得什么?

从 HashSet 添加和检索项目始终由对象本身进行,而不是通过索引器,并且接近 O(1) 操作(列表是 O(1) 添加,O(1) 通过索引检索,O( n) 查找/删除)。

可以将 HashSet 的行为与使用 Dictionary&lt;TKey,TValue&gt; 进行比较,只需添加/删除键作为值,而忽略字典值本身。您会希望字典中的键没有重复值,这就是“设置”部分的重点。

【讨论】:

【参考方案10】:

HashSet 将用于删除 IEnumerable 集合中的重复元素。例如,

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

在这些代码运行后,uniqueStrings 保存 "abc", "ghjr", "yre", "obm", "qwrt", "vyeu";

【讨论】:

【参考方案11】:

在基本预期场景中,当您希望对两个集合进行比 LINQ 提供的更具体的集合操作时,应使用HashSet&lt;T&gt;DistinctUnionIntersectExcept 等 LINQ 方法在大多数情况下就足够了,但有时您可能需要更细粒度的操作,HashSet&lt;T&gt; 提供:

UnionWith IntersectWith ExceptWith SymmetricExceptWith Overlaps IsSubsetOf IsProperSubsetOf IsSupersetOf IsProperSubsetOf SetEquals

LINQ 和HashSet&lt;T&gt;“重叠”方法的另一个区别是LINQ 总是返回一个新的IEnumerable&lt;T&gt;,而HashSet&lt;T&gt; 方法修改源集合。

【讨论】:

以上是关于什么时候应该使用 HashSet<T> 类型?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 HashSet<T> 没有实现 ICollection?

克隆 HashSet<T> 的有效方法?

C# HashSet<T> 只读解决方法

C#中List怎么转换成hashset

使用ISerializationCallbackReceiver解决HashSet/Dictionary无法序列化的问题

HashSet<T>.removeAll 方法非常慢