【问题标题】:How to enumerate list items randomly in C#?如何在 C# 中随机枚举列表项?
【发布时间】:2011-05-03 15:00:57
【问题描述】:

我有一个具有挑战性的问题要请教各位大专家。这在我的代码中还没有实际使用,但来自我刚刚的一个想法。

如果我有一个IList<T>,我如何实现一个随机遍历列表并且可以被多个线程同时使用的枚举器?

例如,如果我有元素 ABCDEF 和两个并发线程在列表上执行 for-each 循环并获得了 ReaderLock (所以我确信没有其他人会触及列表从而导致异常),我希望他们各自的周期返回,例如,BECDAFEBDCAF

我需要这个的原因是因为我需要在List<SslStream> 元素上加锁以向客户端because SslStream is not thread-safe 发送数据。随机选择元素(但要确保我选择它们全部)降低了锁冲突的可能性,并且应该提高 I/O 绑定操作的性能。

请记住,即使我告诉你为什么我需要这样一个枚举器,我仍然喜欢挑战。可以有其他方式将相同的数据发送到多个客户端,但我的问题仍然是一样的 :) :)

【问题讨论】:

  • 喜欢挑战没关系。但是,有可能没有人能提供具有这些限制的良好解决方案。做好准备:)

标签: c#


【解决方案1】:

这样的东西(显然需要生产化):

class RandomList<T> : IEnumerable<T> {
     private readonly IList<T> list;
     private readonly Random rg;
     private readonly object sync = new Object();
     public RandomList(IList<T> list) : this(list, new Random()) { }

     public RandomList(IList<T> list, Random rg) {
         Contract.Requires<ArgumentNullException>(list != null);
         Contract.Requires<ArgumentNullException>(rg != null);
         this.list = list;
         this.rg = rg;
     }

     public IEnumerator<T> GetEnumerator() {
         List<int> indexes;
         // Random.Next is not guaranteed to be thread-safe
         lock (sync) {
             indexes = Enumerable
                 .Range(0, this.list.Count)
                 .OrderBy(x => this.rg.Next())
                 .ToList();
         }
         foreach (var index in indexes) {
             yield return this.list[index];
         }
     }
}
      IEnumerator IEnumerable.GetEnumerator() {
          return GetEnumerator();
      }
}

【讨论】:

  • 我真的相信您的代码虽然对我的目的仍然有效,但显示了您在 thecoop 的帖子中强调的相同问题。虽然 LINQ 更易于读写,但它们的执行时间不是 O(1)。但这仍然是一个很好的起点
  • @djechelon:嗯?这不可能永远旋转。我随机排序索引。
  • 我想我说错了。无论如何,我从来没有想过永远。我只是说我对 LINQ 的性能还不够信任。您只阅读了一个语句,但它至少隐藏了两个循环,仅此而已。
  • @djechelon,你不应该对 Linq 性能如此警惕......它并不比手动循环慢很多
  • Thomas 你说到点子上了!! ;) LINQ 并没有看起来更快,所以这就是为什么我现在避免链接到 System.Core :-)
【解决方案2】:

创建一个与列表大小相同的数组,最初将其填充为 a[i] = i,然后使用 Fisher Yates algorithm 随机播放。

然后您的枚举器可以遍历这个数组,从您的源列表中返回提供的随机索引处的元素。

【讨论】:

  • 你绝对说服了我 :) 恭喜你赢得了我的挑战。信不信由你,当我发布我之前的评论时,我正要重新考虑我的两层方法和就地方法,几乎在没有听说过的情况下重新发明了 Fisher-Yates :)非常感谢您的回答,即使在我使用静态数组实现 C# 常规 List 并允许您删除元素从而留下“漏洞”的情况下,它也可以工作(需要注意)(实际上,甲板将仅由有效索引构成) .我会试试代码:)
【解决方案3】:

应该这样做:

public static IEnumerable<T> YieldRandom<T>(this IList<T> items) {
    Random random = new Random();
    HashSet<int> yielded = new HashSet<int>(items.Count);

    for (int i=0; i<items.Count; i++) {
        // find an index we haven't yielded yet
        int yieldIndex;
        do {
            yieldIndex = random.Next(items.Count);
        }
        while (yielded.Contains(yieldIndex));

        yielded.Add(yieldIndex);
        yield return items[yieldIndex];
    }
}

我确信可以在某些地方使用更多的 LINQ :)

【讨论】:

  • 如果需要,我可以实现自己的 IList ;),不用担心扩展
  • 当您接近枚举的“结束”时,这可能会旋转很长时间。
  • 随着您产生的 HashSet 增长并且您必须不断重试随机条目,这将非常糟糕。
  • @Jason:是的,我在另一篇关于洗牌的文章中读到了这一点。我开始考虑一种两层方法——@thecoop:当我将锁放入其中时,我认为 LINQ 并不比常规的顺序 for-each 循环更智能
  • @djechelon:两层方法?
猜你喜欢
  • 2023-03-06
  • 2013-05-28
  • 1970-01-01
  • 2011-05-02
  • 2018-07-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多