您如何有效地生成介于 0 和上限 N 之间的 K 个非重复整数列表 [重复]

Posted

技术标签:

【中文标题】您如何有效地生成介于 0 和上限 N 之间的 K 个非重复整数列表 [重复]【英文标题】:How do you efficiently generate a list of K non-repeating integers between 0 and an upper bound N [duplicate] 【发布时间】:2010-09-14 14:17:39 【问题描述】:

问题给出了所有必要的数据:什么是在给定区间 [0,N-1] 内生成 K 个非重复整数序列的有效算法.如果 K 很大并且足够接近 N。

Efficiently selecting a set of random elements from a linked list 中提供的算法似乎比必要的复杂,需要一些实现。我刚刚发现另一种算法似乎可以很好地完成这项工作,只要您一次知道所有相关参数即可。

【问题讨论】:

等等,如果你已经找到了另一种算法,那么问题是什么? 这么简洁的算法!不得不与某人分享 - 根据***.com/faq,这似乎是推荐的行为:“询问和回答你自己的编程问题也很好,但假装你在危险中 这个问题的答案对我来说是最好的。 ***.com/questions/2394246/… @tucuxi 我得到了全权委托以缩小meta.***.com/questions/334325/… 的范围。诚然,我应该在编辑摘要中提到这一点。 【参考方案1】:

The Art of Computer Programming, Volume 2: Seminumerical Algorithms, Third Edition 中,Knuth 描述了以下选择抽样算法:

算法 S(选择采样技术)。从一组 N 中随机选择 n 条记录,其中 0

S1。 【初始化】设置t ← 0, m ← 0。(在这个算法中,m代表到目前为止选择的记录数,t是我们处理过的输入记录的总数。)

S2。 [生成 U。] 生成一个随机数 U,均匀分布在 0 和 1 之间。

S3。 【测试】如果(N – t)U ≥ n – m,进入步骤S5。

S4。 [Select.] 选择样本的下一条记录,将m和t加1。如果m

S5。 [Skip.] 跳过下一条记录(样本中不包含),t加1,返回步骤S2。

实现可能比描述更容易理解。这是一个从列表中随机选择 n 个成员的 Common Lisp 实现:

(defun sample-list (n list &optional (length (length list)) result)
  (cond ((= length 0) result)
        ((< (* length (random 1.0)) n)
         (sample-list (1- n) (cdr list) (1- length)
                      (cons (car list) result)))
        (t (sample-list n (cdr list) (1- length) result))))

这是一个不使用递归的实现,它适用于各种序列:

(defun sample (n sequence)
  (let ((length (length sequence))
        (result (subseq sequence 0 n)))
    (loop
       with m = 0
       for i from 0 and u = (random 1.0)
       do (when (< (* (- length i) u) 
                   (- n m))
            (setf (elt result m) (elt sequence i))
            (incf m))
       until (= m n))
    result))

【讨论】:

感谢权威解答。我有同样的要求,这是我计划实施的算法。再次感谢。【参考方案2】:

Python 库中的random module 使其非常简单有效:

from random import sample
print sample(xrange(N), K)

sample 函数返回从给定序列中选择的 K 个唯一元素的列表。xrange 是一个“列表模拟器”,即它的行为类似于一个连续数字列表,而无需在内存中创建它,这使得对于像这样的任务,它的速度非常快。

【讨论】:

python 实现相当不错(见svn.python.org/view/python/trunk/Lib/random.py?view=markup,搜索“sample”)。他们区分了两种情况,一种是大 K(K 接近 N),另一种是小 K。对于大 K,他们选择性地复制元素。对于小 K,他们随机绘制元素,避免使用集合重复。 这对于大序列来说内存效率很低。 hg.python.org/cpython/file/tip/Lib/random.py 是新的源链接。 为什么不只是random.shuffle 答案缺乏解释 - 请参阅 Jonathans Hartley 的评论。【参考方案3】:

实际上可以在与所选元素数量成正比的空间中执行此操作,而不是您选择的集合的大小,无论您选择的总集合的比例如何。为此,您可以生成一个随机排列,然后像这样从中选择:

选择一个分组密码,例如 TEA 或 XTEA。使用XOR folding 将块大小减小到比您要从中选择的集合大二的最小幂。使用随机种子作为密码的密钥。要在排列中生成元素 n,请使用密码加密 n。如果输出编号不在您的集合中,请对其进行加密。重复直到数字在集合内。平均而言,每个生成的数字必须执行少于两次的加密。这还有一个额外的好处,即如果您的种子是加密安全的,那么您的整个排列也是如此。

我更详细地写了这个here。

【讨论】:

好文章。但是,“异或折叠”不会破坏唯一性吗?当然, x != y 意味着 encipher(x) != encipher(y) 用于解码工作,但使用例如(encipher(x) >> 4) ^ (encipher(x) & MASK) 相反可以将不同的 x 值“折叠”到相同的代码中——因此您的“排列”可能包含重复。 我没有理论基础,但是不,它不会破坏分组密码的一对一映射属性。 Xor 折叠取自 TEA 密码 - 更多详细信息请查看参考资料。 @j_random_hacker:当然,你是对的。但是仍然可以使用自定义的 Feistel 密码,使用一些密码散列函数作为函数 F 来提出伪随机排列。 见这里:***.com/questions/196017/unique-random-numbers-in-o1/… 对于今天阅读本文的任何人来说,虽然这种方法听起来可能会更好,但 random 中的 sample 方法与 range 一起使用(在我的实验中)实际上比 TEA 更快,即使你只使用一个周期。此外,当仅使用 v0 作为输出时,我确实偶尔会得到重复。对于那个实验,我创建了一个基于 TEA 的数字生成器,并初始化并计算了 10.000 组 2048 个数字,并在 6 个案例中生成了一个副本。也许多个周期会有所帮助,但即使是一个周期,它也已经比 random.sample 慢,这也保证了唯一的数字。【参考方案4】:

以下代码(C 语言,来源不明)似乎很好地解决了这个问题:

 /* generate N sorted, non-duplicate integers in [0, max[ */
 int *generate(int n, int max) 
    int i, m, a;    
    int *g = (int *)calloc(n, sizeof(int));
    if ( ! g) return 0;

    m = 0;
    for (i=0; i<max; i++) 
        a = random_in_between(0, max - i);
        if (a < n - m) 
            g[m] = i;
            m ++;
        
    
    return g;
 

有人知道我在哪里可以找到更多像这样的宝石吗?

【讨论】:

Jon Bentley 的编程珍珠(“宝石”的双关语是故意的)。 :) “random_in_between”代表什么? 这种算法对于从大集合中选择的小样本来说效率非常低。从一百万中挑选 5 个整数需要一百万次调用 rand() 而不是 5。 感谢书名——我想不出任何其他方法来找到它。 Luis, random_in_between 用于“lo 和 hi 之间的数字,不包括 hi”。普拉塔克,完全正确。应该指定“内存效率”与“时间效率”。至少可以保证在有限的时间内完成...... 这是在another answer中也描述的Knuth算法。【参考方案5】:

生成一个数组0...N-1 填充a[i] = i

然后随机播放第一个 K 项。

洗牌:

开始J = N-1 选择一个随机数0...J(比如R) 将a[R]a[J] 交换 由于R 可以等于J,因此元素可以与自身交换 从J 中减去1 并重复。

最后,取K最后一个元素。

这实际上是从列表中选择一个随机元素,将其移出,然后从剩余列表中选择一个随机元素,依此类推。

O(K)O(N) 时间内工作,需要 O(N) 存储。

洗牌部分称为Fisher-Yates shuffleKnuth 的洗牌,在计算机编程的艺术第 2 卷中有描述。

【讨论】:

您的方法适用于在 [0, N[ 中生成排列,但我想要 [0, K[ 范围内的数字。例如,如果 N=2 且 K=10,则 5, 9 是有效的输出序列。 然后生成0..K,然后随机删除数字,直到有N个数字。 这不是一律随机的:因为J 曾经从k[J] 移开一次,所以它被选中的概率不同。例如。 K=1, N-1 永远不能被选中。 @ivan_pozdeev 否。请注意,在我的示例中,R 首先位于 0...9 范围内,这意味着 R=9 可能会与 A[9] 自身交换。 好的,我知道了,但是您的解释中缺少 1。【参考方案6】:

通过将 K 个数字存储在散列存储中来加速简单算法。在开始之前知道 K 可以消除插入哈希映射的所有低效率,并且您仍然可以获得快速查找的好处。

【讨论】:

是的,当我需要 1000 万个非重复随机数用于彩票时,我就是这样做的 不太节省内存 - 需要一个 K 大小的辅助结构。随着时间的推移,您需要 K 次插入和 N 次删除。我发现的算法只需要(最多)K 次随机抽取。 你根本不需要辅助结构。只需让地图成为您唯一的结构。你总是需要 K 次插入来存储 K 项。为什么需要 N 次移除? 插入并检查 K 大小的数据结构并不是琐碎算法的问题所在,因为 K -> N,您的 RNG 将很有可能生成一个数字'之前在填充序列末尾时已经看到过。你需要一个哈希映射,但那是辅助的。【参考方案7】:

我的解决方案是面向 C++ 的,但我确信它可以翻译成其他语言,因为它非常简单。

首先,生成一个有K个元素的链表,从0到K 那么只要列表不为空,就生成一个介于 0 和向量大小之间的随机数 获取该元素,将其推入另一个向量,然后将其从原始列表中删除

这个解决方案只涉及两次循环迭代,没有哈希表查找或任何类似的东西。所以在实际代码中:

// Assume K is the highest number in the list
std::vector<int> sorted_list;
std::vector<int> random_list;

for(int i = 0; i < K; ++i) 
    sorted_list.push_back(i);


// Loop to K - 1 elements, as this will cause problems when trying to erase
// the first element
while(!sorted_list.size() > 1) 
    int rand_index = rand() % sorted_list.size();
    random_list.push_back(sorted_list.at(rand_index));
    sorted_list.erase(sorted_list.begin() + rand_index);
                 

// Finally push back the last remaining element to the random list
// The if() statement here is just a sanity check, in case K == 0
if(!sorted_list.empty()) 
    random_list.push_back(sorted_list.at(0));

【讨论】:

【参考方案8】:

第 1 步:生成整数列表。 第 2 步:执行Knuth Shuffle。

请注意,您不需要对整个列表进行随机播放,因为 Knuth 随机播​​放算法只允许您应用 n 次随机播放,其中 n 是要返回的元素数。生成列表仍然需要与列表大小成正比的时间,但您可以重用现有列表以满足任何未来的洗牌需求(假设大小保持不变),而无需在重新启动洗牌算法之前对部分洗牌的列表进行预洗牌。

Knuth Shuffle 的基本算法是从整数列表开始。然后,将第一个整数与列表中的任何数字交换并返回当前(新)第一个整数。然后,将第二个整数与列表中的任何数字(第一个除外)交换并返回当前(新)的第二个整数。然后……等等……

这是一个简单得离谱的算法,但在执行交换时要小心将当前项目包含在列表中,否则会破坏算法。

【讨论】:

【参考方案9】:

Reservoir Sampling 版本非常简单:

my $N = 20;
my $k;
my @r;

while(<>) 
  if(++$k <= $N) 
    push @r, $_;
   elsif(rand(1) <= ($N/$k)) 
    $r[rand(@r)] = $_;
  


print @r;

这是从 STDIN 中随机选择的 $N 行。如果您不使用文件中的行,请将 $_ 替换为其他内容,但这是一个非常简单的算法。

【讨论】:

【参考方案10】:

如果列表是排序好的,例如,如果你想从N中提取K个元素,但你并不关心它们的相对顺序,那么在论文An Efficient Algorithm for Sequential Random Sampling中提出了一种高效的算法(Jeffrey Scott Vitter,ACM Transactions on Mathematical Software,第 13 卷,第 1 期,1987 年 3 月,第 56-67 页。)。

已编辑以使用 boost 在 c++ 中添加代码。我刚刚输入了它,可能有很多错误。随机数来自 boost 库,带有一个愚蠢的种子,所以不要对此做任何严重的事情。

/* Sampling according to [Vitter87].
 * 
 * Bibliography
 * [Vitter 87]
 *   Jeffrey Scott Vitter, 
 *   An Efficient Algorithm for Sequential Random Sampling
 *   ACM Transactions on MAthematical Software, 13 (1), 58 (1987).
 */

#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <string>
#include <iostream>

#include <iomanip>

#include <boost/random/linear_congruential.hpp>
#include <boost/random/variate_generator.hpp>
#include <boost/random/uniform_real.hpp>

using namespace std;

// This is a typedef for a random number generator.
// Try boost::mt19937 or boost::ecuyer1988 instead of boost::minstd_rand
typedef boost::minstd_rand base_generator_type;

    // Define a random number generator and initialize it with a reproducible
    // seed.
    // (The seed is unsigned, otherwise the wrong overload may be selected
    // when using mt19937 as the base_generator_type.)
    base_generator_type generator(0xBB84u);
    //TODO : change the seed above !
    // Defines the suitable uniform ditribution.
    boost::uniform_real<> uni_dist(0,1);
    boost::variate_generator<base_generator_type&, boost::uniform_real<> > uni(generator, uni_dist);



void SequentialSamplesMethodA(int K, int N) 
// Outputs K sorted random integers out of 0..N, taken according to 
// [Vitter87], method A.
    
    int top=N-K, S, curr=0, currsample=-1;
    double Nreal=N, quot=1., V;

    while (K>=2)
        
        V=uni();
        S=0;
        quot=top/Nreal;
        while (quot > V)
            
            S++; top--; Nreal--;
            quot *= top/Nreal;
            
        currsample+=1+S;
        cout << curr << " : " << currsample << "\n";
        Nreal--; K--;curr++;
        
    // special case K=1 to avoid overflow
    S=floor(round(Nreal)*uni());
    currsample+=1+S;
    cout << curr << " : " << currsample << "\n";
    

void SequentialSamplesMethodD(int K, int N)
// Outputs K sorted random integers out of 0..N, taken according to 
// [Vitter87], method D. 
    
    const int negalphainv=-13; //between -20 and -7 according to [Vitter87]
    //optimized for an implementation in 1987 !!!
    int curr=0, currsample=0;
    int threshold=-negalphainv*K;
    double Kreal=K, Kinv=1./Kreal, Nreal=N;
    double Vprime=exp(log(uni())*Kinv);
    int qu1=N+1-K; double qu1real=qu1;
    double Kmin1inv, X, U, negSreal, y1, y2, top, bottom;
    int S, limit;
    while ((K>1)&&(threshold<N))
        
        Kmin1inv=1./(Kreal-1.);
        while(1)
            //Step D2: generate X and U
            while(1)
                
                X=Nreal*(1-Vprime);
                S=floor(X);
                if (S<qu1) break;
                Vprime=exp(log(uni())*Kinv);
                
            U=uni();
            negSreal=-S;
            //step D3: Accept ?
            y1=exp(log(U*Nreal/qu1real)*Kmin1inv);
            Vprime=y1*(1. - X/Nreal)*(qu1real/(negSreal+qu1real));
            if (Vprime <=1.) break; //Accept ! Test [Vitter87](2.8) is true
            //step D4 Accept ?
            y2=0; top=Nreal-1.;
            if (K-1 > S)
                bottom=Nreal-Kreal; limit=N-S;
            else bottom=Nreal+negSreal-1.; limit=qu1;
            for(int t=N-1;t>=limit;t--)
                y2*=top/bottom;top--; bottom--;
            if (Nreal/(Nreal-X)>=y1*exp(log(y2)*Kmin1inv))
                //Accept !
                Vprime=exp(log(uni())*Kmin1inv);
                break;
                
            Vprime=exp(log(uni())*Kmin1inv);
            
        // Step D5: Select the (S+1)th record
        currsample+=1+S;
        cout << curr << " : " << currsample << "\n";
        curr++;
        N-=S+1; Nreal+=negSreal-1.;
        K-=1; Kreal-=1; Kinv=Kmin1inv;
        qu1-=S; qu1real+=negSreal;
        threshold+=negalphainv;
        
    if (K>1) SequentialSamplesMethodA(K, N);
    else 
        S=floor(N*Vprime);
        currsample+=1+S;
        cout << curr << " : " << currsample << "\n";
        
    


int main(void)
    
    int Ntest=10000000, Ktest=Ntest/100;
    SequentialSamplesMethodD(Ktest,Ntest);
    return 0;
    

$ time ./sampling|tail

在我的笔记本电脑上提供以下输出

99990 : 9998882
99991 : 9998885
99992 : 9999021
99993 : 9999058
99994 : 9999339
99995 : 9999359
99996 : 9999411
99997 : 9999427
99998 : 9999584
99999 : 9999745

real    0m0.075s
user    0m0.060s
sys 0m0.000s

【讨论】:

根据***.com/a/2394292/648265,这会生成组合。不是排列。 问的是“K 个不重复整数的列表”而不是排列。我在回答中指定“如果您对订单不感兴趣”【参考方案11】:

这段 Ruby 代码展示了 Reservoir Sampling, Algorithm R 方法。在每个循环中,我从[0,N=10)范围中选择n=5唯一的随机整数:

t=0
m=0
N=10
n=5
s=0
distrib=Array.new(N,0)
for i in 1..500000 do
 t=0
 m=0
 s=0
 while m<n do

  u=rand()
  if (N-t)*u>=n-m then
   t=t+1
  else 
   distrib[s]+=1
   m=m+1
   t=t+1
  end #if
  s=s+1
 end #while
 if (i % 100000)==0 then puts i.to_s + ". cycle..." end
end #for
puts "--------------"
puts distrib

输出:

100000. cycle...
200000. cycle...
300000. cycle...
400000. cycle...
500000. cycle...
--------------
250272
249924
249628
249894
250193
250202
249647
249606
250600
250034

0-9 之间的所有整数都以几乎相同的概率被选中。

它本质上是Knuth's algorithm 应用于任意序列(实际上,这个答案有一个 LISP 版本)。该算法在时间上是 O(N),如果序列如@MichaelCramer's answer 所示流入其中,则在内存中可以是 O(1)

【讨论】:

您应该测量每个完整排列的概率而不是单个数字以实际显示方法的质量 - 否则,您只会显示数字集选择的随机性,而不是它们的顺序。【参考方案12】:

这是一种在 O(N) 中无需额外存储空间的方法。我很确定这不是一个纯粹的随机分布,但它可能已经足够接近许多用途了。

/* generate N sorted, non-duplicate integers in [0, max[  in O(N))*/
 int *generate(int n, int max) 
    float step,a,v=0;
    int i;    
    int *g = (int *)calloc(n, sizeof(int));
    if ( ! g) return 0;

    for (i=0; i<n; i++) 
        step = (max-v)/(float)(n-i);
        v+ = floating_pt_random_in_between(0.0, step*2.0);
        if ((int)v == g[i-1])
          v=(int)v+1;             //avoid collisions
        
        g[i]=v;
    
    while (g[i]>max) 
      g[i]=max;                   //fix up overflow
      max=g[i--]-1;
    
    return g;
 

【讨论】:

【参考方案13】:

这是 Perl 代码。 grep 是一个过滤器,和往常一样我没有测试这段代码。

@list = grep ($_ % I) == 0, (0..N);
I = 区间 N = 上限

仅通过模运算符获取与您的区间匹配的数字。

@list = grep ($_ % 3) == 0, (0..30);

将返回 0、3、6、... 30

这是伪 Perl 代码。您可能需要对其进行调整才能编译。

【讨论】:

它似乎没有执行指定的任务。

以上是关于您如何有效地生成介于 0 和上限 N 之间的 K 个非重复整数列表 [重复]的主要内容,如果未能解决你的问题,请参考以下文章

GetMonthName:有效值介于 1 和 13 之间,包括 1 和 13。为啥?

使用VBS如何能随机产生一个介于50与100之间的整数?

使用 numpy.random.normal 时如何指定上限和下限

如何在没有 for 循环的情况下生成 n 个随机数

O(1)中的唯一(非重复)随机数?

证明一个随机生成的数是均匀分布的