我有一个包含n个元素的外部集合,我想随机选择其中的一些数字(k),将这些元素的索引输出到某个序列化数据文件。我希望索引以严格的升序输出,并且没有重复。 n和k都可能非常大,将整个数组简单地存储在该大小的内存中通常是不可行的。
我想出的第一个算法是从1到nk中选择一个随机数r [0] ...然后从r [i-1] +1中选择一个连续的随机数r [i]到n- k + i,只需要在任何时候为'r'存储两个条目。然而,一个相当简单的分析表明,选择小数的概率与整个集合均匀分布时的概率不一致。例如,如果n是十亿而k是五亿,那么用我刚刚描述的方法选择第一个条目的概率非常小(五分之一十亿),实际上,因为一半条目是被选中,第一个应该在50%的时间被选中。即使我使用外部排序来对k个随机数进行排序,我也不得不丢弃任何重复项,然后再试一次。当k接近n时,重试次数将继续增加,但不保证终止。
如果可能的话,我想找到一个O(k)或O(k log k)算法来做到这一点。我将使用的实现语言是C ++ 11,但伪代码中的描述可能仍然有用。
答案 0 :(得分:5)
如果实际上k与n具有相同的数量级,也许非常简单的O(n)算法就足够了:
assert(k <= n);
std::uniform_real_distribution rnd;
for (int i = 0; i < n; i++) {
if (rnd(engine) * (n - i) < k) {
std::cout << i << std::endl;
k--;
}
}
它以相同的概率产生所有递增序列。
答案 1 :(得分:3)
如果在范围的中间进行分区,可以在O(k log k)中递归求解,并从hypergeometric probability distribution中随机抽样,选择中间点上方和下方有多少个值(即每个子序列的k值,然后为每个子序列递归:
int sample_hypergeometric(int n, int K, int N) // samples hypergeometric distribution and
// returns number of "successes" where there are n draws without replacement from
// a population of N with K possible successes.
// Something similar to scipy.stats.hypergeom.rvs in Python.
// In this case, "success" means the selected value lying below the midpoint.
{
std::default_random_engine generator;
std::uniform_real_distribution<double> distribution(0.0,1.0);
int successes = 0;
for(int trial = 0; trial < n; trial++)
{
if((int)(distribution(generator) * N) < K)
{
successes++;
K--;
}
N--;
}
return successes;
}
select_k_from_n(int start, int k, int n)
{
if(k == 0)
return;
if(k == 1)
{
output start + random(1 to n);
return;
}
// find the number of results below the mid-point:
int k1 = sample_hypergeometric(k, n >> 1, n);
select_k_from_n(start, k1, n >> 1);
select_k_from_n(start + (n >> 1), k - k1, n - (n >> 1));
}
来自binomial distribution的采样也可用于近似超几何分布,其中p =(n> 1)/ n,拒绝其中k1> 1的样本。 (n>&gt; 1)。
答案 2 :(得分:2)
正如我的评论中所提到的,使用std::set<int>
来存储随机生成的整数,以便生成的容器具有固有的排序且不包含重复项。示例代码段:
#include <random>
#include <set>
int main(void) {
std::set<int> random_set;
std::random_device rd;
std::mt19937 mt_eng(rd());
// min and max of random set range
const int m = 0; // min
const int n = 100; // max
std::uniform_int_distribution<> dist(m,n);
// number to generate
const int k = 50;
for (int i = 0; i < k; ++i) {
// only non-previously occurring values will be inserted
if (!random_set.insert(dist(mt_eng)).second)
--i;
}
}
答案 3 :(得分:0)
您能否以补偿您所描述的概率失真的方式调整每个升序索引选择?
IANAS,但我的猜测是,如果你选择0到1之间的随机数r(你将在调整后缩放到完整的剩余索引范围),你可以通过计算r ^来调整它。 (x)(将范围保持在0..1,但增加较小数字的概率),通过求解第一个条目概率的等式来选择x?
答案 4 :(得分:0)
假设您无法在内存中存储k
个随机数,则必须按严格随机顺序生成数字。一种方法是生成0到n / k之间的数字。拨打该号码x
。您必须生成的下一个数字介于x+1
和(n-x)/(k-1)之间。以这种方式继续,直到你选择了k数字。
基本上,您将剩余范围除以要生成的值的数量,然后在该范围的第一部分中生成数字。
一个例子。您想生成0到99之间的3个数字,包括0和99。所以你先生成0到33之间的数字。假设你选择10。
所以现在需要一个11到99之间的数字。剩下的范围包含89个值,你还有两个值可供选择。所以,89/2 = 44.你需要一个11到54之间的数字。假设你选择36。
您的剩余范围是37到99,您还有一个号码可供选择。所以在37到99之间随机选择一个数字。
这不会给你一个正常的分布,因为一旦你选择了一个数字,就不可能得到一个小于后续选择的数字。但它可能足以满足您的目的。
这个伪代码显示了基本思想。
pick_k_from_n(n, k)
{
num_left = k
last_k = 0;
while num_left > 0
{
// divide the remaining range into num_left partitions
range_size = (n - last_k) / num_left
// pick a number in the first partition
r = random(range_size) + last_k + 1
output(r)
last_k = r
num_left = num_left - 1
}
}
请注意,这需要O(k)时间并需要额外的O(1)空间。
答案 5 :(得分:0)
你可以在O(k)时间用Floyd的算法(不是Floyd-Warshall,这是最短路径的东西)来做。您需要的唯一数据结构是1位表,它将告诉您是否已经选择了一个数字。搜索哈希表可以是O(1),因此这不会成为负担,并且即使对于非常大的n也可以保留在内存中(如果n非常大,您必须使用b树或布隆过滤器或东西)。
从n:
中选择k项for j = n-k+1 to n:
select random x from 1 to j
if x is already in hash:
insert j into hash
else
insert x into hash
那就是它。最后,您的哈希表将包含n中k个项的统一选择样本。按顺序读出它们(你可能必须选择一种允许的哈希表)。
答案 6 :(得分:0)
这是一个使用O(√n)空间字的O(k log k +√n)时间算法。对于任何整数常数c,这可以推广到O(k + n ^(1 / c)) - 时间,O(n ^(1 / c)) - 空间算法。
为了直觉,想象一个简单的算法,它使用(例如)Floyd's采样算法生成n个元素的k,然后在基数√n中生成radix sorts个。我们不会记住实际样本是什么,而是先进行第一遍,我们运行Floyd的变体,我们只记得每个桶中的样本数。对于每个桶,第二遍是从桶范围中随机重新采样适当数量的元素。这是一个涉及条件概率的简短证明,它给出了均匀分布。
# untested Python code for illustration
# b is the number of buckets (e.g., b ~ sqrt(n))
import random
def first_pass(n, k, b):
counts = [0] * b # list of b zeros
for j in range(n - k, n):
t = random.randrange(j + 1)
if t // b >= counts[t % b]: # intuitively, "t is not in the set"
counts[t % b] += 1
else:
counts[j % b] += 1
return counts