如何生成升序随机整数列表

时间:2016-06-07 22:38:26

标签: c++ algorithm sorting random

我有一个包含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,但伪代码中的描述可能仍然有用。

7 个答案:

答案 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