在.NET中模拟Python的random.choice

Python的模块’random’有一个函数random.choice

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

我怎样才能在.NET中模拟这个?

 public T RandomChoice (IEnumerable source) 

编辑:几年前我听到这是一个采访问题,但今天问题自然发生在我的工作中。 面试问题有限制

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

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

 public T RandomChoice (IEnumerable 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,依此类推。


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

 public T RandomChoice (IEnumerable source) { IList list = source as IList; 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 

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

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

编辑 Chris Sinclair实施了一个非常好的主意 。

 private static Random rng = new Random(); ... return source.Skip(rng.next(source.Count())).Take(1); 
  public T RandomChoice (IEnumerable 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 (this IEnumerable 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)]; } 

我将使用dasblinkenlight的答案 , 只做一个小改动:利用source可能已经是索引集合的事实,在这种情况下,您实际上不需要填充新数组(或列表):

 public static class RandomExt { public static T Choice(this Random random, IEnumerable sequence) { var list = sequence as IList ?? 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的答案 。

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

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

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

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

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