从N个记录中随机选择n个记录

9

我需要从一个包含N个记录的集合中随机选择n条记录(其中0 < n < N)。

一种可能的算法是:

遍历列表,对于每个元素,使其被选中的概率等于 (所需数量) / (剩余数量)

因此,如果你有40个项目,则第一个项目被选中的概率为5/40

如果它被选中,则下一个项目的概率为4/39,否则为5/39。当你到达最后一个元素时,你将得到你的5个项目,而通常在那之前就已经得到所有项目了。

假设有一个良好的伪随机数生成器,这个算法是否正确?


注意

在stackoverflow上有很多这种问题(其中很多被标记为C#中从List<T>中选择N个随机元素的重复问题)。

上述算法经常被提出(例如Kyle Cronin的答案),但总是受到质疑(例如,请参见这里这里这里这里…)。

我可以就此事发表最后的看法吗?


虽然在这里提出维基类型的问题并回答自己是完全可以的,但它应该是之前没有被问过的。我还在考虑是否关闭。一方面,答案质量很高。另一方面,那应该是重复问题的答案。 - amit
我决定不独自关闭它,因为我不确定。如果这不是一个明显的问题,我本来会投票关闭的。让社区决定它是否是重复问题。 - amit
@amit 抱歉,我的问题不是“如何采样”(就像其他问题一样),而是“这个算法正确吗?”该算法经常在没有参考/细节的情况下提出,答案总是会引发很多关于正确性、偏差等方面的问题...也许其他用户(像我一样)会对此感到困惑。我试图澄清一些要点。 - manlio
@manilio 重复线程中的最佳答案详细解释了这一点,并提到了Knuth的参考资料。 - amit
1
@amit...这是一本书中同一段落中描述的类似但不同的算法(算法S与算法R)。 - manlio
显示剩余2条评论
2个回答

16

这个算法是绝对正确的。

它不是一个突然想出来的好方法,而是一种被称为选择取样/算法S(由Fan、Muller和Rezucha(1)在1962年发现,独立于Jones(2)),在TAOCP-第2卷-半数值算法-§3.4.2中有很好的描述。

正如Knuth所说:

乍一看,这个算法可能不可靠,甚至是错误的。但仔细分析后可以发现它是完全可信的。

该算法从大小为N的集合中抽取n个元素,当已经选出m个元素时,第t+1个元素以(n-m)/(N-t)的概率被选中。

很容易看出,在选择n项之前,我们从剩余的k个元素中选择时,我们永远不会用完集合(当我们有k个元素可供选择时,概率将为1)。
此外,我们永远不会选择太多元素(当n == m时,概率将为0)。
证明样本完全无偏差有点困难,但是尽管我们没有以n / N的概率选择第t + 1项,它仍然是正确的。这在已发表的文献中引起了一些混乱(因此不仅仅是在Stackoverflow上!)
事实上,我们不应混淆条件和无条件概率:
例如,考虑第二个元素;如果在样本中选择了第一个元素(这发生的概率为 n / N),则第二个元素被选中的概率为 (n - 1) / (N - 1);如果没有选择第一个元素,则第二个元素被选中的概率为 n / (N - 1)。选择第二个元素的总体概率为 (n / N) ((n - 1) / (N - 1)) + (1 - n/N)(n / (N - 1)) = n/NTAOCP - Vol 2 - Section 3.4.2 exercise 3 除了理论考虑,算法S(以及算法R / 蓄水池抽样)被许多知名库使用(例如SGI的原始STL实现, std::experimental::sample, random.sample在Python中...)。
当然,算法S并不总是最佳答案
  • 它的时间复杂度为O(N)(即使我们通常不必遍历所有N条记录:当n=2时,考虑的平均记录数约为2/3 N;一般公式见TAOCP - Vol 2 - § 3.4.2 - ex 5/6);
  • N的值事先未知时,无法使用

无论如何,它都能工作!


  1. C. T. Fan, M. E. Muller和I. Rezucha,《美国统计协会杂志》57(1962年),pp 387-402
  2. T. G. Jones,《CACM》5(1962年),pp 343

编辑

你如何以7/22的概率随机选择此项?

[删减]

在罕见情况下,您可能会在想要5个元素时选择4或6个元素

这来自N3925(进行了小修改以避免常见的接口/标记分派):

template<class PopIter, class SampleIter, class Size, class URNG>
SampleIter sample(PopIter first, PopIter last, SampleIter out, Size n, URNG &&g)
{
  using dist_t = uniform_int_distribution<Size>;
  using param_t = typename dist_t::param_type;

  dist_t d{};

  Size unsampled_sz = distance(first, last);
  for (n = min(n, unsampled_sz); n != 0;  ++first)
  {
    param_t const p{0, --unsampled_sz};

    if (d(g, p) < n) { *out++ = *first; --n; }
  }

  return out;
}

没有浮点数。

  • 如果您需要5个元素,您将获得5个元素;
  • 如果uniform_int_distribution如广告所述”运作正常,则没有偏差。

因为这种技术在数论中是正确的,但这不是math.stackexchange.com,这是stackoverflow,答案需要在计算机中正确,而你无法用此答案所需的无限精度来表示概率。请参阅我的关于公平性和无限精度的答案。 - Edward Ned Harvey
是的,当N的值(即列表中的项目数)未知时,它可以使用。链接到维基百科页面上的描述准确地展示了如何实现。第一个项目的保留概率为1/1,第二个项目的保留概率为1/2,第三个项目的保留概率为1/3等等。不需要知道N的值。 - Jim Mischel
@JimMischel 算法S需要预先知道N的值;当你不知道N的值时,算法R是一个很好的替代方案(但你必须始终读取整个输入集)。 - manlio
1
你的示例代码对我来说非常不清晰。我不知道PopIter或SampleIter是什么等等。也不清楚sample()方法或函数如何以7/22的概率返回true/false。 - Edward Ned Harvey
@EdwardNedHarvey 这个函数需要前向迭代器(PopIter)来访问输入集合,但只需要输出迭代器(PopIter)来生成结果样本。这是一个C++技术细节,并不是普遍感兴趣的问题,但我不想太过改动原始源代码。你在其他评论中已经强调了关键点:d(g, p)类似于UInt32 RandomRange()。不管怎样,我也看到很多答案受到你描述的这个问题的影响(例如https://dev59.com/TGw15IYBdhLWcg3wo9Vx#6482925)。 - manlio
"...但只有一个输出迭代器(SampleIter)到..." - manlio

-3
尽管所描述的算法 技术上 正确, 但它依赖于有一个算法返回一个布尔值,该值由两个整数的比率确定任意概率。例如,如何以 7/22 的概率选择此项?为了讲清楚,让我们称其为 bool RandomSelect(int x, int y) 方法,或者只是RS(x,y) 方法,旨在以 x/y 的概率返回 true。如果您不太关心准确性,通常给出的答案是使用return Random.NextDouble() < (double)x/(double)y;,这是不精确的,因为Random.NextDouble() 不精确且不完全均匀,并且除法(double)x/(double)y 也不精确。使用<<= 应该无关紧要(但实际上不是),因为理论上不可能随机选择精确等于指定概率的无限精度随机数。虽然我确信可以创建或找到一种算法,以精确实现 RS(x,y) 方法,从而使您能够正确实现所描述的算法,但我认为简单地回答“是,算法是正确的”会误导人们进行使用 double 计算和选择元素而不知道引入的偏差。

请不要误解我的意思 - 我并不是说每个人都应该避免使用所描述的算法 - 我只是想说,除非你找到一种更精确的实现RS(x,y)算法的方法,否则你的选择将会在某些元素上比其他元素更加偏向。

如果你关心公平性(所有可能结果的等概率性),我认为最好、最容易理解的方法是使用另一种算法,如下所述:

如果你认为你唯一可用的随机源是随机位,那么你必须定义一种随机选择技术,以确保在给定二进制随机数据的情况下具有相等的概率。这意味着,如果你想从一个2的幂次方范围内选择一个随机数,你只需选择随机位并返回它们。但是,如果你想在不是2的幂次方的范围内选择一个随机数,你需要获取更多的随机位,并且丢弃不能映射到公平结果的结果(放弃随机数并重试)。我在这里用图示和C#示例代码进行了博客记录:https://nedharvey.com/blog/?p=284 从你的集合中重复随机选择,直到你有n个唯一的项目。


在我看来,你描述的问题似乎是一个重要的实现细节,但这并不改变算法正确性的事实。直接移植算法可以完全基于整数(例如,在C ++中使用[std::uniform_int_distribution <unsigned>](http://en.cppreference.com/w/cpp/numeric/random/uniform_int_distribution)作为随机数分布, unsigned用于nN),因此它避免了浮点数固有的精度不足。 - manlio
@manlio std::uniform_int_distribution<unsigned> 会在一个范围内产生随机整数,与我博客文章中的 UInt32 RandomRange() 相同。它不是 RS(x,y) 的实现。你的评论让我意识到我的回答并没有直接回答你的问题,所以我进行了修订。 - Edward Ned Harvey
1
考虑到你的 UInt32 RandomRange(UInt32 max) 函数,那么 bool RandomSelect(int x, int y) { return RandomRange(y-1) < x; } 怎么样?如果 RandomRange 提供了相等的分布,这应该不会像 double 实现一样存在问题。 - grek40
作为一名真正的加密专家,我知道不要自己编写加密算法。这就是为什么我使用像RNGCryptoServiceProvider这样的标准库。我没有自己编写加密算法;我使用了一个标准库,并使用div和mod从范围中选择一个元素。你的陈述表明你对加密随机性或熵一无所知,因为将RNGCryptoServiceProvider称为“默认prng”,并且熵不足是与传统智慧完全相反的。此外,我刚刚发现这个div和mod方法正是Python在random.py中在_randbelow函数内部实现的,所以你是错误的。 - Edward Ned Harvey
如果出现“让我们在聊天中继续”选项,我愿意为我的反对票进行辩解(理由是“没有回答OP的问题”和“不是一个很好的想法”)。否则,我不认为来回讨论有任何意义。 - tucuxi
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接