【发布时间】:2012-12-28 15:13:20
【问题描述】:
我有一组 int 值,我用以下方式填充 HashSet<int> -
var hashSet = new HashSet<int>(myIEnumerable);
假设迭代IEnumerable 是O(n),那么以这种方式创建HashSet<int> 的最坏情况复杂性是多少?
【问题讨论】:
标签: c# .net complexity-theory
我有一组 int 值,我用以下方式填充 HashSet<int> -
var hashSet = new HashSet<int>(myIEnumerable);
假设迭代IEnumerable 是O(n),那么以这种方式创建HashSet<int> 的最坏情况复杂性是多少?
【问题讨论】:
标签: c# .net complexity-theory
【讨论】:
HashSet 如何获取它在哈希计算中使用的模值。我使用这些信息为构造函数提供了所有属于同一索引的各种值,并且我的测试中的性能下降似乎几乎是完全二次的。毕竟,最坏情况的复杂度似乎确实是O(n^2),即使哈希值没有冲突。
您可以通过在集合达到其最大大小时将所有散列到同一存储桶的对象提供给O(N^2) 来解决最坏的情况。例如,如果你传递一个由 17519 个ints 构成的序列
x[i] = i * 17519
对于介于 1 和 17519(含)之间的 i,所有数字都将散列到 Microsoft 实现 HashSet<int> 的初始存储桶中,以 O(N^2) 插入:
var h = new HashSet<int>(Enumerable.Range(1, 17519).Select(i => i*17519));
设置一个中断点,并在调试器中检查h。查看原始视图/非公共成员/m_buckets。观察初始桶有 17519 个元素,而其余 17518 个元素都为零。
【讨论】:
GetHashCode,则可以强制比 O(n^2) 时间更差。例如,您可以有一个永远不会返回的GetHashCode,并且永远无法完成任务,或者您可以有一个需要O(n^2) 时间来计算的GetHashCode 方法,从而生成HashSet 方法。 ..比那更糟糕。
GetHashCode 或 Int32,因此您无法将 OP 中的 new HashSet<int>(myIEnumerable) 强制进入 O(N^2) 领域。当您可以控制GetHashCode 时,您可以强制HashSet<T> 无限期阻止:) HashSet<long> 是中间路线:您可以做的最糟糕的事情是O(N^2) 通过为.NET 实现提供一个特别糟糕的序列Int64.GetHashCode.
ints,您仍然可以创建存储桶索引的冲突。只需添加整数是Capacity 的倍数。我希望在这种情况下增加 O(n^2) 的性能,但我懒得弄清楚HashSet<T> 的首选容量。
简并哈希码(一个常数)的快速实验表明它是二次的。
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<T> 使用类似bucket = (hash&0x7FFFFFFF)%Capacity 的东西。因此,如果您添加一个整数序列是首选存储桶大小的倍数,它仍然会非常慢。
【讨论】:
HashSet 构造函数的最坏情况复杂性特别感兴趣,而不是多个Add 调用的复杂性。
int 是少数有 no 冲突的类型之一。可能的int 值的数量不大于可能的int 值的数量,因此int 值的哈希码对于不同的值实际上是唯一的。 (换句话说,它的哈希码可以只返回自己。)其他类型,如byte 和char 的值也比int 少,因此永远不会发生冲突。