zoukankan      html  css  js  c++  java
  • 【转】关于C语言生成不重复的随机数

    一 说起随机函数,恐怕又有人说这是老生长谈了……一般很多人都形成了自己的固定格式,因为随机数用处比较大,用的时候比较多,拿过来就用了。但是新手不这么 干,他们总是抱有疑惑,我就是一个新手,而且较菜……为了让跟我一样的菜鸟看明白,我会尽量的说得让高手们不屑一顾(:由于可能内容太多可能会分篇,大家 见谅^

     计算机的好处是精确,所以它不擅长模拟信号,但它的缺点也是 如此。于是在一些模拟问题上计算机遇到麻烦了……比如所随机数,因为函数嘛,总会是确定的,确定的算法就会生成确定的结果。各种编程语言返回的随机数(确 切地说是伪随机数)实际上都是根据递推公式计算的一组数值,当序列足够长,这组数值近似满足均匀分布。c的标准函数库提供一随机数生成器rand(定义在 stdlib.h),能返回0-RAND_MAX之间均匀分布的伪随机整数(RAND_MAX至少为32767,一般都默认为32767)。

    例如:

    #include<stdio.h>
    #include<stdlib.h>
    void main()
    {
           for(int i=0;i<10;i+)
                 printf("%d ",rand());
    }

    如 果我们是第一次运行,而且对其不太清楚,那么它生成的基本上算是0-RAND_MAX之间的等概率随机数列了。但是如果你第二次运行的时候会发现输出结果 仍和第一次一样。:(原来rand()生成伪随机数时需要一个种子(计算伪随机序列的初始数值)才行,如果种子相同就会得到相同的序列结果(这就是函数的 好处T-T)。这个“优点”被有的软件利用于加密和解密。加密时,可以用某个种子数生成一个伪随机序列并对数据进行处理;解密时,再利用种子数生成一个伪 随机序列并对加密数据进行还原。这样,对于不知道种子数的人要想解密就需要多费些事了。当然,这种完全相同的序列对于你来说是非常糟糕的。要解决这个问 题,需要在每次产生随机序列前,先指定不同的种子,这样计算出来的随机序列就不会完全相同了。

    srand() 用来设置rand()产生随机数时的随机数种子。在调用rand()函数产生随机数前,必须先利用srand()设好随机数种子(seed), 如果未设随机数种子, rand()在调用时会自动设随机数种子为1(有人说默认是0,困惑中)。上面的两个例子就是因为没有设置随机数种子,每次随机数种子都自动设成相同值1 ,进而导致rand()所产生的随机数值都一样。(可能有人知道C语言中的随机函数random,可是random函数并不是ANSI C标准,所以说,random函数不能在gcc,vc等编译器下编译通过。我们可以自己编一个^0^)我们需要使程序每一次使用的种子都不一样,现在主要 问题是种子srand的选择是不是接近随机(不存在完全随机),你也可以人为指定种子数。Windows 9x/NT的游戏FreeCell就允许用户指定种子数,这样用户如果一次游戏没有成功,下次还可以以同样的发牌结果再玩一次。但我们还是喜欢系统自动生 成……最简单的方法就是利用系统时间了(几乎所有的人都这么做),因为时间的数值随时间变化而变化,运行两次,一般不会出现前一次和后一次相同的局 面,time(NULL)会返回一个表示当前系统时间的整数(它在time.h中,据说time()函数求出来的是自1970年1月1日到现在的秒数,有 的说是unix元年,不知道这两个是不是一天……:( ,另外有的嫌麻烦会写作time(0))。我们这么来写:

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

    void main()
    {

         srand((int)time(0));
         for(int x=0;x<10;x++)
               printf("%d ",(rand());
    }

     据说如果软件一直开两天,种子会有1/(60*60*24)个可能会重复,虽说这对于一般人已经绝对足够了,但纵然人不舒服,于是我在网上开到有这么写的:

     #include<stdio.h>
    #include<stdlib.h>
    #include<sys/types.h>
    #include<sys/timeb.h>

    void main(void)
    {
        int i;
        unsigned int seedVal;
        struct timeb timeBuf;
        ftime(&timeBuf);
        seedVal=((((unsigned int)timeBuf.time&0xFFFF)+
                     (unsigned int)timeBuf.millitm)^
                     (unsigned int)timeBuf.millitm);
        srand((unsigned int)seedVal);
        for(i=0;i<10;++i)
            printf("%6d ",rand());
    }

    (下面是关于它的解释,但我也不是太明白,引用过来大家分析一下)

    “ 上面的程序先是调用_ftime()来检查当前时间,并把它的值存入结构成员timeBuf.time中,当前时间的值从1970年1月1日开始以秒计 算。在调用了_ftime()之后,在结构timeBuf的成员millitm中还存入了当前那一秒已经度过的毫秒数,但在DOS中这个数字实际上是以百 分之一秒来计算的。然后,把毫秒数和秒数相加,再和毫秒数进行异或运算。当然也可以对这两个结构成员进行更多的计算,以控制seedVal的取值范围,并 进一步加强它的随机性,但上例用的逻辑运算就已经足够了。”

    看下面代码:

    void main() 

     for(int i=0;i<100000;i++) 
     { 
     srand( (unsigned)time( NULL ) ); 
     printf("%d ",rand());
     } 
    }

     为 什么总是生成一个数???因为此程序产生一个随机数之前,都调用一次srand,而由于计算机运行很快,所以每用time得到的时间都是一样的(time 的时间精度较低,只有55ms)。这样相当于使用同一个种子产生随机序列,所以产生的随机数总是相同的。把srand放在循环外看看: 
    srand( (unsigned)time( NULL ) ); 
    for(int i=0;i<100000;i++) 
    问题解决了:)

    既 然生成的是 0-RAND_MAX之间均匀分布的随机整数(我们姑且把以上方法生成的是理想的随机数吧),那么要生成0-x之间(包括0不包括x)的随机数就把 rand()改成  rand()/(double)RAND_MAX*x ,要生成x-y之间(包括x不包括y)的就是  rand()/(double)RAND_MAX*(y-x)+x   了。但是如果要生0-10之间的整数很多人会这么写:

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

    void main()
    {

         srand((int)time(0));
         for(int x=0;x<10;x++)
               printf("%d ",(rand()%10);
    }

    x-y的就是 rand()%(y-x)+x

    ??? 懂一点概率的就知道这样写,会使得到的数列分布不均的,但是大家确实都喜欢这么写……因为在x的值比较小,RAND_MAX相对较大,而生成的数列有不算 太大,又是用来解决精确度要求不高的问题(如一些游戏目标,传说游戏中踩地雷式的遇敌就是用rand()实现的. 当主角在地图上走的时候动不动冒出三两小兵挑衅兼找死.它的实现方式是设主角所立位置为0,主角每走一步,变量加1,当变量==随机取得的数时,小兵出 现),这样写足够了……

     

     

     

    下面再讨论生成m个小于n的不重复随机数的算法

    本人认为算法结构很重要,但语句结构也很重要,所以我喜欢一个算法给出多个语句结构……

    最容易想到的傻瓜算法,逐个产生这些随机数,每产生一个,都跟前面的随机数比较,如果重复,就重新产生:

    算法一(1)

    srand((unsigned)time(NULL));

    for(j = 0; j < m; j++) {

    a:a[j] = rand() % n; 
    for(i=0;i<j;i++){if(a[i]==a[j])goto a;}

    很 早的时候学编程都喜欢用goto语句,因为过去算法是用流程图表示的,用goto可以直接转译,而且循环语句发展不完善,仅仅是作为条件分支的一个补充, 如果条件分支箭头向上就是一个循环,而且goto可以实现过去循环所不能实现的结构,形成很巧妙的循环交叉。所以过去一般认为有两种结构,顺序和分支,循 环是分支的特殊情况。但是break和continue语句使这些结构实现成为可能,后来发现goto的使用会造成维护和调试的许多麻烦,所以循环逐渐代 替了goto,使程序更有层次。现在编程不建议用goto了,如果用goto还会被认为编程修养不好……言归正题,把它的goto去掉:

    算法一(2)

    srand((unsigned)time(NULL));

    j=0; 
    while (j<m) 

    a[j] = rand() % n ; 
    for(i=0;i<j;i++){if(a[i]==a[j]) break; } 
    if(i<j)continue;

    j++
    }

     但是后来都建议用for循环,这样可以使循环条件跟紧凑:

    算法一(3)

    srand((unsigned)time(NULL));

    for(j=0;j<m;j++)

    {a[j]=rand()%n;

     for(i=0;i<j;i++)

     {if(a[i]==a[j])

       {i=-1;a[j]=rand()%n;}

     }

    }

    但这是个很笨的方法,且比较次数呈线性增长,越往后次数越多……另一个想法是生成一个就把它从中去掉,就可以实现不重复了:

    算法二(1)

     for (i=0;i<n;i++)

         a[i]=i+1;

    for(i=0;i<m;i++)

    {j=rand()%(n-i);

     b[i]=a[j];

     for (k=j;k<n-i-2;k++)

     a[k]=a[k+1];

     }

    b[]是生成的随机数列

     这样做也太麻烦了,生成的直接做个标记不就行了?

    算法三(1-1)

    i=rand()%m;

    a[i]=1;b[0]=i;

    for(j=1;j<m;j++)

    {for (i=rand()%m;a[i]==1;i=rand()%m);

     b[j]=i;a[i]=1;

    }

    写紧凑一些吧,直接:

    算法三(1-2)

    for(j=0;j<m;j++)

    {for (i=rand()%m;a[i]==1;i=rand()%m);

     b[j]=i;a[i]=1;

    }

    但是我看到了更简洁的:

    int n=某值;

    int a[n]={0};

    for (i=1;i<=n;i++)

    {while(a[x=rand()%n]);

      a[x]=i;

    }

    这个无循环体的while保证a[m]是初始化的0状态。这生成了n个,我们只取m个,这太浪费了,结合一下:

    int n=某值,m=某值;

    int a[n]={0},b[m]

    for (i=1;i<=n;i++)

    {while(a[x=rand()%n]);

      b[i]=x;a[x]=1;

    }

    但是总叫人不舒服,对这种算法谁有更好的写法呢?

    这种算法越到后面,遇到已使用过的元素的可能性越高,重复次数就越多,但是当m较小时还是很好的:)

    这都不太让人满意么?看看下面这个:

    算法四(1)

    for (i=0;i<n;i++)

      a[i]=i+1;

    for (i=n-1;i>0;i--)

    {w=rand()%i;

     t=a[i];

     a[i]=a[w];

     a[w]=t;

    }

    这个算法很不错,有人会怀疑其随机性,但个人认为是没问题的,首先第二行按顺序用0到n填满整个数组;第三行,是随机产生从0到n-2个数组下标,把这个下标的元素值跟n-1下标的元素值交换,一直进行到下标为1的元素。因此它只需要遍历一次就能产生全部的随机数。

    但这样会生成n个小于n的随机数,我们只要m个,再加两句:

    for(i=0;i<m;i++)

     b[i]=a[i];

    b[]是生成的随机数列

     如果m和n很接近的话或者相等是最好,但可能不是……那改一下:

    算法四(2)

    for (i=0;i<n;i++)

      a[i]=i+1;

    for (i=0;i<m;i++)

    {w=rand()%n;

     t=a[i];

     a[i]=a[w];

     a[w]=t;

    }

    但是条件反过来了,如果m远小于n还行,如果接近,其随机性就存在问题了(为什么?),再改一下:

    算法四(3)

    for (i=0;i<n;i++)

      a[i]=i+1;

    for (i=0;i<m;i++)

    {w=rand()%(n-i)+i;

     t=a[i];

     a[i]=a[w];

     a[w]=t;

    }

    这样可以万无一失了……

  • 相关阅读:
    suse12安装详解
    Centos7上部署openstack mitaka配置详解(将疑难点都进行划分)
    菜鸟帮你跳过openstack配置过程中的坑[文末新添加福利]
    openstack中dashboard页面RuntimeError: Unable to create a new session key. It is likely that the cache is unavailable.
    Multiple network matches found for name 'selfservice', use an ID to be more specific.报错
    查看 SELinux状态及关闭SELinux
    SELinux深入理解
    IP地址、子网掩码、网络号、主机号、网络地址、主机地址
    Oracle job procedure 存储过程定时任务
    POI文件导出至EXCEL,并弹出下载框
  • 原文地址:https://www.cnblogs.com/xlzhh/p/4222898.html
Copyright © 2011-2022 走看看