8

Python的模块“随机”有一个功能random.choice

random.choice(seq)
从非空序列 seq 返回一个随机元素。如果seq为空,则提高IndexError.

如何在 .NET 中模拟它?

public T RandomChoice<T> (IEnumerable<T> source)

编辑:几年前我听说这是一个面试问题,但今天这个问题在我的工作中自然而然地出现了。面试问题的陈述有限制

  • '序列太长,无法保存到内存'
  • '你只能在序列上循环一次'
  • '序列没有长度/计数方法' (à la .NET IEnumerable)
4

7 回答 7

14

要创建一个只迭代一次源的方法,并且不必分配内存来临时存储它,你计算你迭代了多少项目,并确定当前项目应该是结果的概率:

public T RandomChoice<T> (IEnumerable<T> source) {
  Random rnd = new Random();
  T result = default(T);
  int cnt = 0;
  foreach (T item in source) {
    cnt++;
    if (rnd.Next(cnt) == 0) {
      result = item;
    }
  }
  return result;
}

当你在第一个项目时,它应该被使用的概率是 1/1(因为这是你迄今为止看到的唯一项目)。当您在第二项时,它应该替换第一项的概率是 1/2,依此类推。


正如 dasblinkenlight 指出的那样,这自然会使用更多的 CPU,因为它会为每个项目创建一个随机数,而不仅仅是一个随机数来选择一个项目。您可以IList<T>按照 Dan Tao 的建议检查源是否实现了 ,并使用使用这些功能获取集合长度并按索引访问项目的实现:

public T RandomChoice<T> (IEnumerable<T> source) {
  IList<T> list = source as IList<T>;
  if (list != null) {
    // use list.Count and list[] to pick an item by random
  } else {
    // use implementation above
  }
}

注意:您应该考虑将Random实例发送到方法中。否则,如果您两次调用该方法的时间太接近,您将获得相同的随机种子,因为种子是从当前时间创建的。


测试运行的结果,从包含 0 - 9 的数组中选择一个数字,1000000 次,以表明所选数字的分布没有倾斜:

0: 100278
1: 99519
2: 99994
3: 100327
4: 99571
5: 99731
6: 100031
7: 100429
8: 99482
9: 100638
于 2012-07-03T16:16:11.190 回答
6

为了避免对序列进行两次迭代(一次用于计数,一次用于元素),在获取随机元素之前将序列保存在数组中可能是个好主意:

public static class RandomExt {
    private static Random rnd = new Random();
    public static T RandomChoice<T> (this IEnumerable<T> source) {
        var arr = source.ToArray();   
        return arr[rnd.Next(arr.Length)];
    }
    public static T RandomChoice<T> (this ICollection<T> source) {
        return source[rnd.Next(rnd.Count)];
    }
}

编辑Chris Sinclair实现了一个非常好的想法。

于 2012-07-03T16:10:39.347 回答
2
private static Random rng = new Random();

...
return source.Skip(rng.next(source.Count())).Take(1);
于 2012-07-03T16:10:03.340 回答
2
        public T RandomChoice<T> (IEnumerable<T> source)
        {
            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            var list = source.ToList();

            if (list.Count < 1)
            {
                throw new MissingMemberException();
            }

            var rnd = new Random();
            return list[rnd.Next(0, list.Count)];
        }

或扩展名

    public static T RandomChoice<T> (this IEnumerable<T> source)
    {
        if (source == null)
        {
            throw new ArgumentNullException("source");
        }

        var list = source.ToList();

        if (list.Count < 1)
        {
            throw new MissingMemberException();
        }

        var rnd = new Random();
        return list[rnd.Next(0, list.Count)];
    }
于 2012-07-03T16:12:47.147 回答
1

我会选择dasblinkenlight 的答案,但有一点小改动:利用可能已经是索引集合的事实source,在这种情况下,您真的不需要填充新数组(或列表):

public static class RandomExt
{
    public static T Choice<T>(this Random random, IEnumerable<T> sequence)
    {
        var list = sequence as IList<T> ?? sequence.ToList();
        return list[random.Next(list.Count)];
    }
}

请注意,我还修改了上述答案的界面,使其与您在问题中引用的 Python 版本更加一致:

var random = new Random();
var numbers = new int[] { 1, 2, 3 };
int randomNumber = random.Choice(numbers);

编辑:实际上,我更喜欢Guffa 的回答

于 2012-07-03T16:14:43.000 回答
0

好吧,获取序列中所有元素的列表。向随机数生成器询问索引,按索引返回元素。定义什么是 Sequence - IEnumerable 是最明显的,但您需要将其具体化为一个列表,然后才能知道随机数生成器的元素数量。这是顺便说一句,不是模仿,它是实施。

这是一些家庭作业初学者学习课程的问题吗?

于 2012-07-03T16:09:31.100 回答
0

假设有一个扩展方法IEnumerable.MinBy

var r = new Random();
return source.MinBy(x=>r.Next())

该方法MinBy不会将序列保存到内存中,它的工作方式类似于IEnumerable.Min进行一次迭代(参见MoreLinq其他地方

于 2012-07-04T10:59:02.563 回答