【问题标题】:Unique random number generation in an integer array [duplicate]整数数组中的唯一随机数生成
【发布时间】:2010-12-09 03:42:11
【问题描述】:

可能重复:
Unique random numbers in O(1)?

如何在 C 中用唯一值(不重复)填充整数数组?

int vektor[10];   

for (i = 0; i < 10; i++) {
    vektor[i] = rand() % 100 + 1;
}

//No uniqueness here

【问题讨论】:

  • 顺便说一句,仅分配数组索引将满足“唯一值”的要求,而不是解决隐含的“唯一随机值”。
  • 不,不是。从 N 中仅选择 M 的要求(如上面的“100 中的 10”)是一个重要的细节。

标签: c algorithm random


【解决方案1】:

有几种方法可以解决您的问题,每种方法都有自己的优点和缺点。

首先我想指出,您已经收到不少响应,它们执行以下操作:它们生成一个随机数,然后以某种方式检查它是否已在数组中使用,如果已使用,它们只需生成另一个号码,直到他们找到未使用的号码。 这是一种幼稚的方法,说实话,存在严重缺陷。问题在于数字生成的循环试错性质(“如果已使用,请再试一次”)。如果数字范围(例如,[1..N])接近所需数组的长度(例如,M),那么在接近尾声时,算法可能会花费大量时间来尝试找到下一个数字。如果随机数生成器有一点点损坏(例如,从不生成某个数字,或者很少生成),那么使用 N == M 算法可以保证永远循环(或很长时间)。一般来说,这种反复试验的方法是无用的,或者充其量是有缺陷的。

这里已经提出的另一种方法是在大小为 N 的数组中生成随机排列。随机排列的想​​法很有前途,但是在大小为 N 的数组上(当 M

可以在 Bentley 的“Programming Pearls”(其中一些摘自 Knuth)中找到该问题的良好解决方案。


  • Knuth 算法。 这是一个非常简单的算法,复杂度为 O(N)(即数值范围),这意味着当 M 接近 N 时它最有用。但是,该算法没有除了您的 vektor 数组之外,不需要任何额外的内存,而不是已经提供的具有排列的变体(这意味着它需要 O(M) 内存,而不是 O(N),因为这里建议的其他基于排列的算法)。后者使它成为一个可行的算法,即使对于 M

算法的工作原理如下:遍历从1到N的所有数字,并以rm / rn的概率选择当前数字,其中rm是我们还需要找到多少个数字,rn是多少个数字我们仍然需要迭代。这是您的案例的可能实现

#define M 10
#define N 100

int in, im;

im = 0;

for (in = 0; in < N && im < M; ++in) {
  int rn = N - in;
  int rm = M - im;
  if (rand() % rn < rm)    
    /* Take it */
    vektor[im++] = in + 1; /* +1 since your range begins from 1 */
}

assert(im == M);

在这个循环之后,我们得到一个数组vektor,其中填充了随机选择的数字升序。 “升序”位是我们在这里不需要的。因此,为了“修复”我们只需对vektor 的元素进行随机排列,我们就完成了。请注意,这是一个 O(M) 排列,不需要额外的内存。 (我省略了置换算法的实现。这里已经给出了很多链接。)。

如果您仔细查看此处提出的对长度为 N 的数组进行操作的基于排列的算法,您会发现它们中的大多数几乎都是相同的 Knuth 算法,但为 M == N 重新制定。在这种情况下,上述选择循环将以概率 1 选择 [1..N] 范围内的每个数字,有效地转化为数字 1 到 N 的 N 数组的初始化。考虑到这一点,我认为它变得相当很明显,为M == N 运行该算法然后截断结果(可能丢弃大部分结果)比仅以原始形式运行该算法以获取 M 的原始值并立即获得结果而没有任何截断的意义要小得多.


  • 弗洛伊德算法(参见here)。这种方法的复杂度约为 O(M)(取决于所使用的搜索结构),因此在 M 不会进行任何可恶的试错迭代,试图找到一个未使用的随机数。该算法保证在每次调用随机数生成器后生成一个唯一的随机数。

对于您的情况,这是一个可能的实现。 (有不同的方法可以跟踪已使用的数字。假设 N 不是太大,我将只使用一组标志)

#define M 10
#define N 100    

unsigned char is_used[N] = { 0 }; /* flags */
int in, im;

im = 0;

for (in = N - M; in < N && im < M; ++in) {
  int r = rand() % (in + 1); /* generate a random number 'r' */

  if (is_used[r])
    /* we already have 'r' */
    r = in; /* use 'in' instead of the generated number */

  assert(!is_used[r]);
  vektor[im++] = r + 1; /* +1 since your range begins from 1 */
  is_used[r] = 1;
}

assert(im == M);

上述工作的原因并不是很明显。但它有效。 [1..N] 范围内的恰好 M 个数字将均匀分布。

注意,对于较大的 N,您可以使用基于搜索的结构来存储“已使用”的数字,从而获得一个 O(M log M) 且内存要求为 O(M) 的不错的算法。

(虽然这个算法有一点:虽然结果数组不会被排序,但原始 1..N 排序的某种“影响”仍然会出现在结果中。例如,很明显数字N,如果被选中,只能是结果数组的最后一个成员。如果这种意外排序对结果的“污染”是不可接受的,那么结果vektor 数组可以随机打乱,就像在 Khuth 中一样算法)。


请注意在这两种算法的设计中观察到的非常关键的一点:它们从不循环,试图找到一个新的未使用的随机数。从实际的角度来看,任何使用随机数进行试错迭代的算法都是有缺陷的。此外,这些算法的内存消耗与 M 相关,而不是 N

对于 OP,我会推荐 Floyd 算法,因为在他的应用程序中,M 似乎远小于 N,并且它不需要(或可能不需要)额外的置换传递。然而,对于如此小的 N 值,差异可能可以忽略不计。

【讨论】:

  • 我不同意您关于“反复试验”无用的说法。朴素的试错算法即使在 N==M 时也有很强的保证(它以高概率在 O(nlgn) 时间内完成)。例如,对于 M
  • 我只能说这个保证不适合实践。对于 N==M 的情况,遇到质量差或质量差的 'rand()' 的无限循环的机会相当高(即使使用良好的rand(),最后一个元素的搜索时间也很长的机会是更高)。我不知道您在实践中如何合理地期望O(n lg n)。在理想的世界里,也许……
  • O(n lg n) 可能来自类似于(令人惊讶的)优惠券收集者问题的分析:en.wikipedia.org/wiki/Coupon_collector%27s_problem 虽然糟糕的 rand() 可能会使更糟糕的是,只要 rand() 实际上达到所有值,它就应该只关闭一个常量:我不知道任何 rand() 实现,这不会是真的。
  • 或者,对优惠券收集者问题的(简短)解决方案的非正式总结:确实,在列表末尾附近,您可能需要调用 rand() O(n) 次以找到一个新元素,但这仅适用于其中大约 O(log n) 的元素,因此一切正常。这个 O(n log n) 是否真的足够好是另一回事:不要低估那些对数因素!
  • 我也不同意反复试验是“无用的”,因为大多数可靠的爬山算法都会在某些时候使用它。一个有趣的谷歌将是 enigma m4 项目,其中 steckers 在分布式网络中爬山。然而,+1,这显然是问题的最佳答案。
【解决方案2】:

在您的示例中(选择 10 个介于 1 和 100 之间的唯一随机数),您可以创建一个包含 1 到 100 数字的列表,使用随机数生成器对列表进行打乱,然后从列表中获取前 10 个值.

int list[100], vektor[10];
for (i = 0; i < 100; i++) {
    list[i] = i;
}
for (i = 0; i < 100; i++) {
    int j = i + rand() % (100 - i);
    int temp = list[i];
    list[i] = list[j];
    list[j] = temp;
}
for (i = 0; i < 10; i++) {
    vektor[i] = list[i];
}

根据下面cobbal的评论,最好说:

for (i = 0; i < 10; i++) {
    int j = i + rand() % (100 - i);
    int temp = list[i];
    list[i] = list[j];
    list[j] = temp;

    vektor[i] = list[i];
}

现在设置列表是 O(N),但选择随机元素是 O(M)。

【讨论】:

  • 在一般情况下效率较低,即当范围长度(例如,N)明显大于所需的数组长度(例如,M)时。一个有效的算法应该接近 O(M)。这个是 O(N)。
  • 我同意 - 请参阅 eyalim 链接中接受的答案。
  • 在使用的随机数机制中有一个很小但不一定可以忽略的偏差,但如果你解决了这个问题,这是一个很好的技术。注意中间循环的上限;您只能将 list[99] 与自身交换,您的代码可以这样做,但这有点“浪费”。
  • 有可能运行到 i
  • @mobrule:链接上接受的答案仅适用于您需要 1000 个长度范围内的 1000 个数字的情况。对于 OP 的问题,该方法只会产生比光更多的热量。
【解决方案3】:

简单地生成随机数并查看它们是否正常通常是解决此问题的糟糕方法。这种方法采用所有可能的值,将它们打乱,然后取前十名。这直接类似于洗牌并从顶部发牌。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define randrange(N) rand() / (RAND_MAX/(N) + 1)

#define MAX 100        /* Values will be in the range (1 .. MAX) */
static int vektor[10];
int candidates[MAX];

int main (void) {
  int i;

  srand(time(NULL));   /* Seed the random number generator. */

  for (i=0; i<MAX; i++)
    candidates[i] = i;

  for (i = 0; i < MAX-1; i++) {
    int c = randrange(MAX-i);
    int t = candidates[i];
    candidates[i] = candidates[i+c];
    candidates[i+c] = t;
  }

  for (i=0; i<10; i++)
    vektor[i] = candidates[i] + 1;

  for (i=0; i<10; i++)
    printf("%i\n", vektor[i]);

  return 0;
}

有关详细信息,请参阅comp.lang.c FAQ list question 13.19 了解随机数和question 13.16 有关生成随机数。

【讨论】:

    【解决方案4】:

    我认为这可以做到(我没有尝试构建它,所以语法错误留给读者作为练习来修复)。可能有更优雅的方法,但这是蛮力解决方案:

    int vektor[10];    
    int random;
    int uniqueflag;
    int i, j
    
    for(i = 0; i < 10; i++) {
         do {
            /* Assume things are unique... we'll reset this flag if not. */
            uniqueflag = 1;
            random = rand() % 100+ 1;
            /* This loop checks for uniqueness */
            for (j = 0; j < i && uniqueflag == 1; j++) {
               if (vektor[j] == random) {
                  uniqueflag = 0;
               }
            }
         } while (uniqueflag != 1);
         vektor[i] = random;
    }
    

    【讨论】:

    • 任何使用“重试”方法的算法的实用价值都非常有限。实际上,我会说 suffling 方法更好,但是 shuffle 可以更好地实现(请参阅我的回复中的 Knuth 方法)。
    【解决方案5】:

    一种方法是检查数组是否已经包含新的随机数,如果是,则创建一个新的并重试。

    这开启了 (random ;) ) 的可能性,即您永远不会得到不在数组中的数字。因此,您应该计算检查数字是否已经在数组中的次数,如果计数超过 MAX_DUPLICATE_COUNT,则抛出异常左右:)(编辑,看到您在 C 中。忘记异常部分 :) 返回错误代码代替:P)

    【讨论】:

    • 好吧,如果我给一个函数一个定义明确的任务和一个定义明确的解决方案,我会感到非常惊讶,并且该函数会返回一个“对不起,我不能这样做这次”错误代码:)
    • 哈哈,是的,那看起来很棒:)
    【解决方案6】:

    一个快速的解决方案是创建一个掩码数组,其中包含初始化为零的所有可能数字,并在生成该数字时设置一个条目

    int rand_array[100] = {0};
    int vektor[10];   
    int i=0, rnd;
    while(i<10) {
        rnd = rand() % 100+ 1;
        if ( rand_array[rnd-1] == 0 ) {
            vektor[i++] = rnd;
            rand_array[rnd-1] = 1;
        }
    }
    

    【讨论】:

      【解决方案7】:

      这是一个 O(M) 平均时间的方法。

      方法:如果 M N/2,使用过程 S(NM,N)生成 R,然后计算 X = {1..M}\R [R 在 {1..M} 中的补码],将 X 与 Fisher-Yates shuffle [in time O(M)] 混洗,并返回 X。

      在 M > N/2 的情况下,其中 O(M) == O(N),有几种快速计算补码的方法。在下面显示的代码中,为简洁起见,我只包含了一个在 main() 中内联编码的过程 S(M,N) 的示例。 Fisher-Yates shuffle 为 O(M),并在相关问题 #196017 的主要答案中进行了说明。其他之前的相关问题:#158716#54059

      当 M Coupon-collector's problem 中所述,期望 E(t_k) 为 kH_k,其中 E(t_{k/2}) = k(H_k - H_{k/2}) 或大约 k*(ln(k)-ln(k/2)+O(1) )) = k*(ln(k/(k/2))+O(1)) = k*(ln(2)+O(1)) = O(k)。

      过程S(k,N):[这个过程的主体是下面代码中“Gen M distinct random numbers”注释之后的十几行。]分配并初始化三个M+1元素整数数组H, L 和 V 为所有 -1 值。对于 i=0 到 M-1:将随机值 v 放入 V[i] 并放入哨兵节点 V[-1]。从 H[v%M] 中获取 M 列表头之一并跟随该列表直到找到与 v 匹配。如果匹配在 V[-1] 则 v 是一个新值;所以更新列表头 H[v%M] 和列表链接 L[i]。如果匹配不在 V[-1],获取并测试另一个 v,等等。

      每个“跟随列表”步骤的预期成本为 O(1),因为在除最后一步之外的每个步骤中,平均列表长度小于 1。(在处理结束时,M 个列表包含 M 个元素,因此平均长度逐渐正好上升到 1。)

       // randomMofN - jiw 8 Nov 2011     
       // Re: https://stackoverflow.com/questions/1608181/
       #include <stdlib.h>
       #include <stdio.h>
       int main(int argc, char *argv[]) {
         int h, i, j, tM, M, N, par=0, *H, *L, *V, cxc=0;
         // Get M and N values
         ++par; M = 42;  if (argc > par) M = atoi(argv[par]);
         ++par; N = 137; if (argc > par) N = atoi(argv[par]);
         tM = 3*M+3;
         H = malloc(tM*sizeof(int));
         printf ("M = %d,  N = %d  %s\n", M, N, H?"":"\nmem error");
         if (!H) exit(13);
         for (i=0; i<tM; ++i)           // Init arrays to -1's
           H[i] = -1;
         L = H+M;  V = L+M;
      
         // Gen M distinct random numbers
         for (i=0; i<M; ++i) {
           do {
             ++cxc;                     // complexity counter
             V[-1] = V[i] = random()%N;
             h = V[i]%M;                // h = list-head index
             j = H[h];
             while (V[j] != V[i])
               j = L[j];
           } while (j>=0);
           L[i] = H[h];
           H[h] = i;
         }
      
         // Print results
         for (j=i=0; i<M; ++i) {
           j += printf ("%4d ", V[i]);
           if (j>66) j = printf ("\n");
         }
         printf ("\ncxc %d\n", cxc);
         return 0;
       }
      

      【讨论】:

      • 问题#2394246 也是相关的,包括对罗伯特·弗洛伊德采样算法的讨论。
      【解决方案8】:

      我喜欢弗洛伊德算法。

      但是我们可以从0M(而不是in)的所有随机数:

      #define M 10
      #define N 100    
      
      unsigned char is_used[N] = { 0 }; /* flags */
      int in, im;
      
      im = 0;
      
      for (in = N - M; in < N && im < M; ++in) {
        int r = rand() % (N + 1); /* generate a random number 'r' */
      
        while (is_used[r])
        {
           /* we already have 'r' */
           r = rand() % (N + 1);
        }
        vektor[im++] = r + 1; /* +1 since your range begins from 1 */
        is_used[r] = 1;
      }
      
      assert(im == M);
      

      【讨论】:

        【解决方案9】:

        分别生成第一位和第二位数字。 如果需要,稍后将它们洗牌。 (记忆中的语法)

        int vektor[10];
        int i = 0;
        
        while(i < 10) {
          int j = rand() % 10;
          if (vektor[j] == 0) { vektor[j] = rand() % 10 + j * 10; i ++;}
        }
        

        但是,这些数字几乎相差 n,0

        否则,您需要保持数字排序 (O(n log n)),以便可以快速检查新生成的数字是否存在 (O(log n))。

        【讨论】:

          猜你喜欢
          • 2016-07-03
          • 1970-01-01
          • 1970-01-01
          • 2017-06-06
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-07-20
          相关资源
          最近更新 更多