我需要从一组n
(其中N
)随机选择0 < n < N
条记录。
可能的算法是:
遍历列表并对每个元素进行选择概率=
(number needed) / (number left)
因此,如果您有40个项目,那么第一个项目将有
5/40
被选中。如果是,则下一次有
4/39
次机会,否则有5/39
次机会。等到你结束的时候 你将获得5件物品,而且在此之前你通常会拥有所有这些物品。
假设一个好的伪随机数发生器,这个算法是否正确?
注意
在stackoverflow上有很多这类问题(其中很多被标记为Select N random elements from a List<T> in C#的重复)。
经常提出上述算法(例如Kyle Cronin的回答)和 它一直受到质疑(例如,见 here,here,here,here ......)。
我可以对此事做最后的决定吗?
答案 0 :(得分:8)
算法绝对正确。
这不是突然发明的好海报,它是一种众所周知的技术,称为选择采样 / 算法S (由Fan,Muller和Rezucha发现(1)和由Jones(2)于1962年独立完成,在TAOCP - 第2卷 - 精神数学算法 - 第3.4.2节中有详细描述。
正如Knuth所说:
该算法乍一看似乎不可靠,实际上是不正确的。但仔细分析表明它完全值得信赖。
当已经n
元素已经N
时,算法会从一组大小为t + 1
的{{1}}元素中选择(n - m) / (N - t)
个元素,并且概率为m
被选中了。
很容易看出,在选择n
项之前,我们永远不会在集合的末尾运行(因为当我们有1
个元素可供选择时,概率为k
k
个元素。)
此外,我们从不选择太多元素(概率0
即n == m
}。
有点难以证明样本是完全无偏的,但它是
...尽管我们不选择概率为
t + 1
的{{1}}项目,但这是真的。这在已发表的文献中引起了一些混淆
(所以不只是在Stackoverflow上!)
事实上,我们不应混淆条件和无条件概率:
例如考虑第二个元素;如果在样本中选择了第一个元素(这种情况以概率
n / N
发生),则以概率n / N
选择第二个元素;如果未选择第一个元素,则以概率(n - 1) / (N - 1)
选择第二个元素。选择第二个元素的总体概率为
n / (N - 1)
。
TAOCP - 第2卷 - 第3.4.2节练习3
除了理论上的考虑之外,算法S (以及算法R / reservoir sampling)在许多知名的库中使用(例如SGI's original STL implementation, std::experimental::sample
,
Python中的random.sample
...)。
当然算法S 并不总是最佳答案:
(n / N) ((n - 1) / (N - 1)) + (1 - n/N)(n / (N - 1)) = n/N
(即使我们通常不必传递所有O(N)
条记录:N
约为n=2
时所考虑的平均记录数;一般情况公式给出
TAOCP - 第2卷 - 第3.4.2节 - 前5/6); N
isn't known in advance。无论如何它都有效!
修改强>
如何随机选择此项目,概率为7/22
[CUT]
在极少数情况下,您甚至可以在需要5
时选择4或6个元素
这来自N3925(为避免公共接口/标签调度而进行的小修改):
2/3 N
没有漂浮物。
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;
}
“有效as advertised”则没有偏见。答案 1 :(得分:-3)
尽管描述的算法是technically correct,但它依赖于具有返回bool的算法,该bool具有由两个int的比率确定的任意概率。例如,如何以7/22的概率选择此项?就谈话而言,我们称其为bool RandomSelect(int x, int y)
方法,或称为RS(x,y)
方法,旨在以概率true
返回x/y
。如果您不太关心准确性,那么经常给出的答案就是使用return Random.NextDouble() < (double)x/(double)y;
这是不准确的,因为Random.NextDouble()
不精确且不完全一致,而(double)x/(double)y
除以也是不精确的。 <
或<=
的选择应该是无关紧要的(但事实并非如此),因为理论上不可能随机选择完全等于指定概率的无限精度随机数。虽然我确定可以创建或找到一个算法,但是为了准确地实现RS(x,y)
方法,这将允许您正确地实现所描述的算法,我认为只需回答这个问题为&#34 ;是的,算法是正确的&#34;会误导 - 因为它误导了很多人,使用double
来计算和选择元素,而不知道他们引入的偏见。
RS(x,y)
算法,否则我只是说,你的选择会比其他元素更频繁地偏向某些元素。
如果您关心公平性(所有可能结果的概率相等),我认为使用不同的算法会更好,更容易理解,如下所述:
如果你认为随机可用的随机位是随机位,你必须定义一种随机选择技术,在给定二进制随机数据的情况下确保相等的概率。这意味着,如果你想在一个恰好是2的幂的范围内选择一个随机数,你只需选择随机位并返回它们。但是如果你想要一个范围不是2的幂的随机数,你必须得到更多的随机位,并丢弃无法映射到公平结果的结果(扔掉随机数并重试)。我在这里用博客表示和C#示例代码在博客中写道:https://nedharvey.com/blog/?p=284从您的收藏中重复随机选择,直到您拥有n
个唯一项目。