为什么HashSet <t>类不用于实现Enumerable.Distinct

时间:2017-01-31 09:56:20

标签: c# .net linq big-o hashset

我需要访问大O表示法中post.php的渐近时间和空间复杂度

所以我正在研究扩展方法Enumerable.Distinct的实现,我看到它是使用内部类Set<T>实现的,这几乎是一个带有“开放寻址”的哈希表的经典实现< / p>

Set<T>中的大量代码只是来自HashSet<T>的复制粘贴,但有一些遗漏

但是,这种简化的Set<T>实施有一些明显的缺陷,例如Resize方法不使用素数作为广告位的大小,例如{{3} },看看HashSet<T>

所以,我的问题是:

  1. 这里代码重复的原因是什么,为什么不坚持HashHelpers.ExpandPrime原则?特别是考虑到这两个类都在同一个程序集IEnumerable.Distinct
  2. 看起来DRY的效果会更好,所以我应该避免使用Distinct扩展方法,并编写自己的扩展方法,使用HashSet<T>代替HashSet<T>吗?

1 个答案:

答案 0 :(得分:6)

  

这几乎是具有“开放寻址”的哈希表的经典实现

再看一遍。它与列表头单元格分开链接。当插槽全部在一个阵列中时,通过检查当前插槽的next字段来查找碰撞情况下的下一个插槽。这比使用链接列表和每个节点作为单独的堆对象具有更好的缓存效率,但在这方面不如开放寻址那么好。同时,它避免了一些开放式寻址效果不佳的情况。

  

Set中的很多代码只是来自HashSet的复制粘贴,有一些遗漏

AFAICT使用哈希集的私有实现的原因是EnumerableHashSet几乎在同一时间独立开发。这只是我的猜想,但它们都是用.NET 3.5引入的,所以它是可行的。

HashSet<T>很可能是通过复制Set<T>开始,然后让它更好地公开曝光,尽管这两者都可能都基于与列表头分开链接的相同原则细胞

就性能而言,HashSet使用素数意味着它更有可能避免与较差的哈希冲突(但这只是一个优势,这不是一个简单的问题),但是{{1}在很多方面都很轻松,尤其是在.NET Core中,它不需要的东西被删除了。特别是,Set的那个版本利用了这样一个事实:一旦项目被移除(例如,在Set期间发生),将永远不会添加任何项目,这使得它可以省略Intersect以及freelist无法完成的与此相关的任何工作。即使最初的实施也没有跟踪版本以便在枚举期间捕获变化,这是一个很小的成本,但是每次添加和删除都会产生成本。

因此,对于具有不同哈希码分布的不同数据集,有时一个表现更好,有时表现更好。

  

特别是考虑到这两个类都在同一个程序集System.Core

仅在某些版本的.NET中,在某些版本中,它们位于不同的程序集中。在.NET Core中,我们有两个版本的HashSet,一个在程序集中有Set<T>,另一个在具有System.Linq的单独程序集中。前者如上所述被削减,后者被System.Linq.Expressions取代,因为它在那里做得少。

当然System.Core是第一位的,但是这些元素可以完全分开的事实说明System.Core不是一个单一依赖的整体blob。

.NET Core版本的Linq现在有一个HashSet<T>方法,可以用ToHashSet()替换Set<T>更合理,但不是一件容易的事。我认为@james-ko正在考虑测试这样做的好处。

  

看起来HashSet<T>会表现得更好

由于上面解释的原因,情况可能并非如此,但可能确实如此,具体取决于源数据。这是在考虑经过一些不同的linq方法的优化之前(在linq的初始版本中并不多,但在.NET Core中很少)。

  

我应该避免使用HashSet<T>扩展方法,并编写自己的扩展方法,使用Distinct代替HashSet<T>

使用Set<T>。如果你有一个瓶颈,那么Distinct()可能会赢得给定的数据集,但如果你这样做,请确保你的分析与你的代码在现实生活中会遇到的真实值非常接近。没有必要决定一种方法是基于某些任意测试更快,如果你的应用程序遇到另一个做得更好的情况。 (如果我发现这是一个问题点,我会先看看有问题类型的HashSet<T>是否可以根据速度或位分布进行改进,首先是这样做。

相关问题