这个算法是绝对正确的。
它不是一个突然想出来的好方法,而是一种被称为选择取样/算法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/N
。
TAOCP - 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
的值事先未知时,无法使用。
无论如何,它都能工作!
- C. T. Fan, M. E. Muller和I. Rezucha,《美国统计协会杂志》57(1962年),pp 387-402
- 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
“如广告所述”运作正常,则没有偏差。