【问题标题】:Worst case complexity of creating a HashSet<int> from a collection从集合创建 HashSet<int> 的最坏情况复杂性
【发布时间】:2012-12-28 15:13:20
【问题描述】:

我有一组 int 值,我用以下方式填充 HashSet&lt;int&gt; -

var hashSet = new HashSet<int>(myIEnumerable);

假设迭代IEnumerableO(n),那么以这种方式创建HashSet&lt;int&gt;最坏情况复杂性是多少?

【问题讨论】:

    标签: c# .net complexity-theory


    【解决方案1】:

    文档实际上指出:

    这个构造函数是一个 O(n) 操作,其中 n 是 集合参数中的元素。

    http://msdn.microsoft.com/en-us/library/bb301504.aspx

    【讨论】:

    • 但它是最坏情况复杂性还是摊销复杂性?
    • @UghSegment 您的意思是“平均”复杂性而不是“摊销”。 “摊销”用于有时昂贵的操作(例如后备存储加倍)而其余操作便宜。该概念与平均与最坏情况正交。
    • @UghSegment 添加到 CodeInChaos 的答案中,它既是最坏的情况,也是摊销的复杂性。 (鉴于他解释了为什么可能两者兼而有之,我想说的是这里实际上就是这种情况。)
    • 不,通常最坏的情况当然是二次的,但这是针对具有相同 GetHashCode() 输出的对象。我想知道 int 的。
    • @JeppeStigNielsen 我使用 .NET Reflector 来了解 HashSet 如何获取它在哈希计算中使用的模值。我使用这些信息为构造函数提供了所有属于同一索引的各种值,并且我的测试中的性能下降似乎几乎是完全二次的。毕竟,最坏情况的复杂度似乎确实是O(n^2),即使哈希值没有冲突。
    【解决方案2】:

    您可以通过在集合达到其最大大小时将所有散列到同一存储桶的对象提供给O(N^2) 来解决最坏的情况。例如,如果你传递一个由 17519 个ints 构成的序列

    x[i] = i * 17519
    

    对于介于 1 和 17519(含)之间的 i,所有数字都将散列到 Microsoft 实现 HashSet&lt;int&gt; 的初始存储桶中,以 O(N^2) 插入:

    var h = new HashSet<int>(Enumerable.Range(1, 17519).Select(i => i*17519));
    

    设置一个中断点,并在调试器中检查h。查看原始视图/非公共成员/m_buckets。观察初始桶有 17519 个元素,而其余 17518 个元素都为零。

    【讨论】:

    • 如果是 O(N^2),我不会感到惊讶
    • 但是非摊销的最坏情况复杂性呢?
    • 如果您假设自定义时间带有不良或恶意 GetHashCode,则可以强制比 O(n^2) 时间更差。例如,您可以有一个永远不会返回的GetHashCode,并且永远无法完成任务,或者您可以有一个需要O(n^2) 时间来计算的GetHashCode 方法,从而生成HashSet 方法。 ..比那更糟糕。
    • @Servy 我的观点是,由于您无法控制 .NET 的 GetHashCodeInt32,因此您无法将 OP 中的 new HashSet&lt;int&gt;(myIEnumerable) 强制进入 O(N^2) 领域。当您可以控制GetHashCode 时,您可以强制HashSet&lt;T&gt; 无限期阻止:) HashSet&lt;long&gt; 是中间路线:您可以做的最糟糕的事情是O(N^2) 通过为.NET 实现提供一个特别糟糕的序列Int64.GetHashCode.
    • 对于ints,您仍然可以创建存储桶索引的冲突。只需添加整数是Capacity 的倍数。我希望在这种情况下增加 O(n^2) 的性能,但我懒得弄清楚HashSet&lt;T&gt; 的首选容量。
    【解决方案3】:

    简并哈希码(一个常数)的快速实验表明它是二次的。

    for(int n=0;n<100;n++)
    {
        var start=DateTime.UtcNow;
        var s=new HashSet<Dumb>(Enumerable.Range(0,n*10000).Select(_=>new Dumb()));
        Console.Write(n+" ");
        Console.WriteLine((int)((DateTime.UtcNow-start).TotalSeconds*10));
    }
    

    输出:

    0 0
    1 8
    2 34
    3 73
    4 131
    

    现在有些人声称您不会遇到整数的HashCode 的多次冲突。虽然这在技术上是正确的,但对性能而言重要的不是 HashCode 的冲突,而是桶索引的冲突。我认为HashSet&lt;T&gt; 使用类似bucket = (hash&amp;0x7FFFFFFF)%Capacity 的东西。因此,如果您添加一个整数序列是首选存储桶大小的倍数,它仍然会非常慢。

    【讨论】:

    • 如果所有对象都返回相同的哈希码,那么是的,这是 O(n*n),因为冲突。但是OP的问题是关于int的收集。所以我想知道选择一对具有相同哈希码的 int 有多困难(可能?)。
    • 我认为您执行的测试与我在问题中描述的不同。我对将具有已知数量元素的集合传递给HashSet 构造函数的最坏情况复杂性特别感兴趣,而不是多个Add 调用的复杂性。
    • @SergeyS int 是少数有 no 冲突的类型之一。可能的int 值的数量不大于可能的int 值的数量,因此int 值的哈希码对于不同的值实际上是唯一的。 (换句话说,它的哈希码可以只返回自己。)其他类型,如bytechar 的值也比int 少,因此永远不会发生冲突。
    • 即使使用它也可能导致存储桶索引的冲突。只是更烦人了。 | @UghSegment 它与构造函数相同。查看更新的代码。
    猜你喜欢
    • 2021-03-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-02-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多