【问题标题】:Get random element from C# HashSet quickly快速从 C# HashSet 中获取随机元素
【发布时间】:2015-08-09 13:31:46
【问题描述】:

我需要存储一组元素。我需要的是功能

  1. 删除(单个)元素并
  2. 添加(组)元素和
  3. 每个对象只能在集合中出现一次并且
  4. 从集合中随机获取一个元素

我选择了 HashSet (C#),因为它采用 fast 方法来删​​除元素 (hashSet.remove(element)),添加集合 (hashSet.UnionWith (anotherHashSet)) 并且 HashSet 的性质保证不存在重复,因此需要注意要求 1 到 3。

我发现获得随机元素的唯一方法是

Object object = hashSet.ElementAt(rnd.Next(hashSet.Count));

但这非常慢,因为我为地图的每个像素调用一次(从多个起点创建随机洪水填充;目前地图大小为 500x500,但我想更大)并且哈希集相当很多项目。 (快速测试表明它在再次缩小之前会炸毁多达 5752 个条目。)

分析(CPU 采样)告诉我 ElementAt 调用占了 50% 以上。

我意识到对大哈希集进行 500x500 操作并非易事,但其他操作(Remove 和 UnionWith)的调用频率与 ElementAt 一样频繁,因此主要问题似乎是操作而不是调用次数。

我隐约明白为什么从 HashSet 中获取某个元素非常昂贵(与从列表或其他有序数据结构中获取它相比,但我只想随机选择。真的很难吗?有没有办法解决它?是否有更好的数据结构适合我的目的?

将所有内容更改为 Lists 并没有帮助,因为现在其他方法成为瓶颈,并且需要更长的时间。

将 HashSet 转换为一个数组并从中选择我的随机元素预计没有帮助,因为虽然从数组中选择一个随机元素很快,但首先将 hashset 转换为数组需要比运行 hashSet.ElementAt 更长的时间自己。

如果你想更好地理解我想要做什么:A link to my question and the answer.

【问题讨论】:

  • 你要删除什么?它只是随机找到的元素,还是任意的?
  • 为什么不使用 HashSet 进行所有添加和删除操作,然后在进行随机像素获取之前,只需转换为 List 一次?使用那个 List,然后扔掉。除非您需要同时添加、删除和获取随机元素...
  • @spender 我只删除随机找到的元素
  • @Baldrick 我担心是后者。循环基本上是:选择一个随机单元格(哈希集包含随机洪水填充可以传播到的所有可能的单元格,“边缘”)->填充它->找到相邻的空单元格并将它们添加到哈希集中->删除填充来自 hashset 的单元格 -> 再次循环直到 hashset 为空
  • 感觉二维链表在这里会成为你的朋友。

标签: c# performance random hashset


【解决方案1】:

我认为OrderedDictionary 可能适合您的目的:

var dict = new OrderedDictionary();

dict.Add("My String Key", "My String");
dict.Add(12345, 54321);

Console.WriteLine(dict[0]); // Prints "My String"
Console.WriteLine(dict[1]); // Prints 54321

Console.WriteLine(dict["My String Key"]); // Prints "My String"
Console.WriteLine(dict[(object)12345]);   // Prints 54321 (note the need to cast!)

这具有快速添加和删除以及 O(1) 索引。它仅适用于 object 键和值 - 没有通用版本。

[编辑] 多年后:我们现在有了强类型泛型 SortedDictionary<TKey, TValue>,它可能会更好。

【讨论】:

    【解决方案2】:

    基本问题是索引。

    在数组或列表中,数据由其坐标索引 - 通常只是一个简单的 int 索引。在HashSet 中,您自己选择索引 - 键。但是,副作用是没有“坐标”——“索引 3 处的元素”这个问题真的没有意义。它的实际实现方式是枚举整个HashSet,逐项枚举,并返回第n 项。这意味着要获得第 1000 个项目,您还必须枚举之前的所有 999 个项目。这很痛。

    解决此问题的最佳方法是根据HashSet 的实际密钥选择随机数。当然,这只有在像这样选择随机密钥是合理的情况下才有效。

    如果您不能以令人满意的方式随机选择密钥,您可能希望保留两个单独的列表 - 每当您将新项目添加到 HashSet 时,将其密钥添加到 List<TKey>;然后,您可以轻松地从List 中选择一个随机密钥,然后按照它进行操作。根据您的要求,重复可能不是什么大问题。

    当然,如果您只进行一次枚举,您可以节省 ElementAt 枚举 - 例如,在搜索 HashSet 之前,您可以将其转换为 List。当然,这仅在您一次选择多个随机索引时才有意义(例如,如果您一次随机选择 5 个索引,您将平均节省大约 1/5 的时间) - 如果您总是选择一个,然后修改 HashSet 并选择另一个,这将无济于事。

    根据您的具体用例,可能还值得一看 SortedSet。它的工作方式与HashSet 类似,但它保持键的顺序。有用的部分是您可以使用 GetViewBetween 方法来获取整个范围的键 - 如果您的键是稀疏的,但在任意范围之间很好地平衡,您可以非常有效地使用它。您只需先随机选择一个范围,然后使用GetViewBetween 获取范围内的项目,然后从其中随机选择一个。实际上,这将允许您对搜索结果进行分区,并且应该会节省相当多的时间。

    【讨论】:

    • 是的,我正在考虑一个列表和一个哈希集来索引它。
    • @spender 是的,如果您不关心删除垃圾,这可以很好地工作。但是,如果这样做,它可能会变得非常昂贵。
    • 我要从中随机选择的对象是网格中的单元格,因此为它们提供唯一 ID 应该很容易(x 坐标到字符串 + y 坐标到字符串? ) 那么如果我想“根据 HashSet 的实际键选择随机数”,我是否需要在 Cell 类中覆盖 GetHashCode?​​span>
    • @ChristianGeese 您现在使用什么键?整个细胞?那是什么类型的?
    • 是的,整个细胞。不确定“类型”是什么意思。它只是一个包含一些信息的自定义类,没有继承。
    猜你喜欢
    • 1970-01-01
    • 2017-12-11
    • 2018-04-18
    • 2011-10-20
    • 1970-01-01
    • 1970-01-01
    • 2022-01-22
    • 2015-09-22
    • 1970-01-01
    相关资源
    最近更新 更多