C++大随机整数的产生,排序和查找

来源:互联网 发布:plsql中导入excel数据 编辑:程序博客网 时间:2024/05/16 11:04

这里想记录一个之前遇到的一个很有意思的与大随机数相关的问题,也是自己长时间琢磨出来的结果,这里想分享一下;

先在这里列出结果方便读者看是否相关,以免浪费你的时间,这里想通过一个小例子得出如下的结果:

1) 通常的离散型均匀分布的产生;

2) 大随机数的离散型均匀分布的产生;

3) 大量数据,在范围已知或可估计的条件下的,简单快速排序算法:O(range)的空间O(n)的时间

4) 数据分布可估计时的基于统计的高成功率的快速查找,进一步的节省空间和时间,且错误概率可控,直至为零

如果想来看看,你爱我们就开始了:

首先给出问题(很短,其实就一句话): 

        生成N=10^12个范围在0~(10^8)-1之间的整数,并找出其中第N/2大和第N/4大的数。

        咋看之下这个问题似乎很简单,只是数据量很大而已。可是仔细一想似乎数据大的有点多了,因此其存储倒是问题(因为我们后面还需要寻找N/2大和第N/4大的数),还有一个问题就是,C++中的rand()生成的随机数是不会超过RAND_MAX(0x7fff)的,而RAND_MAX是小于(10^8)的,所以随机数的生成也是一个问题,还有就是,即便我们可以生成10^8的随机数,但是如果10^8进一步变大的话(如:变为10^9)还是会面临随机数生成的问题,所以有必要找到一种利用已有随机数生成函数生成大随机数的方法。

        首先这里先来解决第一个问题,大随机数生成:

        其实,看到这个问题可能很多人已经想到解决方案了,或者是google之,或者是baidu之,都可以得到几种解决方案,

        例如:

        示例1(注意这样是有问题,下面会分析):

                      srand(time(NULL));

                       int  r1=rand()%10000;

                       int r2=rand()%10000;

                       int re=r1*r2;

        示例2(这里也有点问题,下面会解决):

                        srand(time(NULL));

                       int  r1=rand()%10000;

                       int r2=rand()%10000;

                       int re=r1*10000+r2;

      如果你对为随机数的生成方法比较熟悉的话那么你也可以自己写一个随机数生产函数,如 http://blog.csdn.net/suiyunonghen/article/details/3863260 所做的那样,但是我们可能更愿意使用C++标准提供的随机数生产函数,而且博文中关于随机数种子的部分处理得并不理想,因而这种方法存在一些问题,文中自己也有提到。

      而对于第一种方法,他是最不推荐使用的方法,理由如下:

     首先,我们需啊明白的的是,这里所说的随机数是指均匀分布,也就是:P(re=n)=1/N这里N=10^8,re就是产生的随机数,而示例1产生的随机数根本就不是均匀分布(这里我们假设rand()%10000是服从均匀分布的,(其实这会有一点问题,但是后面会细说并改正它,这里先使用后面的结果):

     证明: 

              其实要证明他不是均匀分布只需要举出一个反例就好了,以上面的情况为例:示例1中的算法产生数字6的情况为:(r1, r2)=(1,6)或(2,3)或(3,2)或(6,1),每一个的概率为1/N*1/N*6=1/10000*1/0000*6=6/(10^8)但是对于数字10001,示例1中的代码是根本不可能产生的,而其进一步的我们可以看到的是这种现象是普遍存在的,而并是特例,这是上面的代码的固有问题。

       对于示例2,类似于前面的步骤,这里先计算取到0~(10^8)-1之间任意一个数的概率,根据示例2中的代码这里利用如下的步骤计算概率:对于任意一个数re属于0~(10^8)-1,做如下的分解:

                                                           r2=re%10000;

                                                           r1=(int)re/10000;

                                                          则:re =r1*10000+r2;

在独立的情况下(这里不考虑非独立的情况)用rand()%10000产生(r1,r2)的概率为:1/N*1/N=1/10000*1/0000=1/(10^8),这正是均匀分布的密度,也就是:

                                                           P(re)=1/N*1/N=1/10000*1/0000=1/(10^8),

到这里似乎随机数产生的问题似乎已经那个解决了,但是正如我在前面的括号中注明的一样,这还是有问题,但是是可以修正的:问题主要出在我们使用rand()%N产生0~N-1之间的随机数时,N(这里是10000)并不一定能够整除RAND_MAX(0x7fff),这就导致了产生0~(RAND_AMX%N)-1之间的概率要比RAND_MAX~N-1之间的概率要大,这种情况在N较大时影响会很大,特别的当N=(3/4)×RAND_MAX时产生(0~RAND_MAX/4-1)的可能行会是产生其他数据的2倍,虽然对这这里的10000而言没有达到这么大的差异但是为了完整性(我有点轻微的强迫症,望见量)还是有必要进行改进的:

其实改进的方法很简单(这里为了简洁和通用,使用N代替10000):

int myrand(int N)   //产生0~N-1之间的均匀分布的随机数N<=RAND_MAX

{

        srand(time(NULL));

        int BOUND=RAND_MAX-(RAND_MAX%N); // 这样BOUND就是N的整数倍了

        int re=rand();

       while(re>BOUND) // 如果超过BOUND就重新产生

                re=rand();

      return re%N;

}

证明(其实用一点条件概率的知识就可证明了):

        假设rs是任意的一个0~N-1之间的数,那么myrand产生rs的概率为P(rs)=P(re%N=rs|re<BOUND)也就是:在re<BOUND的条件下re%N=rs的概率,利用条件概率公式展开可以得到:

                          P(rs)=(rs=re%N, re<BOUND)/P(re<BOUND)  ; // 条件密度的定义,条件密度等于联合密度除以边际密度

 其中可以将分母表述为:re<BOUND且re%N=rs,而在0~RAND_MAX-1中满足这些条件的数有(BOUND÷N)个,因此产生这样的re的概率为(BOUND÷N)÷RAND_MAX

对于分母可以很直观的的到其概率为:(BOUND÷RAND_MAX)将结果带入就可以得到的是:

                         P(rs)=1/N; //这正是均匀分布的密度函数,至此,已经得证;

于是我们将示例2的代码改为:

                       int  r1=myrand(10000);

                       int r2=myrand(10000);

                       int re=r1*10000+r2;

到此,随机数产生的问题已经基本解决,但是,里还想花点篇幅对算法进行一下拓展,使得它更通用:

         对于上面的算法产生10^8以内的随机数是符合要求的,需要两个10000的数来构造,但是随之而来的一个问题是如果我想产生0~98765432-1之间的数的话应该怎么构造呢?此时,选10000之间的两个数显然是不可以的,因为此时的随机数生成概率是1/10000*1/10000=1/(10^8)而不是1/98765432;那么用rand()%9877和rand()%5432来构造可以吗?显然这也是不行,原因在于1/9877*1/5432不等于1/98765432,那么应该怎么办呢?

       其实要解决这个问题也是很容易的,毕竟我们已经能够生成10^8>98765432的随机数了,这不就便成通常的随机数产生问题了吗!如果再考虑一下不能整除的问题的话,那么这里写出如下的生成指定范围内服从均匀分布的大整数的算法:

这里部分参考了:http://hi.baidu.com/silyt/item/6f9c19d352a4644dfb576891 的想法

// 一个中间函数

long randTmp(int mtg) //产生0~(10^mtg)-1之间的数,这里它的作用就相当于初始的rand()在myrand()中的作用

{

      while(mtg>=4) // 这里选取4,也可以选别的数

      {

             re=re*10000;

              re+=myrand(10000);

              mtg-=4;

       }

      re=myrand(pow(10,mtg)); // 最后一项mtg可能等于0,1,2,3,计算可知此时re是服从够1/pow(10,mtg)的均匀分布的

     return re;

}

long randLong(long N)

{

       int mtg=ceil(log10(N*1.0)); //向上取整

       long suN=pow(10,mtg);   // 盖住N,相当于myrand()中的RAND_MAX

       long BOUND=suN-(suN%N);

       long re=randTmp(mtg);       

      //-------------------------类似myrand()中的处理:

      while(re>BOUND)

                  re=randTmp();

     return re%N;

}

有点花时间,可以把#LENGTH 100000000改小进行测试,还有就是改用long long类型的话会更具有扩展性,今天在gcc4.8.2上测试时发现他是支持long long(8 byte)的。

#pragma    once#ifndef    RANDNUM_H#define    RANDNUM_H#ifndef    INCLUDE_IOSTREAM#define    INCLUDE_IOSTREAM#include   <iostream>#endif#ifndef    INCLUDE_CTIME#define    INCLUDE_CTIME#include   <ctime>#endif#ifndef    INCLUDE_CMATH#define    INCLUDE_CMATH#include   <cmath>#endif#ifndef    INCLUDE_CSTDLIB#define    CSTDLIB#include   <cstdlib>#endifclassRandNum{public:RandNum(time_t* init=NULL);int Rand(int N); // 基本的随机数生成函数0~N-1~RandNum();long randLong(long upBound); //大的随机数生产函数,均匀0~upBound-1,当使用long long 类型时可生成范围会更大(只要该一下类型就好)private:long randTemp(int exp);//随机数生成函数0~10^exp-1,这是作为private 成员,是生成其他大随机数的基础};#endif    /*RANDNUM_H*///****************************************************************#include   "RandNum.h"#ifndef    INCLUDE_ALGORITHM#define    INCLUDE_ALGORITHM#include   <algorithm>#endif#define    LENGTH 100000000RandNum::RandNum(time_t* init /* =NULL */){ srand(time(init)); // set the seed;}RandNum::~RandNum(){};int RandNum::Rand(int N){// the basic random number generate funcionint BOUND=RAND_MAX-(RAND_MAX%N); // BOUND = k*N 是N的整数倍int re=rand();while(re>BOUND) // 如果大于BOUND那么重新生成随机数re=rand();    return re%N;}long RandNum::randTemp(int exp){long re=0;while(exp>=4) //这里的4也可以换为其他的数字,但不要使得10^exp超过RAND_MAX就好,太小的话可能会有点慢
    {re*=pow(10,4);re+=Rand(10000);    exp-=4;    }    re=re*(pow(10,exp))+Rand(pow(10,exp)); // 最后一个可能是0,1,2,3而不一定是4,所以需要特殊处理使得服从密度为1/pow(10,exp)的均匀分布;    return re;}long RandNum::randLong(long upBound){int mtg=ceil(log10(upBound*1.0)); long suN=pow(10,mtg);   // 简单地计算其上界    long BOUND=suN-(suN%upBound);    long re=randTemp(mtg);           //-------------------------类似于myrand()中的处理    while(re>BOUND)         re=randTemp(mtg);    return re%upBound;}int main(int argc, char *argv[]){RandNum rng;long* randnum=new long[LENGTH];// 数据太多使用堆来存储std::cout<<"befor sorted"<<std::endl;for(long count =0;count<LENGTH;count++){randnum[count]=rng.randLong(LENGTH);std::cout<<randnum[count]<<"\n";}std::cout<<"after sorted!"<<std::endl;std::sort(randnum,randnum+LENGTH);for(long i=0;i<LENGTH;i++)  std::cout<<randnum[i]<<"\n";std::cout<<std::endl;std::cout<<sizeof(long)<<std::endl;delete randnum;return 0;}

注意这里也可以不用pow(10,mtg)来盖住N,例如要产生0~23456789之间的数可以用rand()%2346和rand()%1000来构造0~2345999之间的均匀分布随机数来盖住2345678,再用上面的方法产生对应期间的随机数,总之就是要构造大于它的均匀分布,且方便编程会更好。

到此算是比较圆满的解决了大随机数生成的问题,接下来据说处理大数据的问题了。

       题目要求生成10^12的数据,显然要想在个人电脑上存储这样大的数据是不现实的,而且就算退一万步讲的话,存储了这样大的数据,而它的访问仍然会是一个巨大的问题,同时可以得到的是对数据进行排序再进行搜索的策略也是不可取的,所以这里需要另寻出路:

       这里需要注意到的是:生成的是10^8范围内的数,而生成的个数为10^12 这就意味着每个数会有大量的重复,而且要查找的N/2和N/4大的数也只是10^8中的某个数,于是一个想法由此产生:

      使用一个大小为10^8的数组,记为count[ ],初始是将数组初始化为0,每产生一个随机数就将相应的位置+1,也就是re=randLong(10^8) ; count[re]+=1;当所有的随机数生成完毕的时候,我们就完成了数据的统计,同时还要注意的到的是这也同时完成了数据的排序,也就是说现在查找只需要再这个数组上进行就好了:

      初始化:一个数据  double pos=0;循环遍历数组count[ ]寻找N/2和N/4大的数:

                                   double pos=0;// 注意pos需要使用double类型,因为10^12已经超出了 unsigned int 的范围,而我的机器上long 也是32 位的所以这里还是超过了,但是有的编译器上可以使用 long long 类型(64位)但是有的编译器不支持,所以这里还是使用double类型

                                   for(long cnt=0; cnt<10^8;cnt++)

                                   {

                                                if(pos>=3*N/4)  //注意这里N/4大的数:通常理解的是: N/4大的数<N/2大的数,但是实际上的是:N/4大的数>N/2大的数,就像我们说的 “第1大的数”>“第2大的数”一样

                                                        printf(" 第%lf大的数为%d:" , N/4,cnt); // 肯恩个需要设置N/4的显示方式,使结果更好看,这里就不细说了!

                                                else if(pos>=N/2)

                                               {

                                                       printf(" 第%lf大的数为%d:" , N/2,cnt);

                                                        break;

                                                }

                                     }

至此大数据的查找问题因而基本解决了!但是现在你可能会问还有没有更好的方法,那么下面就给出一个统计上方法,该方法可以进一步的缩减所需空间和查找时间,而且可以保证很高的正确(算法失败的可能行可以任意小,直至退化为上面确定性算法):

        我们注意到,前面已经说明的是这里的随机数是服从均匀分布的,也就是说10^12个数(样本量很大)是“均匀”落在0~10^8-1的期间上的,那么我们可以以很高的概率相信这些数据中的第N/2和N/4 大的数就是10^8/2和3*10^8/4附近的数,而其他的数我们只需要知道有多少个在各个区间之间,而不需要知道它具体是什么,拥编程语言描述为:

                          const long span=10000; // 这里取10000表示“附近”这个概念,它可以是别的你认为合理的数,但是最好不要是使得下面各个区间重叠;

                          long scale1=N/2-span; //第一个区间的上界,因为两个“附近”将0~N-1分为了五个区间

                          long sclae2=N/2+span; //第二个区间的上界,N/2大的数极有可能在这个区间

                          long scale3=3*N/4-span; //第三个区间的上界

                          long scale4=3*N/4+span ; //第四个区间的上界

                           //第五个区间的上界就是N这里就不用记了

                         double count1=0; // 第一个区间的计数

                         double count2[2*span]; // 第二个区间,需要详细记录,因为他是最有可能包N/2大的数的 

                         /* 初始化count2的所有数为0 */

                         double count3=0;

                         double count4[2*span]; //第四个区间,最有可能包含第N/4大的数

                          /*初始化count4为0*/

                         double count5=0;

                       /*然后就是产生随机数或者读取已经生成的数据,根据数据用几个if-else语句将统计各个数据到相应的count*中,最后通过上面类似的算法查找所需的数据,这里我就不写代码了,一是因为这很简单,二是因为文章的篇幅有点长了,和最开始的计划有点出入,后面还有一点东西需要整理*/

        至此大数据查找问题也基本解决了,这里说几点需要注意的的事情:

        1>> 10^12 已经超过了 int 的范围所以产生随机数的循环需要注意,这里有两种解决方法:

                       1)for(int i=0;int i<10000;i++)  // 利用一个10000*10^8的双重循环产生10^12的执行次数

                                    for(int j=0; j<1e8;j++) 

                       2)for(double cnt=0;cnt<1e12;cnt++) // 10^12 double还是可以表示的(如果没有记错的话double有15位的精度)

       这里更推荐第一中方法,其扩展性更好,虽然可能要多写一两行代码

     2>> 程序中的数组都使用了double 类型。这是防止10^12落在极少的几个地方使得这几个地方的计数非常的大(这里不用long是因为我的机器上long还是32位的跟unsigned              int 一样,long long 有的编译器不支持,例如我的~~)

     3>> 上面的排序算法可以推广:时间复杂度为:O(range)// range=max(arry)-min(arry)也就是数组的取值范围,这对与有些问题非常好用,如本问题,以及其他的已知范围且数据紧密的数组。例如 qq号(我不知道腾讯是怎样管理qq的,但是腾讯的qq用户因该不超过10亿也就是10^9只比这里大了一个数量级,而且注意到qq号是不重复的也就是count[]数组中大多为1,那么反过来而我们只需要维护这个范围中count为零的(或者说未被申请的,或者是申请后又废止的(我也不知道是不是存在这种情况,只是表示类似的情况)),这样就可以大大缩减count的大小,其实10^9 对于服务器而言按理说都是渣渣,更何况还是随机存储,这根本就不是个事!,当然一个qq账户不会只是一个数那么简单,这可能只是作为查找索引,好吧!又扯远了。

       同样地,上面的统计方法也可以推广,如果我们知道数据的分布的化!

这样最开始的问题就基本解决了,最后的结果为了方便都放在了开头处!

最后不得不提的还有一件事是:产生10^12个数字是非常耗时间的,而且在pc上存储是不可能的(10^12*sizeof(long)),所以还是只能每次都重新生成!


0 0