【问题标题】:Given a list of length n select k random elements using C#给定长度为 n 的列表,使用 C# 选择 k 个随机元素
【发布时间】:2013-07-20 15:17:07
【问题描述】:

我找到了这篇文章:

Efficiently selecting a set of random elements from a linked list

但这意味着,为了接近样本中的真正随机性,我必须遍历所有元素,将它们与随机数一起放入内存中,然后进行排序。我这里有大量的项目(数百万) - 有没有更有效的方法来解决这个问题?

【问题讨论】:

  • 使用 c# 列表不同于使用链接中描述的链表。
  • 我们需要更多信息。您是否已经在内存中的可变集合中拥有元素?还需要保留原订单吗?
  • 根据已经请求的信息,您最好的方法可能是执行部分Fisher-Yates Shuffle,在k 迭代后停止。
  • 您在寻找唯一索引处的元素吗?您在寻找独特的元素值吗?您需要按值排序或按索引排序的元素吗?
  • @JonSkeet - 我正在使用 LINQ,所以我认为这意味着如果需要它们可以在内存中。它们位于 Queryable DbSet 中。我不需要保留原始顺序 - 我宁愿不要,以随机性的名义。

标签: c# .net algorithm


【解决方案1】:

我建议像编写修改后的 Fisher-Yates shuffle 一样简单地打乱元素,但只需打乱第一个 k 元素。例如:

public static void PartialShuffle<T>(IList<T> source, int count, Random random)
{
    for (int i = 0; i < count; i++)
    {
        // Pick a random element out of the remaining elements,
        // and swap it into place.
        int index = i + random.Next(source.Count - i);
        T tmp = source[index];
        source[index] = source[i];
        source[i] = tmp;
    }
}

调用此方法后,第一个count元素将从原始列表中随机选取。

请注意,我已将Random 指定为参数,以便您可以重复使用相同的参数。不过要小心线程 - 请参阅我的 article on randomness 了解更多信息。

【讨论】:

  • 有没有办法让这个算法适应加权概率列表?
  • @Iravanchi:你想要对所有权重求和,这样当你选择一个随机数时,你可以计算出对应的值......但是当交换值时,你' d 也需要交换权重。我认为很难做到非常有效(你不能轻易地使用权重的累积列表,这样可以很容易地找到相应的值 - 更新累积列表会很痛苦)但其余的都可以.
【解决方案2】:

看看这个扩展方法http://extensionmethod.net/csharp/ienumerable-t/shuffle。您可以添加 Skip() Take() 类型以将值从最终列表中分页。

【讨论】:

    【解决方案3】:

    如果元素可以在内存中,先放到内存中

    List<Element> elements = dbContext.Select<Element>();
    

    现在您知道元素的数量了。创建一组唯一索引。

    var random = new Random();
    var indexes = new HashSet<int>();
    while (indexes.Count < k) {
        indexes.Add(random.Next(elements.Count));
    }
    

    现在你可以从列表中读取元素了

    var randomElements = indexes.Select(i => elements[i]);
    

    我假设数据库包含独特的元素。如果不是这种情况,您将不得不创建一个HashSet&lt;Elements&gt;,或者在从数据库查询时附加.Distinct()


    更新

    正如 Patricia Shanahan 所说,如果 k 与 n 相比较小,则此方法将很有效。如果不是这样,我建议选择一组要排除的 n - k 个索引

    var random = new Random();
    var indexes = new HashSet<int>();
    IEnumerable<Element> randomElements;
    
    if (k <= elements.Count / 2) {
        while (indexes.Count < k) {
            indexes.Add(random.Next(elements.Count));
        }
        randomElements = indexes.Select(i => elements[i]);
    } else {
        while (indexes.Count < elements.Count - k) {
            indexes.Add(random.Next(elements.Count));
        }
        randomElements = elements
            .Select((e,i) => indexes.Contains(i) ? null : elements[i])
            .Where(e => e != null);
    }
    

    【讨论】:

    • 如果 k 比 n 小,这种方法会很好地工作,因此很少会重复选择相同的元素。如果 k 接近 n,它可能会进行大量迭代。
    • 正如 Patricia 所说,这不是一个好方法。最好进行部分洗牌 - 请参阅我的答案以获取示例代码。
    • @JonSkeet:如果 k 接近 n,您可以选择 n-k 个随机索引来排除。
    • @OlivierJacot-Descombes:确实如此 - 但在这一点上,它比部分洗牌更复杂效率更低。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-01-09
    • 2017-01-26
    • 1970-01-01
    • 2018-09-21
    • 2015-01-16
    • 1970-01-01
    • 2013-10-19
    相关资源
    最近更新 更多