【问题标题】:Optimal LINQ query to get a random sub collection - Shuffle获取随机子集合的最佳 LINQ 查询 - 随机播放
【发布时间】:2010-12-11 17:14:25
【问题描述】:

请建议一种最简单的方法,以从具有“N”个项目的集合中获取计数“n”个随机打乱的集合。其中 n

【问题讨论】:

    标签: c# linq ienumerable observablecollection


    【解决方案1】:

    除了 mquander 的回答和 Dan Blanchard 的评论,这里有一个 LINQ 友好的扩展方法,它执行 Fisher-Yates-Durstenfeld shuffle

    // take n random items from yourCollection
    var randomItems = yourCollection.Shuffle().Take(n);
    
    // ...
    
    public static class EnumerableExtensions
    {
        public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source)
        {
            return source.Shuffle(new Random());
        }
    
        public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, Random rng)
        {
            if (source == null) throw new ArgumentNullException("source");
            if (rng == null) throw new ArgumentNullException("rng");
    
            return source.ShuffleIterator(rng);
        }
    
        private static IEnumerable<T> ShuffleIterator<T>(
            this IEnumerable<T> source, Random rng)
        {
            var buffer = source.ToList();
            for (int i = 0; i < buffer.Count; i++)
            {
                int j = rng.Next(i, buffer.Count);
                yield return buffer[j];
    
                buffer[j] = buffer[i];
            }
        }
    }
    

    【讨论】:

    • 是我还是这个方法搞砸了列表索引?什么时候在洗牌后使用 elementAt() 我得到的结果是完全出乎意料的......
    • @Htbaa:该方法返回一个延迟评估的序列。如果您多次执行seq.Shuffle().ElementAt(n),那么您每次都会重新洗牌,因此您很可能会在位置n 获得不同的项目。如果您想洗牌一次,那么您需要将序列存储在某种具体的集合中:例如,var list = seq.Shuffle().ToList()。然后,您可以随心所欲地使用list.ElementAt(n)——或者只是list[n]——你总是会得到相同的东西。
    • @gilly3:这可能是因为这些Shuffle 方法返回一个惰性IEnumerable&lt;T&gt;,每次您在string.JoinElementAt 调用中使用它时都会重新评估它。这就是IEnumerable&lt;T&gt; 的本质;如果您想要具体集合的行为,那么您实际上应该通过在Shuffle 调用的结果上调用ToArrayToList 创建一个具体集合。
    • @gilly3 很抱歉在一年后提出这个问题,但 ShuffleIterator 的目的是让 Shuffle 预先执行参数验证,而不是推迟到第一次在枚举器上调用 MoveNext 之前(这可能永远不要,或者在不期望它的代码中)。见msmvps.com/blogs/jon_skeet/archive/2008/03/02/…
    • @jocull 如果您使用不采用 Random 实例的版本,它将为您创建一个。及时创建的 Random 实例有使用相同种子的风险,因此会给出相同的随机数序列。如果您在不同的线程上将相同的 Random 实例传递给它,您将得到不可预知的结果,因为 Random 的实例不是线程安全的。您应该使用不同的种子创建 Random 实例,而不是在线程之间共享它们。
    【解决方案2】:

    另一种选择是使用 OrderBy 并按 GUID 值排序,您可以这样做:

    var result = sequence.OrderBy(elem => Guid.NewGuid());
    

    我做了一些实证测试来说服自己,上面的内容实际上生成了一个随机分布(它似乎确实如此)。你可以在Techniques for Randomly Reordering an Array看到我的结果。

    【讨论】:

    • 这个解决方案违反了 orderby 的约定,特别是给定对象在整个排序过程中具有一致的键。如果它确实有效,它仅靠偶然性就可以做到,并且可能会在框架的未来版本中中断。更多详情请见blogs.msdn.com/b/ericlippert/archive/2011/01/31/…
    • 好收获。但是,如果这被提交给一个实际的集合——例如添加 .ToList() 或 .ToArray() 到最后——那么集合就不可能被处理/迭代 Linq 代码超过一次来执行种类。我想这也能在未来的升级中幸存下来,因为它可以作为一个可预测的快照。
    • 我只是偶然发现了这个页面并认为它很棒,但我不明白评论。使用这种方法对集合进行洗牌可能会出现什么问题(我并不是在讽刺地问这个问题,我真的对可能性感到好奇)?
    • 这里的问题不在于key不一致。约翰梅尔维尔误读了我的文章;那篇文章指出,进行不一致的比较(即,该项目更大、更小或等于另一个)违反了 Sort 方法的约定。这个答案是错误的,原因完全不同:Guids 只能保证是唯一的;它们的随机性是你不应该依赖的实现细节
    • 特别是,从初始随机元素顺序生成guid是合法的;这仍然很好地保证了唯一性。仅仅因为 guid 生成器 today 实际上并没有生成顺序 guid 是一个可能发生变化的实现细节,如果它确实发生了变化,那么突然间你的“洗牌”每次都将事情“洗牌”成排序顺序.使用 guid 来生成唯一性,而不是随机性。使用旨在为随机性生成随机性的类。
    【解决方案3】:

    这有一些“随机偏差”的问题,我确信它不是最优的,这是另一种可能性:

    var r = new Random();
    l.OrderBy(x => r.NextDouble()).Take(n);
    

    【讨论】:

    • 除了来自Random 本身的任何微不足道的偏差之外,它没有任何随机偏差。它也非常有效。
    【解决方案4】:

    Shuffle 将集合按随机顺序排列,并从结果中取出第一个 n 项。

    【讨论】:

    • 请注意,当 n
    【解决方案5】:

    不那么随机,但高效:

    var rnd = new Random();
    var toSkip = list.Count()-n;
    
    if (toSkip > 0)
        toSkip = rnd.Next(toSkip);
    else
        toSkip=0;
    
    var randomlySelectedSequence = list.Skip(toSkip).Take(n);
    

    【讨论】:

      【解决方案6】:

      我写了这个覆盖方法:

      public static IEnumerable<T> Randomize<T>(this IEnumerable<T> items) where T : class
      {
           int max = items.Count();
           var secuencia = Enumerable.Range(1, max).OrderBy(n => n * n * (new Random()).Next());
      
           return ListOrder<T>(items, secuencia.ToArray());
      }
      
      private static IEnumerable<T> ListOrder<T>(IEnumerable<T> items, int[] secuencia) where T : class
              {
                  List<T> newList = new List<T>();
                  int count = 0;
                  foreach (var seed in count > 0 ? secuencia.Skip(1) : secuencia.Skip(0))
                  {
                      newList.Add(items.ElementAt(seed - 1));
                      count++;
                  }
                  return newList.AsEnumerable<T>();
              }
      

      然后,我有我的源列表(所有项目)

      var listSource = p.Session.QueryOver<Listado>(() => pl)
                              .Where(...);
      

      最后,我调用“Randomize”,我得到了一个随机的子集合,在我的例子中是 5 个:

      var SubCollection = Randomize(listSource.List()).Take(5).ToList();
      

      【讨论】:

      • 创建newList 是在浪费内存。也许考虑在foreach 语句中使用yield return
      • Enumerable.Range(1, max).OrderBy(n =&gt; n * n * (new Random()).Next()) 这行很糟糕——它根本不是随机的。在大多数情况下,像这样调用 new Random()).Next() 只会返回相同的数字,而 n * n 会使数字有偏差 - 并且可能会导致溢出异常。
      【解决方案7】:

      抱歉,代码很丑 :-),但是

      var result =yourCollection.OrderBy(p => (p.GetHashCode().ToString() + Guid.NewGuid().ToString()).GetHashCode()).Take(n);

      【讨论】:

      • p.GetHashCode().ToString() + Guid.NewGuid().ToString()).GetHashCode() 不是随机的。它可能看起来很随机,但事实并非如此。您应该使用 RNG 来实现随机性 - 数据科学家专门将它们设计为尽可能随机。
      猜你喜欢
      • 2021-12-18
      • 1970-01-01
      • 2011-11-01
      • 1970-01-01
      • 2023-01-23
      • 1970-01-01
      • 1970-01-01
      • 2010-09-13
      • 1970-01-01
      相关资源
      最近更新 更多