快速从C#散列集中获取随机元素

快速从C#散列集中获取随机元素,c#,performance,random,hashset,C#,Performance,Random,Hashset,我需要存储一组元素。我需要的是功能 移除(单个)元件和 添加(组)元素和 每个对象只能在集合中存在一次,并且 从集合中获取一个随机元素 我选择了HashSet(C#),因为它采用了fast方法来删除元素(HashSet.remove(element)),添加集合(HashSet.UnionWith(anotherHashSet)),并且HashSet的性质保证没有重复项,所以需要考虑1到3 我发现获得随机元素的唯一方法是 Object object = hashSet.ElementAt(rnd

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

  • 移除(单个)元件和
  • 添加(组)元素和
  • 每个对象只能在集合中存在一次,并且
  • 从集合中获取一个随机元素
  • 我选择了HashSet(C#),因为它采用了fast方法来删除元素(HashSet.remove(element)),添加集合(HashSet.UnionWith(anotherHashSet)),并且HashSet的性质保证没有重复项,所以需要考虑1到3

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

    Object object = hashSet.ElementAt(rnd.Next(hashSet.Count));
    
    但这是非常缓慢的,因为我对地图的每个像素都调用一次(从多个起点创建一个随机的泛洪填充;mapsize 500x500,但我现在想变大),并且哈希集包含相当多的项。(快速测试显示,在再次缩小之前,它最多会放大5752个条目。)

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

    我意识到在一个大的hashset上执行500x500操作并不容易,但其他操作(Remove和UnionWith)的调用频率与ElementAt相同,因此主要问题似乎是操作,而不是调用的数量

    我模模糊糊地理解为什么从哈希集中获取某个元素非常昂贵(与从列表或其他有序数据结构中获取元素相比,我只想随机选取。这真的很难,而且没有办法解决吗?有更好的数据结构适合我吗

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

    将HashSet强制转换为数组并从中拾取我的随机元素并没有什么帮助,因为虽然从数组中拾取随机元素很快,但将HashSet强制转换为数组首先需要比单独运行HashSet.ElementAt更长的时间


    如果您想更好地理解我要做的事情:

    基本问题是索引

    在数组或列表中,数据通过其坐标索引(通常只是一个简单的int索引)进行索引。在
    哈希集中,您可以自己选择索引(即键)。不过,副作用是,没有“坐标”(即“索引3处的元素”问题)真的没有意义。它的实际实现方式是一项一项地枚举整个
    哈希集,然后返回第n项。这意味着要获得第1000项,还必须枚举之前的所有999项。这很痛苦

    解决此问题的最佳方法是根据
    散列集
    的实际键选择随机键。当然,只有在这样选择随机键是合理的情况下,这才有效

    如果您无法以令人满意的方式随机选取密钥,您可能需要保留两个单独的列表-每当您将新项添加到
    哈希集
    ,将其密钥添加到
    列表
    ;然后您可以轻松地从
    列表
    中选取一个随机密钥,并按照它进行操作。根据您的要求,重复项可能不太重要问题

    当然,如果只执行一次枚举,则可以在
    元素上保存
    枚举。例如,在搜索
    哈希集之前,您可以将其转换为
    列表
    。当然,这只有在同时拾取多个随机索引时才有意义(例如,如果您一次随机选择5个索引,平均可以节省约五分之一的时间)-如果您总是选择一个,那么修改
    哈希集并选择另一个,这将毫无帮助

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

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

    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)索引。它只适用于
    对象
    键和值,但没有通用版本。

    您要删除什么?它只是随机找到的元素,还是任意的?为什么不使用哈希集进行所有添加和删除操作,然后在您要进行随机像素获取之前,只需转换到列表一次?使用该列表,然后hr然后离开。除非你需要同时添加、删除和获取随机元素…@spender我删除随机找到的元素only@Baldrick我担心是后者。循环基本上是:选择一个随机单元(散列集包含随机泛洪填充可以扩散到的所有可能单元,即“边缘”)->填充它->查找相邻的空单元格并将它们添加到哈希集中->从哈希集中删除填充的单元格->再次循环,直到哈希集为空。我觉得二维链表将是您的朋友。是的,我想用一个列表和一个哈希集对其进行索引。@spender是的,如果您不关心删除垃圾,这可以很好地工作。但是,如果这样做,它可能会变得非常昂贵。我要从中随机选取一个对象的对象是网格中的单元格,因此应该很容易给它们一个唯一的ID(x坐标到字符串+y坐标到字符串?),所以如果我想重写Cell类中的GetHashCode吗“根据哈希集的实际键随机选取”?@Christiangese你现在使用的键是什么?整个单元格?那是什么类型的?是的,整个单元格。不确定“类型”是什么意思。它只是一个自定义类,包含一些信息,没有继承