什么时候应该使用 HashSet<T> 类型?
Posted
技术标签:
【中文标题】什么时候应该使用 HashSet<T> 类型?【英文标题】:When should I use the HashSet<T> type? 【发布时间】:2010-11-17 20:39:22 【问题描述】:我正在探索HashSet<T>
类型,但我不明白它在集合中的位置。
可以用它来代替List<T>
吗?我认为HashSet<T>
的性能会更好,但我看不到个人对其元素的访问。
只用于枚举吗?
【问题讨论】:
【参考方案1】:HashSet 是一个通过散列实现的set。集合是不包含重复元素的值的集合。集合中的值通常也是无序的。所以不,不能使用集合来替换列表(除非您首先应该使用集合)。
如果您想知道集合可能有什么用处:显然,您想在任何地方摆脱重复。作为一个稍微人为的例子,假设您有一个软件项目的 10.000 个修订的列表,并且您想找出有多少人为该项目做出了贡献。您可以使用Set<string>
并遍历修订列表并将每个修订的作者添加到集合中。完成迭代后,集合的大小就是您要寻找的答案。
【讨论】:
但是 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[].Contains
和 HashSet<string>.Contains
可以很好地表达您的意图;选择 HashSet 的原因是它会运行得更快。【参考方案3】:
HashSet<T>
是 .NET 框架中的数据结构,能够将 mathematical set 表示为对象。在这种情况下,它使用哈希码(每个项目的GetHashCode
结果)来比较集合元素的相等性。
集合与列表的不同之处在于它只允许其中包含的相同元素出现一次。如果您尝试添加第二个相同的元素,HashSet<T>
只会返回 false
。实际上,元素的查找非常快(O(1)
时间),因为内部数据结构只是一个哈希表。
如果您想知道使用哪个,请注意,使用 List<T>
(其中 HashSet<T>
是合适的)并不是最大的错误,尽管它可能会在您的集合中有不受欢迎的重复项目时出现问题。更重要的是,查找(项目检索)效率更高 - 理想情况下是 O(1)
(用于完美分桶)而不是 O(n)
时间 - 这在许多情况下都非常重要。
【讨论】:
将现有项目添加到集合中不会引发异常。 Add 只会返回 false。另外:从技术上讲,哈希查找是 O(n),而不是 O(1),除非你有一个完美的哈希函数。当然,在实践中你会假设它是 O(1),除非散列函数真的很糟糕。 @sepp2k:是的,所以它返回一个布尔值......关键是,它会通知你。并且散列查找是最坏的情况如果你正在分桶,O(n) 很糟糕 - 它通常更接近 O(1)。【参考方案4】:List<T>
用于存储有序的信息集。如果您知道列表元素的相对顺序,则可以在恒定时间内访问它们。但是,要确定元素在列表中的位置或检查它是否存在于列表中,查找时间是线性的。另一方面,HashedSet<T>
不保证存储数据的顺序,因此为其元素提供恒定的访问时间。
顾名思义,HashedSet<T>
是实现set semantics 的数据结构。数据结构经过优化以实现集合操作(即 Union、Difference、Intersect),这是传统 List 实现无法高效完成的。
因此,选择使用哪种数据类型实际上取决于您尝试对应用程序执行的操作。如果您不关心元素在集合中的排序方式,而只想枚举或检查是否存在,请使用HashSet<T>
。否则,请考虑使用List<T>
或其他合适的数据结构。
【讨论】:
另一个警告:集合通常只允许一个元素出现一次。【参考方案5】:hashsets 最常见的用途可能是查看它们是否包含某个元素,这对它们来说接近 O(1) 操作(假设一个足够强的散列函数),而不是检查包含的列表是 O(n) (以及它是 O(log n) 的排序集)。因此,如果您进行大量检查,某个项目是否包含在某个列表中,hahssets 可能会提高性能。如果你只对它们进行迭代,则不会有太大区别(迭代整个集合是 O(n),与列表相同,哈希集在添加项目时会有更多开销)。
不,你不能索引一个集合,这无论如何都是没有意义的,因为集合不是有序的。如果你添加一些物品,套装将不记得哪个是第一个,哪个是第二个等等。
【讨论】:
如果只对它们进行迭代,那么与 List 相比,HashSet 方法会增加相当多的内存使用量。【参考方案6】:简而言之 - 任何时候你想使用字典(或字典,其中 S 是 T 的属性),那么你应该考虑一个 HashSet(或 HashSet + 在 T 上实现 IEquatable,它等于 S)
【讨论】:
除非你关心密钥,否则你应该使用字典。【参考方案7】:HashSet<T>
的重要之处就在名称中:它是一个集合。您可以对单个集合做的唯一事情是确定它的成员是什么,并检查一个项目是否是成员。
询问您是否可以检索单个元素(例如set[45]
)是对集合概念的误解。没有像集合的第 45 个元素这样的东西。集合中的项目没有排序。集合 1, 2, 3 和 2, 3, 1 在各个方面都是相同的,因为它们具有相同的成员资格,而成员资格才是最重要的。
迭代HashSet<T>
有点危险,因为这样做会对集合中的项目施加顺序。该顺序并不是该集合的真正属性。你不应该依赖它。如果集合中项目的排序对您很重要,那么该集合就不是集合。
集合非常有限,并且具有独特的成员。另一方面,它们真的很快。
【讨论】:
框架提供SortedSet
数据结构的事实与您所说的顺序不是集合的属性相矛盾 - 或者指出了开发团队的误解。
我觉得说HashSet
里面的item的顺序没有定义比较正确,所以不要依赖迭代器的顺序。如果您因为对集合中的项目做某事而对集合进行迭代,那不是危险的除非您依赖于与订单相关的任何事物。 SortedSet
具有 HashSet
plus 顺序的所有属性,但是 SortedSet
不是从 HashSet
派生的;换个说法,SortedSet 是不同对象的有序集合。【参考方案8】:
这是我使用HashSet<string>
的真实示例:
我的 UnrealScript 文件语法高亮显示的一部分是 highlights Doxygen-style comments 的新功能。我需要能够判断 @
或 \
命令是否有效,以确定是显示为灰色(有效)还是红色(无效)。我有一个所有有效命令的HashSet<string>
,所以每当我在词法分析器中点击@xxx
标记时,我都会使用validCommands.Contains(tokenText)
作为我的O(1) 有效性检查。除了有效命令的set 中命令的存在 之外,我真的不关心任何事情。让我们看看我面临的替代方案:
Dictionary<string, ?>
: 我用什么类型的值?该值没有意义,因为我将使用ContainsKey
。注意:在 .NET 3.0 之前,这是 O(1) 查找的唯一选择 - 为 3.0 添加了 HashSet<T>
,并扩展为在 4.0 中实现 ISet<T>
。
List<string>
:如果我保持列表排序,我可以使用BinarySearch
,即 O(log n)(没有看到上面提到的这个事实)。但是,由于我的有效命令列表是一个永远不会改变的固定列表,所以这永远不会比简单地更合适......
string[]
:同样,Array.BinarySearch
提供 O(log n) 性能。如果列表很短,这可能是性能最佳的选择。它的空间开销总是比HashSet
、Dictionary
或List
少。即使使用BinarySearch
,对于大型系列来说也不会更快,但对于小型系列来说,它值得尝试。不过我的有几百件,所以我把它传递了。
【讨论】:
【参考方案9】:HashSet<T>
实现了ICollection<T>
接口:
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<T>
实现 IList<T>
,它扩展了 ICollection<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 具有设置语义,通过内部哈希表实现:
集合是一个集合,不包含 重复元素,以及其元素 没有特别的顺序。
如果 HashSet 失去索引/位置/列表行为,它会获得什么?
从 HashSet 添加和检索项目始终由对象本身进行,而不是通过索引器,并且接近 O(1) 操作(列表是 O(1) 添加,O(1) 通过索引检索,O( n) 查找/删除)。
可以将 HashSet 的行为与使用 Dictionary<TKey,TValue>
进行比较,只需添加/删除键作为值,而忽略字典值本身。您会希望字典中的键没有重复值,这就是“设置”部分的重点。
【讨论】:
【参考方案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<T>
。 Distinct
、Union
、Intersect
和 Except
等 LINQ 方法在大多数情况下就足够了,但有时您可能需要更细粒度的操作,HashSet<T>
提供:
UnionWith
IntersectWith
ExceptWith
SymmetricExceptWith
Overlaps
IsSubsetOf
IsProperSubsetOf
IsSupersetOf
IsProperSubsetOf
SetEquals
LINQ 和HashSet<T>
“重叠”方法的另一个区别是LINQ 总是返回一个新的IEnumerable<T>
,而HashSet<T>
方法修改源集合。
【讨论】:
以上是关于什么时候应该使用 HashSet<T> 类型?的主要内容,如果未能解决你的问题,请参考以下文章
为啥 HashSet<T> 没有实现 ICollection?
使用ISerializationCallbackReceiver解决HashSet/Dictionary无法序列化的问题