从 C# 中的 List<T> 中选择 N 个随机元素

Posted

技术标签:

【中文标题】从 C# 中的 List<T> 中选择 N 个随机元素【英文标题】:Select N random elements from a List<T> in C# 【发布时间】:2008-09-07 03:12:28 【问题描述】:

我需要一个快速算法来从通用列表中选择 5 个随机元素。例如,我想从List&lt;string&gt; 中获取 5 个随机元素。

【问题讨论】:

随机,您是指包容性还是独家性? IOW,可以多次选择相同的元素吗? (真正随机)或者一旦一个元素被挑选出来,它应该不再从可用池中挑选出来吗? 非常相似:Pick N items at random from sequence of unknown length, Algorithm to select a single, random combination of values? ???你只是洗牌拿第一个N..为什么这里有这么多讨论? @Fattie 这适用于洗牌效率极低的情况(例如,列表很大)或者您不允许修改原始列表的顺序。 @uckelman 这个问题根本没有说明这一点。对于非常大的集合(请注意,在这种情况下使用“List”之类的东西是完全不可想象的),它取决于大小域。请注意,勾选的答案是完全错误的。 【参考方案1】:

使用 linq:

YourList.OrderBy(x => rnd.Next()).Take(5)

【讨论】:

+1 但是如果两个元素从 rnd.Next() 或类似的获得相同的数字,那么第一个将被选中,而第二个可能不会(如果不需要更多元素)。不过,根据使用情况,它是足够随机的。 我认为 order by 是 O(n log(n)),所以如果代码简单性是主要问题(即使用小列表),我会选择此解决方案。 但这不是枚举和排序整个列表吗?除非,“快速”,OP 的意思是“简单”,而不是“性能”...... 这只有在 OrderBy() 只为每个元素调用一次键选择器时才有效。如果它在想要执行两个元素之间的比较时调用它,那么它每次都会返回一个不同的值,这将搞砸排序。 [文档] (msdn.microsoft.com/en-us/library/vstudio/…) 没有说明它是做什么的。 注意YourList 是否有很多项目,但您只想选择几个。在这种情况下,这不是一种有效的方法。【参考方案2】:

遍历每个元素,使选择的概率=(需要的数量)/(剩下的数量)

因此,如果您有 40 个项目,则第一个项目将有 5/40 的机会被选中。如果是,则下一个有 4/39 的机会,否则有 5/39 的机会。到最后,您将拥有 5 件物品,而且通常您会在此之前拥有所有物品。

这种技术称为selection sampling,是Reservoir Sampling 的特例。它的性能类似于 shuffle 输入,但当然允许在不修改原始数据的情况下生成样本。

【讨论】:

【参考方案3】:
public static List<T> GetRandomElements<T>(this IEnumerable<T> list, int elementsCount)

    return list.OrderBy(arg => Guid.NewGuid()).Take(elementsCount).ToList();

【讨论】:

【参考方案4】:

这实际上是一个比听起来更难的问题,主要是因为许多数学上正确的解决方案实际上无法让您找到所有的可能性(更多内容见下文)。

首先,这里有一些易于实现的正确随机数生成器:

(0) 凯尔的答案,即 O(n)。

(1) 生成n对[(0, rand), (1, rand), (2, rand), ...]的列表,将它们按第二个坐标排序,使用前k个(对于你,k=5) 索引来获得你的随机子集。我认为这很容易实现,虽然是 O(n log n) 时间。

(2) 初始化一个空列表 s = [] 将增长为 k 个随机元素的索引。在 0, 1, 2, ..., n-1 中随机选择一个数 r,r = rand % n,并将其添加到 s。接下来取 r = rand % (n-1) 并坚持 s;将 s 中小于它的 # 个元素添加到 r 以避免冲突。接下来取 r = rand % (n-2),并做同样的事情,等等,直到你在 s 中有 k 个不同的元素。这具有最坏情况下的运行时间 O(k^2)。所以对于 k

@Kyle - 你是对的,再想一想我同意你的回答。起初我匆忙阅读它,并错误地认为您是在指示以固定概率 k/n 顺序选择每个元素,这本来是错误的 - 但您的自适应方法对我来说似乎是正确的。对此感到抱歉。

好的,现在是踢球者:渐近(对于固定的 k,n 增长),有 n^k/k!从 n 个元素中选择 k 个元素子集 [这是 (n 选择 k) 的近似值]。如果 n 很大,并且 k 不是很小,那么这些数字就很大。在任何标准的 32 位随机数生成器中,您希望的最佳循环长度是 2^32 = 256^4。因此,如果我们有一个包含 1000 个元素的列表,并且我们想随机选择 5 个,那么标准的随机数生成器不可能满足所有可能性。但是,只要您对适合较小集合的选择感到满意,并且总是“看起来”随机,那么这些算法应该没问题。

附录:写完之后,我意识到要正确实现想法(2)很棘手,所以我想澄清一下这个答案。要获得 O(k log k) 时间,您需要一个支持 O(log m) 搜索和插入的类似数组的结构 - 平衡二叉树可以做到这一点。使用这样的结构来构建一个名为 s 的数组,这里是一些伪 Python:

# Returns a container s with k distinct random numbers from 0, 1, ..., n-1
def ChooseRandomSubset(n, k):
  for i in range(k):
    r = UniformRandom(0, n-i)                 # May be 0, must be < n-i
    q = s.FirstIndexSuchThat( s[q] - q > r )  # This is the search.
    s.InsertInOrder(q ? r + q : r + len(s))   # Inserts right before q.
  return s

我建议通过几个示例案例来看看它如何有效地实现上述英文解释。

【讨论】:

对于 (1) 您可以比排序更快地对列表进行洗牌,对于 (2) 您将使用 % 来偏向您的分布 鉴于您对 rng 的循环长度提出的反对意见,我们有什么方法可以构造一个算法,以相等的概率选择所有集合? 对于 (1),为了提高 O(n log(n)) 您可以使用选择排序来查找 k 个最小元素。这将在 O(n*k) 中运行。 @Jonah:我想是的。假设我们可以组合多个独立的随机数生成器来创建一个更大的随机数生成器 (crypto.stackexchange.com/a/27431)。那么你只需要一个足够大的范围来处理有问题的列表的大小。【参考方案5】:

我认为选择的答案是正确的,而且非常甜蜜。不过我以不同的方式实现了它,因为我还希望结果是随机顺序的。

    static IEnumerable<SomeType> PickSomeInRandomOrder<SomeType>(
        IEnumerable<SomeType> someTypes,
        int maxCount)
    
        Random random = new Random(DateTime.Now.Millisecond);

        Dictionary<double, SomeType> randomSortTable = new Dictionary<double,SomeType>();

        foreach(SomeType someType in someTypes)
            randomSortTable[random.NextDouble()] = someType;

        return randomSortTable.OrderBy(KVP => KVP.Key).Take(maxCount).Select(KVP => KVP.Value);
    

【讨论】:

太棒了!真的帮了我大忙! 你有什么理由不使用基于 Environment.TickCount 与 DateTime.Now.Millisecond 的 new Random() 吗? 不,只是不知道存在默认值。 好吧,晚了一年,但是...这不是@ersin 更短的答案吗,如果你得到一个重复的随机数,它不会失败(Ersin 会偏向于重复对的第一项) Random random = new Random(DateTime.Now.Millisecond); 每次通话肯定错误。每次创建Random 的新实例会降低实际随机性。使用它的static readonly 实例,最好使用默认构造函数构造。【参考方案6】:

我刚刚遇到了这个问题,又在google上搜索了一些随机洗牌的问题:http://en.wikipedia.org/wiki/Fisher-Yates_shuffle

要完全随机打乱您的列表(就地),您可以这样做:

洗牌一个有 n 个元素的数组 a(索引 0..n-1):

  for i from n − 1 downto 1 do
       j ← random integer with 0 ≤ j ≤ i
       exchange a[j] and a[i]

如果你只需要前5个元素,那么不需要从n-1一直运行i到1,你只需要运行到n-5(即:n-5)

假设你需要 k 个项目,

这就变成了:

  for (i = n − 1; i >= n-k; i--)
  
       j = random integer with 0 ≤ j ≤ i
       exchange a[j] and a[i]
  

每个被选中的项目都向数组末尾交换,因此选中的 k 个元素是数组的最后 k 个元素。

这需要时间 O(k),其中 k 是您需要的随机选择元素的数量。

此外,如果您不想修改您的初始列表,您可以在一个临时列表中写下您的所有交换,反转该列表,然后再次应用它们,从而执行反向交换集并返回您的初始在不改变 O(k) 运行时间的情况下列出。

最后,对于真正的坚持者,如果 (n == k),你应该停在 1,而不是 n-k,因为随机选择的整数将始终为 0。

【讨论】:

【参考方案7】:

您可以使用它,但排序将发生在客户端

 .AsEnumerable().OrderBy(n => Guid.NewGuid()).Take(5);

【讨论】:

同意。它可能不是最好的或最随机的,但对于绝大多数人来说这已经足够了。 因为Guids are guaranteed to be unique, not random而被否决。【参考方案8】:

来自Dragons in the Algorithm,C# 中的一种解释:

int k = 10; // items to select
var items = new List<int>(new[]  1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 );
var selected = new List<int>();
double needed = k;
double available = items.Count;
var rand = new Random();
while (selected.Count < k) 
   if( rand.NextDouble() < needed / available ) 
      selected.Add(items[(int)available-1])
      needed--;
   
   available--;

此算法将选择项目列表的唯一索引。

【讨论】:

只在列表中获取足够的项目,而不是随机获取。 这个实现被破坏了,因为使用var导致neededavailable都是整数,这使得needed/available总是0。 这似乎是已接受答案的实现。【参考方案9】:

正在考虑@JohnShedletsky 在accepted answer 上就(释义)发表的评论:

你应该能够在 O(subset.Length) 中做到这一点,而不是 O(originalList.Length)

基本上,您应该能够生成subset 随机索引,然后从原始列表中提取它们。

方法

public static class EnumerableExtensions 

    public static Random randomizer = new Random(); // you'd ideally be able to replace this with whatever makes you comfortable

    public static IEnumerable<T> GetRandom<T>(this IEnumerable<T> list, int numItems) 
        return (list as T[] ?? list.ToArray()).GetRandom(numItems);

        // because ReSharper whined about duplicate enumeration...
        /*
        items.Add(list.ElementAt(randomizer.Next(list.Count()))) ) numItems--;
        */
    

    // just because the parentheses were getting confusing
    public static IEnumerable<T> GetRandom<T>(this T[] list, int numItems) 
        var items = new HashSet<T>(); // don't want to add the same item twice; otherwise use a list
        while (numItems > 0 )
            // if we successfully added it, move on
            if( items.Add(list[randomizer.Next(list.Length)]) ) numItems--;

        return items;
    

    // and because it's really fun; note -- you may get repetition
    public static IEnumerable<T> PluckRandomly<T>(this IEnumerable<T> list) 
        while( true )
            yield return list.ElementAt(randomizer.Next(list.Count()));
    


如果您想提高效率,您可能会使用 索引HashSet,而不是实际的列表元素(以防您有复杂的类型或昂贵的比较);

单元测试

并确保我们没有任何碰撞等。

[TestClass]
public class RandomizingTests : UnitTestBase 
    [TestMethod]
    public void GetRandomFromList() 
        this.testGetRandomFromList((list, num) => list.GetRandom(num));
    

    [TestMethod]
    public void PluckRandomly() 
        this.testGetRandomFromList((list, num) => list.PluckRandomly().Take(num), requireDistinct:false);
    

    private void testGetRandomFromList(Func<IEnumerable<int>, int, IEnumerable<int>> methodToGetRandomItems, int numToTake = 10, int repetitions = 100000, bool requireDistinct = true) 
        var items = Enumerable.Range(0, 100);
        IEnumerable<int> randomItems = null;

        while( repetitions-- > 0 ) 
            randomItems = methodToGetRandomItems(items, numToTake);
            Assert.AreEqual(numToTake, randomItems.Count(),
                            "Did not get expected number of items 0; failed at 1 repetition--", numToTake, repetitions);
            if(requireDistinct) Assert.AreEqual(numToTake, randomItems.Distinct().Count(),
                            "Collisions (non-unique values) found, failed at 0 repetition--", repetitions);
            Assert.IsTrue(randomItems.All(o => items.Contains(o)),
                        "Some unknown values found; failed at 0 repetition--", repetitions);
        
    

【讨论】:

好主意,但有问题。 (1) 如果您的较大列表很大(例如从数据库中读取),那么您会实现整个列表,这可能会超出内存。 (2) 如果 K 接近于 N,那么您将在循环中大量搜索无人认领的索引,从而导致代码需要不可预测的时间。这些问题是可以解决的。 我对抖动问题的解决方案是这样的:如果 K = N/2,选择不应该保留的索引,而不是应该保留的索引。仍有一些颠簸,但要少得多。 还注意到这会改变枚举项目的顺序,这在某些情况下可能是可以接受的,但在其他情况下则不行。 平均而言,对于 K = N/2(Paul 建议改进的最坏情况),(改进的抖动)算法似乎需要 ~0.693*N 次迭代。现在做一个速度比较。这比公认的答案好吗?适用于哪些样本量?【参考方案10】:

12 年过去了,这个问题仍然存在,我没有找到我喜欢的 Kyle 解决方案的实现,所以这里是:

public IEnumerable<T> TakeRandom<T>(IEnumerable<T> collection, int take)

    var random = new Random();
    var available = collection.Count();
    var needed = take;
    foreach (var item in collection)
    
        if (random.Next(available) < needed)
        
            needed--;
            yield return item;
            if (needed == 0)
            
                break;
            
        
        available--;
    

【讨论】:

这个真的很有用。谢谢!【参考方案11】:

从一个组中选择 N 个随机项不应该与 order 有任何关系!随机性是关于不可预测性,而不是在一个群体中改变位置。所有处理某种排序的答案肯定比不处理的答案效率低。由于效率是这里的关键,所以我会发布一些不会过多改变项目顺序的内容。

1) 如果您需要 true 随机值,这意味着可以选择的元素没有限制(即,一旦选择的项目可以重新选择):

public static List<T> GetTrueRandom<T>(this IList<T> source, int count, 
                                       bool throwArgumentOutOfRangeException = true)

    if (throwArgumentOutOfRangeException && count > source.Count)
        throw new ArgumentOutOfRangeException();

    var randoms = new List<T>(count);
    randoms.AddRandomly(source, count);
    return randoms;

如果你关闭了异常标志,那么你可以选择任意次数的随机项目。

如果你有 1, 2, 3, 4 ,那么它可以给出 1, 4, 4 , 1, 4, 3 等 3 个项目甚至 1, 4, 3, 2, 4 5 个项目!

这应该很快,因为它没有什么要检查的。

2) 如果您需要群组中的个人成员且不重复,那么我会依靠字典(正如许多人已经指出的那样)。

public static List<T> GetDistinctRandom<T>(this IList<T> source, int count)

    if (count > source.Count)
        throw new ArgumentOutOfRangeException();

    if (count == source.Count)
        return new List<T>(source);

    var sourceDict = source.ToIndexedDictionary();

    if (count > source.Count / 2)
    
        while (sourceDict.Count > count)
            sourceDict.Remove(source.GetRandomIndex());

        return sourceDict.Select(kvp => kvp.Value).ToList();
    

    var randomDict = new Dictionary<int, T>(count);
    while (randomDict.Count < count)
    
        int key = source.GetRandomIndex();
        if (!randomDict.ContainsKey(key))
            randomDict.Add(key, sourceDict[key]);
    

    return randomDict.Select(kvp => kvp.Value).ToList();

这里的代码比其他字典方法要长一些,因为我不仅要添加,还要从列表中删除,所以它有点两个循环。您可以在这里看到,当count 等于source.Count 时,我根本没有重新排序任何东西。那是因为我认为随机性应该在返回的集合中作为一个整体。我的意思是如果你想要来自 1, 2, 3, 4, 55 个随机项目,它是 1, 3, 4, 2, 5 还是 1, 2, 3, 4, 5 无关紧要,但如果你需要来自 1, 2, 3, 4, 54 个项目相同的集合,那么它应该在1, 2, 3, 41, 3, 5, 22, 3, 5, 4 等中意外产生。其次,当要返回的随机项目数超过原始组的一半时,则更容易删除source.Count - count 组中的项目而不是添加 count 项目。出于性能原因,我使用 source 而不是 sourceDict 在 remove 方法中获取随机索引。

因此,如果您有 1, 2, 3, 4 ,这可能会以 1, 2, 3 , 3, 4, 1 等 3 个项目结束。

3) 如果考虑到原始组中的重复项,您需要与组中真正不同的随机值,那么您可以使用与上述相同的方法,但 HashSet 将比字典轻。

public static List<T> GetTrueDistinctRandom<T>(this IList<T> source, int count, 
                                               bool throwArgumentOutOfRangeException = true)

    if (count > source.Count)
        throw new ArgumentOutOfRangeException();

    var set = new HashSet<T>(source);

    if (throwArgumentOutOfRangeException && count > set.Count)
        throw new ArgumentOutOfRangeException();

    List<T> list = hash.ToList();

    if (count >= set.Count)
        return list;

    if (count > set.Count / 2)
    
        while (set.Count > count)
            set.Remove(list.GetRandom());

        return set.ToList();
    

    var randoms = new HashSet<T>();
    randoms.AddRandomly(list, count);
    return randoms.ToList();

randoms 变量设为HashSet 以避免在Random.Next 可以产生相同值的极少数情况下添加重复项,尤其是当输入列表较小时。

所以 1, 2, 2, 4 => 3 个随机项 => 1, 2, 4 而从不 1, 2, 2

1, 2, 2, 4 => 4 个随机项 => 例外!!或 1, 2, 4 取决于设置的标志。

我使用过的一些扩展方法:

static Random rnd = new Random();
public static int GetRandomIndex<T>(this ICollection<T> source)

    return rnd.Next(source.Count);


public static T GetRandom<T>(this IList<T> source)

    return source[source.GetRandomIndex()];


static void AddRandomly<T>(this ICollection<T> toCol, IList<T> fromList, int count)

    while (toCol.Count < count)
        toCol.Add(fromList.GetRandom());


public static Dictionary<int, T> ToIndexedDictionary<T>(this IEnumerable<T> lst)

    return lst.ToIndexedDictionary(t => t);


public static Dictionary<int, T> ToIndexedDictionary<S, T>(this IEnumerable<S> lst, 
                                                           Func<S, T> valueSelector)

    int index = -1;
    return lst.ToDictionary(t => ++index, valueSelector);

如果列表中的数十个项目都必须迭代 10000 次,那么您可能希望 faster random class 而不是 System.Random,但我认为这没什么大不了的后者很可能永远不是瓶颈,它足够快..

编辑:如果您还需要重新安排退货的顺序,那么没有什么比 dhakim's Fisher-Yates approach 更好的了 - 简短、甜蜜和简单..

【讨论】:

【参考方案12】:

我结合了上述几个答案来创建一个懒惰评估的扩展方法。我的测试表明,Kyle 的方法 (Order(N)) 比 drzaus 使用集合来提出要选择的随机索引 (Order(K)) 慢很多倍。前者对随机数生成器执行更多调用,并且对项目进行更多次迭代。

我的实施目标是:

1) 如果给定的 IEnumerable 不是 IList,则不要实现完整列表。如果给我一个包含无数个项目的序列,我不想耗尽内存。将 Kyle 的方法用于在线解决方案。

2) 如果我可以判断它是一个 IList,请使用 drzaus 的方法,但要有所改变。如果 K 超过 N 的一半,我会冒着颠簸的风险,因为我一次又一次地选择许多随机索引并且不得不跳过它们。因此,我编写了一个不保留的索引列表。

3) 我保证物品将按照遇到的顺序退回。凯尔的算法不需要改变。 drzaus 的算法要求我不要按照选择随机索引的顺序发出项目。我将所有索引收集到一个 SortedSet 中,然后按排序索引顺序发出项目。

4) 如果 K 比 N 大,并且我反转了集合的意义,那么我枚举所有项目并测试索引是否不在集合中。这意味着 我失去了 Order(K) 运行时间,但由于在这些情况下 K 接近于 N,所以我不会失去太多。

代码如下:

    /// <summary>
    /// Takes k elements from the next n elements at random, preserving their order.
    /// 
    /// If there are fewer than n elements in items, this may return fewer than k elements.
    /// </summary>
    /// <typeparam name="TElem">Type of element in the items collection.</typeparam>
    /// <param name="items">Items to be randomly selected.</param>
    /// <param name="k">Number of items to pick.</param>
    /// <param name="n">Total number of items to choose from.
    /// If the items collection contains more than this number, the extra members will be skipped.
    /// If the items collection contains fewer than this number, it is possible that fewer than k items will be returned.</param>
    /// <returns>Enumerable over the retained items.
    /// 
    /// See http://***.com/questions/48087/select-a-random-n-elements-from-listt-in-c-sharp for the commentary.
    /// </returns>
    public static IEnumerable<TElem> TakeRandom<TElem>(this IEnumerable<TElem> items, int k, int n)
    
        var r = new FastRandom();
        var itemsList = items as IList<TElem>;

        if (k >= n || (itemsList != null && k >= itemsList.Count))
            foreach (var item in items) yield return item;
        else
          
            // If we have a list, we can infer more information and choose a better algorithm.
            // When using an IList, this is about 7 times faster (on one benchmark)!
            if (itemsList != null && k < n/2)
            
                // Since we have a List, we can use an algorithm suitable for Lists.
                // If there are fewer than n elements, reduce n.
                n = Math.Min(n, itemsList.Count);

                // This algorithm picks K index-values randomly and directly chooses those items to be selected.
                // If k is more than half of n, then we will spend a fair amount of time thrashing, picking
                // indices that we have already picked and having to try again.   
                var invertSet = k >= n/2;  
                var positions = invertSet ? (ISet<int>) new HashSet<int>() : (ISet<int>) new SortedSet<int>();

                var numbersNeeded = invertSet ? n - k : k;
                while (numbersNeeded > 0)
                    if (positions.Add(r.Next(0, n))) numbersNeeded--;

                if (invertSet)
                
                    // positions contains all the indices of elements to Skip.
                    for (var itemIndex = 0; itemIndex < n; itemIndex++)
                    
                        if (!positions.Contains(itemIndex))
                            yield return itemsList[itemIndex];
                    
                
                else
                
                    // positions contains all the indices of elements to Take.
                    foreach (var itemIndex in positions)
                        yield return itemsList[itemIndex];              
                
            
            else
            
                // Since we do not have a list, we will use an online algorithm.
                // This permits is to skip the rest as soon as we have enough items.
                var found = 0;
                var scanned = 0;
                foreach (var item in items)
                
                    var rand = r.Next(0,n-scanned);
                    if (rand < k - found)
                    
                        yield return item;
                        found++;
                    
                    scanned++;
                    if (found >= k || scanned >= n)
                        break;
                
            
          
     

我使用了一个专门的随机数生成器,但如果你愿意,你也可以使用 C# 的 Random。 (FastRandom 由 Colin Green 编写,属于 SharpNEAT 的一部分。它的周期为 2^128-1,优于许多 RNG。)

这里是单元测试:

[TestClass]
public class TakeRandomTests

    /// <summary>
    /// Ensure that when randomly choosing items from an array, all items are chosen with roughly equal probability.
    /// </summary>
    [TestMethod]
    public void TakeRandom_Array_Uniformity()
    
        const int numTrials = 2000000;
        const int expectedCount = numTrials/20;
        var timesChosen = new int[100];
        var century = new int[100];
        for (var i = 0; i < century.Length; i++)
            century[i] = i;

        for (var trial = 0; trial < numTrials; trial++)
        
            foreach (var i in century.TakeRandom(5, 100))
                timesChosen[i]++;
        
        var avg = timesChosen.Average();
        var max = timesChosen.Max();
        var min = timesChosen.Min();
        var allowedDifference = expectedCount/100;
        AssertBetween(avg, expectedCount - 2, expectedCount + 2, "Average");
        //AssertBetween(min, expectedCount - allowedDifference, expectedCount, "Min");
        //AssertBetween(max, expectedCount, expectedCount + allowedDifference, "Max");

        var countInRange = timesChosen.Count(i => i >= expectedCount - allowedDifference && i <= expectedCount + allowedDifference);
        Assert.IsTrue(countInRange >= 90, String.Format("Not enough were in range: 0", countInRange));
    

    /// <summary>
    /// Ensure that when randomly choosing items from an IEnumerable that is not an IList, 
    /// all items are chosen with roughly equal probability.
    /// </summary>
    [TestMethod]
    public void TakeRandom_IEnumerable_Uniformity()
    
        const int numTrials = 2000000;
        const int expectedCount = numTrials / 20;
        var timesChosen = new int[100];

        for (var trial = 0; trial < numTrials; trial++)
        
            foreach (var i in Range(0,100).TakeRandom(5, 100))
                timesChosen[i]++;
        
        var avg = timesChosen.Average();
        var max = timesChosen.Max();
        var min = timesChosen.Min();
        var allowedDifference = expectedCount / 100;
        var countInRange =
            timesChosen.Count(i => i >= expectedCount - allowedDifference && i <= expectedCount + allowedDifference);
        Assert.IsTrue(countInRange >= 90, String.Format("Not enough were in range: 0", countInRange));
    

    private IEnumerable<int> Range(int low, int count)
    
        for (var i = low; i < low + count; i++)
            yield return i;
    

    private static void AssertBetween(int x, int low, int high, String message)
    
        Assert.IsTrue(x > low, String.Format("Value 0 is less than lower limit of 1. 2", x, low, message));
        Assert.IsTrue(x < high, String.Format("Value 0 is more than upper limit of 1. 2", x, high, message));
    

    private static void AssertBetween(double x, double low, double high, String message)
    
        Assert.IsTrue(x > low, String.Format("Value 0 is less than lower limit of 1. 2", x, low, message));
        Assert.IsTrue(x < high, String.Format("Value 0 is more than upper limit of 1. 2", x, high, message));
    

【讨论】:

测试没有错误吗?你有if (itemsList != null &amp;&amp; k &lt; n/2) 这意味着在if 内部invertSet 总是false 这意味着永远不会使用逻辑。【参考方案13】:

从@ers 的回答扩展,如果有人担心 OrderBy 可能有不同的实现,这应该是安全的:

// Instead of this
YourList.OrderBy(x => rnd.Next()).Take(5)

// Temporarily transform 
YourList
    .Select(v => new v, i = rnd.Next()) // Associate a random index to each entry
    .OrderBy(x => x.i).Take(5) // Sort by (at this point fixed) random index 
    .Select(x => x.v); // Go back to enumerable of entry

【讨论】:

【参考方案14】:

这里有一个基于 Fisher-Yates Shuffle 的实现,其算法复杂度为 O(n),其中 n 是子集或样本大小,而不是列表大小,正如 John Shedletsky 指出的那样。

public static IEnumerable<T> GetRandomSample<T>(this IList<T> list, int sampleSize)

    if (list == null) throw new ArgumentNullException("list");
    if (sampleSize > list.Count) throw new ArgumentException("sampleSize may not be greater than list count", "sampleSize");
    var indices = new Dictionary<int, int>(); int index;
    var rnd = new Random();

    for (int i = 0; i < sampleSize; i++)
    
        int j = rnd.Next(i, list.Count);
        if (!indices.TryGetValue(j, out index)) index = j;

        yield return list[index];

        if (!indices.TryGetValue(i, out index)) index = i;
        indices[j] = index;
    

【讨论】:

【参考方案15】:

我使用的简单解决方案(可能不适合大型列表): 将列表复制到临时列表中,然后在循环中从临时列表中随机选择项目并将其放入选定项目列表中,同时将其从临时列表中删除(因此无法重新选择)。

例子:

List<Object> temp = OriginalList.ToList();
List<Object> selectedItems = new List<Object>();
Random rnd = new Random();
Object o;
int i = 0;
while (i < NumberOfSelectedItems)

            o = temp[rnd.Next(temp.Count)];
            selectedItems.Add(o);
            temp.Remove(o);
            i++;
 

【讨论】:

如此频繁地从列表的中间删除会很昂贵。您可以考虑将链表用于需要大量删除的算法。或者等效地,将已删除的项目替换为空值,但是当您选择已删除的项目并且必须再次选择时,您会有点颠簸。【参考方案16】:

这是我第一次剪辑时能想到的最好的:

public List<String> getRandomItemsFromList(int returnCount, List<String> list)

    List<String> returnList = new List<String>();
    Dictionary<int, int> randoms = new Dictionary<int, int>();

    while (randoms.Count != returnCount)
    
        //generate new random between one and total list count
        int randomInt = new Random().Next(list.Count);

        // store this in dictionary to ensure uniqueness
        try
        
            randoms.Add(randomInt, randomInt);
        
        catch (ArgumentException aex)
        
            Console.Write(aex.Message);
         //we can assume this element exists in the dictonary already 

        //check for randoms length and then iterate through the original list 
        //adding items we select via random to the return list
        if (randoms.Count == returnCount)
        
            foreach (int key in randoms.Keys)
                returnList.Add(list[randoms[key]]);

            break; //break out of _while_ loop
        
    

    return returnList;

使用 1 范围内的随机列表 - 总列表计数然后简单地将这些项目拉到列表中似乎是最好的方法,但使用字典来确保唯一性是我仍在考虑的事情。

另外请注意我使用了一个字符串列表,根据需要替换。

【讨论】:

第一枪就成功了!【参考方案17】:

根据 Kyle 的回答,这是我的 c# 实现。

/// <summary>
/// Picks random selection of available game ID's
/// </summary>
private static List<int> GetRandomGameIDs(int count)
       
    var gameIDs = (int[])HttpContext.Current.Application["NonDeletedArcadeGameIDs"];
    var totalGameIDs = gameIDs.Count();
    if (count > totalGameIDs) count = totalGameIDs;

    var rnd = new Random();
    var leftToPick = count;
    var itemsLeft = totalGameIDs;
    var arrPickIndex = 0;
    var returnIDs = new List<int>();
    while (leftToPick > 0)
    
        if (rnd.Next(0, itemsLeft) < leftToPick)
        
            returnIDs .Add(gameIDs[arrPickIndex]);
            leftToPick--;
        
        arrPickIndex++;
        itemsLeft--;
    

    return returnIDs ;

【讨论】:

【参考方案18】:

这种方法可能与凯尔的方法相当。

假设您的列表大小为 n,并且您想要 k 个元素。

Random rand = new Random();
for(int i = 0; k>0; ++i) 

    int r = rand.Next(0, n-i);
    if(r<k) 
    
        //include element i
        k--;
    
 

像魅力一样工作:)

-亚历克斯·吉尔伯特

【讨论】:

这看起来和我一样。比较类似的***.com/a/48141/2449863【参考方案19】:

以下是三种不同方法的基准:

    Kyle 接受的答案的实现。 一种基于随机索引选择和 HashSet 重复过滤的方法,来自drzaus。 Jesús López 发布的一种更具学术性的方法,称为 Fisher–Yates shuffle。

测试将包括使用多种不同的列表大小和选择大小对性能进行基准测试。

我还包括了这三种方法的标准差测量,即随机选择的分布情况。

简而言之,drzaus 的简单解决方案似乎是这三个中最好的整体。选择的答案很棒而且很优雅,但效率不高,因为时间复杂度是基于样本大小而不是选择大小。因此,如果您从长列表中选择少量项目,则将花费更多时间。当然,它仍然比基于完全重新排序的解决方案表现更好。

奇怪的是,这个O(n) 时间复杂度问题是真实的,即使你只在实际返回一个项目时才触摸列表,就像我在我的实现中所做的那样。我唯一能想到的是Random.Next() 非常慢,如果您为每个选定的项目只生成一个随机数,则性能会有所提高。

而且,同样有趣的是,Kyle 解决方案的 StdDev 相对而言要高得多。我不知道为什么;也许问题出在我的实现中。

抱歉,现在开始的长代码和输出;但我希望它有点启发性。另外,如果您在测试或实现中发现任何问题,请告诉我,我会修复它。

static void Main()

    BenchmarkRunner.Run<Benchmarks>();

    new Benchmarks()  ListSize = 100, SelectionSize = 10 
        .BenchmarkStdDev();


[MemoryDiagnoser]
public class Benchmarks

    [Params(50, 500, 5000)]
    public int ListSize;

    [Params(5, 10, 25, 50)]
    public int SelectionSize;

    private Random _rnd;
    private List<int> _list;
    private int[] _hits;

    [GlobalSetup]
    public void Setup()
    
        _rnd = new Random(12345);
        _list = Enumerable.Range(0, ListSize).ToList();
        _hits = new int[ListSize];
    

    [Benchmark]
    public void Test_IterateSelect()
        => Random_IterateSelect(_list, SelectionSize).ToList();

    [Benchmark]
    public void Test_RandomIndices() 
        => Random_RandomIdices(_list, SelectionSize).ToList();

    [Benchmark]
    public void Test_FisherYates() 
        => Random_FisherYates(_list, SelectionSize).ToList();

    public void BenchmarkStdDev()
    
        RunOnce(Random_IterateSelect, "IterateSelect");
        RunOnce(Random_RandomIdices, "RandomIndices");
        RunOnce(Random_FisherYates, "FisherYates");

        void RunOnce(Func<IEnumerable<int>, int, IEnumerable<int>> method, string methodName)
        
            Setup();
            for (int i = 0; i < 1000000; i++)
            
                var selected = method(_list, SelectionSize).ToList();
                Debug.Assert(selected.Count() == SelectionSize);
                foreach (var item in selected) _hits[item]++;
            
            var stdDev = GetStdDev(_hits);
            Console.WriteLine($"StdDev of methodName: stdDev :n (% of average: stdDev / (_hits.Average() / 100) :n)");
        

        double GetStdDev(IEnumerable<int> hits)
        
            var average = hits.Average();
            return Math.Sqrt(hits.Average(v => Math.Pow(v - average, 2)));
        
    

    public IEnumerable<T> Random_IterateSelect<T>(IEnumerable<T> collection, int needed)
    
        var count = collection.Count();
        for (int i = 0; i < count; i++)
        
            if (_rnd.Next(count - i) < needed)
            
                yield return collection.ElementAt(i);
                if (--needed == 0)
                    yield break;
            
        
    

    public IEnumerable<T> Random_RandomIdices<T>(IEnumerable<T> list, int needed)
    
        var selectedItems = new HashSet<T>();
        var count = list.Count();

        while (needed > 0)
            if (selectedItems.Add(list.ElementAt(_rnd.Next(count))))
                needed--;

        return selectedItems;
    

    public IEnumerable<T> Random_FisherYates<T>(IEnumerable<T> list, int sampleSize)
    
        var count = list.Count();
        if (sampleSize > count) throw new ArgumentException("sampleSize may not be greater than list count", "sampleSize");
        var indices = new Dictionary<int, int>(); int index;

        for (int i = 0; i < sampleSize; i++)
        
            int j = _rnd.Next(i, count);
            if (!indices.TryGetValue(j, out index)) index = j;

            yield return list.ElementAt(index);

            if (!indices.TryGetValue(i, out index)) index = i;
            indices[j] = index;
        
    

输出:

|        Method | ListSize | Select |        Mean |     Error |    StdDev |  Gen 0 | Allocated |
|-------------- |--------- |------- |------------:|----------:|----------:|-------:|----------:|
| IterateSelect |       50 |      5 |    711.5 ns |   5.19 ns |   4.85 ns | 0.0305 |     144 B |
| RandomIndices |       50 |      5 |    341.1 ns |   4.48 ns |   4.19 ns | 0.0644 |     304 B |
|   FisherYates |       50 |      5 |    573.5 ns |   6.12 ns |   5.72 ns | 0.0944 |     447 B |

| IterateSelect |       50 |     10 |    967.2 ns |   4.64 ns |   3.87 ns | 0.0458 |     220 B |
| RandomIndices |       50 |     10 |    709.9 ns |  11.27 ns |   9.99 ns | 0.1307 |     621 B |
|   FisherYates |       50 |     10 |  1,204.4 ns |  10.63 ns |   9.94 ns | 0.1850 |     875 B |

| IterateSelect |       50 |     25 |  1,358.5 ns |   7.97 ns |   6.65 ns | 0.0763 |     361 B |
| RandomIndices |       50 |     25 |  1,958.1 ns |  15.69 ns |  13.91 ns | 0.2747 |    1298 B |
|   FisherYates |       50 |     25 |  2,878.9 ns |  31.42 ns |  29.39 ns | 0.3471 |    1653 B |

| IterateSelect |       50 |     50 |  1,739.1 ns |  15.86 ns |  14.06 ns | 0.1316 |     629 B |
| RandomIndices |       50 |     50 |  8,906.1 ns |  88.92 ns |  74.25 ns | 0.5951 |    2848 B |
|   FisherYates |       50 |     50 |  4,899.9 ns |  38.10 ns |  33.78 ns | 0.4349 |    2063 B |

| IterateSelect |      500 |      5 |  4,775.3 ns |  46.96 ns |  41.63 ns | 0.0305 |     144 B |
| RandomIndices |      500 |      5 |    327.8 ns |   2.82 ns |   2.50 ns | 0.0644 |     304 B |
|   FisherYates |      500 |      5 |    558.5 ns |   7.95 ns |   7.44 ns | 0.0944 |     449 B |

| IterateSelect |      500 |     10 |  5,387.1 ns |  44.57 ns |  41.69 ns | 0.0458 |     220 B |
| RandomIndices |      500 |     10 |    648.0 ns |   9.12 ns |   8.54 ns | 0.1307 |     621 B |
|   FisherYates |      500 |     10 |  1,154.6 ns |  13.66 ns |  12.78 ns | 0.1869 |     889 B |

| IterateSelect |      500 |     25 |  6,442.3 ns |  48.90 ns |  40.83 ns | 0.0763 |     361 B |
| RandomIndices |      500 |     25 |  1,569.6 ns |  15.79 ns |  14.77 ns | 0.2747 |    1298 B |
|   FisherYates |      500 |     25 |  2,726.1 ns |  25.32 ns |  22.44 ns | 0.3777 |    1795 B |

| IterateSelect |      500 |     50 |  7,775.4 ns |  35.47 ns |  31.45 ns | 0.1221 |     629 B |
| RandomIndices |      500 |     50 |  2,976.9 ns |  27.11 ns |  24.03 ns | 0.6027 |    2848 B |
|   FisherYates |      500 |     50 |  5,383.2 ns |  36.49 ns |  32.35 ns | 0.8163 |    3870 B |

| IterateSelect |     5000 |      5 | 45,208.6 ns | 459.92 ns | 430.21 ns |      - |     144 B |
| RandomIndices |     5000 |      5 |    328.7 ns |   5.15 ns |   4.81 ns | 0.0644 |     304 B |
|   FisherYates |     5000 |      5 |    556.1 ns |  10.75 ns |  10.05 ns | 0.0944 |     449 B |

| IterateSelect |     5000 |     10 | 49,253.9 ns | 420.26 ns | 393.11 ns |      - |     220 B |
| RandomIndices |     5000 |     10 |    642.9 ns |   4.95 ns |   4.13 ns | 0.1307 |     621 B |
|   FisherYates |     5000 |     10 |  1,141.9 ns |  12.81 ns |  11.98 ns | 0.1869 |     889 B |

| IterateSelect |     5000 |     25 | 54,044.4 ns | 208.92 ns | 174.46 ns | 0.0610 |     361 B |
| RandomIndices |     5000 |     25 |  1,480.5 ns |  11.56 ns |  10.81 ns | 0.2747 |    1298 B |
|   FisherYates |     5000 |     25 |  2,713.9 ns |  27.31 ns |  24.21 ns | 0.3777 |    1795 B |

| IterateSelect |     5000 |     50 | 54,418.2 ns | 329.62 ns | 308.32 ns | 0.1221 |     629 B |
| RandomIndices |     5000 |     50 |  2,886.4 ns |  36.53 ns |  34.17 ns | 0.6027 |    2848 B |
|   FisherYates |     5000 |     50 |  5,347.2 ns |  59.45 ns |  55.61 ns | 0.8163 |    3870 B |

StdDev of IterateSelect: 671.88 (% of average: 0.67)
StdDev of RandomIndices: 296.07 (% of average: 0.30)
StdDev of FisherYates: 280.47 (% of average: 0.28)

【讨论】:

基准测试表明“Random_RandomIdices”是最好的折衷方案。然而,当 select/needed 接近 listSize 且运行时间延长时,它的简单逻辑效率低下,因为多次重试以捕获最后一个元素,正如 Paul 在 2015 年也提到的那样,并且 50 个中有 50 个的基准证实了这一点。因此,根据要求,效率和简单性的最佳折衷方案很可能是 FisherYates 变体。【参考方案20】:

这比人们想象的要难得多。请参阅 Jeff 的 great Article "Shuffling"。

我确实写了一篇关于该主题的非常短的文章,包括 C# 代码:Return random subset of N elements of a given array

【讨论】:

【参考方案21】:

目标:从集合源中选择 N 个项目而不重复。 我为任何通用集合创建了一个扩展。我是这样做的:

public static class CollectionExtension

    public static IList<TSource> RandomizeCollection<TSource>(this IList<TSource> source, int maxItems)
    
        int randomCount = source.Count > maxItems ? maxItems : source.Count;
        int?[] randomizedIndices = new int?[randomCount];
        Random random = new Random();

        for (int i = 0; i < randomizedIndices.Length; i++)
        
            int randomResult = -1;
            while (randomizedIndices.Contains((randomResult = random.Next(0, source.Count))))
            
                //0 -> since all list starts from index 0; source.Count -> maximum number of items that can be randomize
                //continue looping while the generated random number is already in the list of randomizedIndices
            

            randomizedIndices[i] = randomResult;
        

        IList<TSource> result = new List<TSource>();
        foreach (int index in randomizedIndices)
            result.Add(source.ElementAt(index));

        return result;
    

【讨论】:

【参考方案22】:

我最近在我的项目中使用类似于 Tyler's point 1 的想法进行此操作。 我正在加载一堆问题并随机选择五个。排序是使用 IComparer 实现的。 a所有问题都加载到 QuestionSorter 列表中,然后使用 List's Sort function 和选择的前 k 个元素对其进行排序。

    private class QuestionSorter : IComparable<QuestionSorter>
    
        public double SortingKey
        
            get;
            set;
        

        public Question QuestionObject
        
            get;
            set;
        

        public QuestionSorter(Question q)
        
            this.SortingKey = RandomNumberGenerator.RandomDouble;
            this.QuestionObject = q;
        

        public int CompareTo(QuestionSorter other)
        
            if (this.SortingKey < other.SortingKey)
            
                return -1;
            
            else if (this.SortingKey > other.SortingKey)
            
                return 1;
            
            else
            
                return 0;
            
        
    

用法:

    List<QuestionSorter> unsortedQuestions = new List<QuestionSorter>();

    // add the questions here

    unsortedQuestions.Sort(unsortedQuestions as IComparer<QuestionSorter>);

    // select the first k elements

【讨论】:

【参考方案23】:

为什么不这样:

 Dim ar As New ArrayList
    Dim numToGet As Integer = 5
    'hard code just to test
    ar.Add("12")
    ar.Add("11")
    ar.Add("10")
    ar.Add("15")
    ar.Add("16")
    ar.Add("17")

    Dim randomListOfProductIds As New ArrayList

    Dim toAdd As String = ""
    For i = 0 To numToGet - 1
        toAdd = ar(CInt((ar.Count - 1) * Rnd()))

        randomListOfProductIds.Add(toAdd)
        'remove from id list
        ar.Remove(toAdd)

    Next
'sorry i'm lazy and have to write vb at work :( and didn't feel like converting to c#

【讨论】:

【参考方案24】:

这是我的方法(全文在这里http://krkadev.blogspot.com/2010/08/random-numbers-without-repetition.html)。

它应该在 O(K) 而不是 O(N) 中运行,其中 K 是所需元素的数量,N 是可供选择的列表的大小:

public <T> List<T> take(List<T> source, int k) 
 int n = source.size();
 if (k > n) 
   throw new IllegalStateException(
     "Can not take " + k +
     " elements from a list with " + n +
     " elements");
 
 List<T> result = new ArrayList<T>(k);
 Map<Integer,Integer> used = new HashMap<Integer,Integer>();
 int metric = 0;
 for (int i = 0; i < k; i++) 
   int off = random.nextInt(n - i);
   while (true) 
     metric++;
     Integer redirect = used.put(off, n - i - 1);
     if (redirect == null) 
       break;
     
     off = redirect;
   
   result.add(source.get(off));
 
 assert metric <= 2*k;
 return result;

【讨论】:

【参考方案25】:

这不像公认的解决方案那样优雅或高效,但写起来很快。首先,随机排列数组,然后选择前 K 个元素。在python中,

import numpy

N = 20
K = 5

idx = np.arange(N)
numpy.random.shuffle(idx)

print idx[:K]

【讨论】:

【参考方案26】:

我会使用扩展方法。

    public static IEnumerable<T> TakeRandom<T>(this IEnumerable<T> elements, int countToTake)
    
        var random = new Random();

        var internalList = elements.ToList();

        var selected = new List<T>();
        for (var i = 0; i < countToTake; ++i)
        
            var next = random.Next(0, internalList.Count - selected.Count);
            selected.Add(internalList[next]);
            internalList[next] = internalList[internalList.Count - selected.Count];
        
        return selected;
    

【讨论】:

【参考方案27】:
public static IEnumerable<T> GetRandom<T>(this IList<T> list, int count, Random random)
    
        // Probably you should throw exception if count > list.Count
        count = Math.Min(list.Count, count);

        var selectedIndices = new SortedSet<int>();

        // Random upper bound
        int randomMax = list.Count - 1;

        while (selectedIndices.Count < count)
        
            int randomIndex = random.Next(0, randomMax);

            // skip over already selected indeces
            foreach (var selectedIndex in selectedIndices)
                if (selectedIndex <= randomIndex)
                    ++randomIndex;
                else
                    break;

            yield return list[randomIndex];

            selectedIndices.Add(randomIndex);
            --randomMax;
        
    

内存:~count 复杂度:O(count2)

【讨论】:

【参考方案28】:

当 N 非常大时,随机打乱 N 个数字并选择前 k 个数字的常规方法可能会因为空间复杂性而令人望而却步。以下算法的时间和空间复杂度都只需要 O(k)。

http://arxiv.org/abs/1512.00501

def random_selection_indices(num_samples, N):
    modified_entries = 
    seq = []
    for n in xrange(num_samples):
        i = N - n - 1
        j = random.randrange(i)

        # swap a[j] and a[i] 
        a_j = modified_entries[j] if j in modified_entries else j 
        a_i = modified_entries[i] if i in modified_entries else i

        if a_i != j:
            modified_entries[j] = a_i   
        elif j in modified_entries:   # no need to store the modified value if it is the same as index
            modified_entries.pop(j)

        if a_j != i:
            modified_entries[i] = a_j 
        elif i in modified_entries:   # no need to store the modified value if it is the same as index
            modified_entries.pop(i)
        seq.append(a_j)
    return seq

【讨论】:

【参考方案29】:

将 LINQ 与大型列表一起使用(当触摸每个元素的成本很高时)并且您是否可以忍受重复的可能性:

new int[5].Select(o => (int)(rnd.NextDouble() * maxIndex)).Select(i => YourIEnum.ElementAt(i))

为了我的使用,我有一个包含 100.000 个元素的列表,由于它们是从数据库中提取的,与整个列表中的 rnd 相比,我的时间大约减半(或更好)。

拥有一个大列表将大大降低重复的几率。

【讨论】:

这个解决方案可能有重复元素!!洞列表中的随机可能不会。 嗯。真的。我在哪里使用它,但这并不重要。编辑答案以反映这一点。【参考方案30】:

这将解决您的问题

var entries=new List<T>();
var selectedItems = new List<T>();


                for (var i = 0; i !=10; i++)
                
                    var rdm = new Random().Next(entries.Count);
                        while (selectedItems.Contains(entries[rdm]))
                            rdm = new Random().Next(entries.Count);

                    selectedItems.Add(entries[rdm]);
                

【讨论】:

虽然这可能会回答问题,但您应该edit 您的回答包括对如何此代码块回答问题的解释。这有助于提供上下文,并使您的答案对未来的读者更有用。

以上是关于从 C# 中的 List<T> 中选择 N 个随机元素的主要内容,如果未能解决你的问题,请参考以下文章

C# 从 List<T> 中选择存在于另一个列表中的所有元素

从 C# 中的 List<T> 中删除重复项

如何从 C# 中的通用 List<T> 中获取元素? [复制]

从 List<OwnStruct> 返回 List<T> 的方法,其中 List<T> 仅包含 List 中所有 OwnStructs 的一个属性(C#)[重复]

在 List<T> 中选择一周内没有休息日的记录 - C#

请教C#中的List<T>,筛选list中特定元素的方法