下面是在牛客网看到的一道题;
//假设这n个数的序号依次为0,1,2,...,n-1,数组名为num
void knuth1(int* pNum, int m, int n){srand((unsigned int)time(0));for (int i=0; i<n; i++){if (rand()%(n-i) < m)//rand()%(n-i)的取值范围是[0, n-i){cout << pNum[i] << endl;m--;}}}
这是牛客网上的一道题,目的是从n个数中可放回地随机抽取m个数字。注意数字是可放回的,所以n个数字每一个数字被cout的概率都是m/n。当i取0,rand()%(n-i)的取值在是[0,n-1]范围随机分布,小于m的概率自然是m/n。当i取1,随机数的范围在[0,n-2],共n-1个取值。这时m的值要取决于i=0时有没有输出,所以可以用全概率公式计算。
这里想说的不是这道题本身,而是这个rand()函数。Rand()函数括号内是没有参数的,直接返回[0,RAND_MAX]的随机整数。但需要注意的是rand产生的是伪随机数,用线性同余法实现,依然是一个有限状态转换机,依然有周期(周期很长),所以当我们再一次调用这个函数,得到的结果相同,这在我们调试的时候很方便,但如果需要产生真正的随机数就需要srand来设置随机种子了。void srand (unsigned int seed);直接调用rand时,种子的值默认是1.要得到真正的随机数,每次设置的种子也应该不一样,我们通常使用time(0)作为种子,即把系统时间作为种子,保证了不同时刻得到的种子是不一样的。
在matlab中,rand函数就可以直接得到真正的随机数。为了在不同时刻运行函数时得到相同的随机数,便于调试,我们需要把随机数生成器初始化:RAND('state',0)。但是这一用法在新的matlab版本中不再支持,而推荐使用RNG。
在编译器中可看到RAND_MAX是一个宏定义,为0x7fff,也就是二进制的15位1. 百度百科中有:(C11)标准中未规定 RAND_MAX 的具体数值。但该标准规定了RAND_MAX 的值应至少为32767,最大为2147483647.这就引出了一个问题:int型明明在32位系统和64位系统中都占4字节,为什么这里产生的随机数的最大值只是15位全1的二进制和31位全1的二进制?其实,这就是带符号的short int型和int型的正数的最大值。于是,就有了第二个,也是很基本的一个问题,int型表示的范围是什么,正整数和负整数都是怎么表示的?(惭愧)
我们以一个字节长度为例。先不用管书上所强行灌输的数目符号位,原码,补码,我们从头开始,自己试着解决问题。8bit编码方式有2的8次方共256种,在图像中可以表示[0,255]的灰度级,在图像中像素取值只能是0或者正数,在计算机中,我们当然还需要表示负数,那么负数(先研究负整数)是怎么表示的呢?一个最自然而然的方式是把256种编码方式的一部分表示正数,一部分表示负数,一部分表示0.我们把0000 0001~01111 1111这一部分用来表示正整数,因为这一部分从0开始,是最符合我们数数的习惯的。那么现在的问题就是如何把剩下的表示负数。
首先,我们可以观察到剩下的部分除了0000 0000,最高位都是1,这就可以解释,为什么最高位的1来表示负数。那么1000 0000~1111 1111到底和负数是怎么对应的呢?一个理所当然的思路是1111 1111=-1*(0111 1111)=-127。我们来验证一下,1111 1111+0111 1111=0?明显不等于,但同时也给了我们一个思路,可以利用已有的正整数表达方法和绝对值相等的正负数之和为0的特点求负整数的表达方式。-1的二进制形式等于
0000 0000-0000 0001=1111 1111+0000 0001-0000 0001=1111 1111
于是我们知道,1111 1111对应的是-1.上面的式子还告诉了我们更多:0000 0000可以写做全1的数再加1,进位舍去就是全0.并且我们发现,将0拆分成全1和1的和,这样我们求-A的补码=全1-A+1,全1和二进制的加减都相对于异或,也就是取反,所以我们也终于得到了所谓的求负数补码的方法:按位取反再加1.
于是我们可以得到-2的补码:1111 1111+0000 0001-0000 0010=1111 1110
现在再考虑几个特殊的数,128=1000 0000,-128的补码=1000 0000,可见自然数128=-128的补码形式,由于我们已经规定了最高位是符号位,符号位1表示负数,所以0~255是代表补码时只有-128,没有128.于是我们也得到了所谓的一字节带符号整数取值范围[-128,127].
这样,我们得到规律,原来的0~255的数被分成两部分,[0,127]是递增的正数,和原来的表示方法一样,之后的数代表负数,也是递增。
到这里我们依然没有解释一句话,补码是为了让计算机把减法当做加法来做。其实,我们数轴首尾相接形成一个圆就好理解了。刚才我们也提到了,计算机中的加法超过长度会高位舍去,这其实意味着计算机中的数字是闭环的状态机。无论是加还是减,都是在这个闭环里面移位,只不过是逆时针还是顺时针罢了。我们把时钟的十二点位置看作是0/255,加法看作是顺时针移位(蓝色曲线),减法是逆时针(黄色曲线),这样六点钟附近是加数和减数绝对值最大的位置。为了避免减法(逆时针),我们可以顺时针移动相比于逆时针较大的角度达到相同的效果。逆时针转动30度就相当于顺时针转动330度,而330度就可以用时钟上的刻度来衡量,即0~255就是时钟的刻度。330度就是30度的补角,这也是补码的来历。
在查阅关于rand的使用的过程中,看到了一个例子,产生[0,10]之间的随机数:
#include<stdlib.h>int main(){int i,j;for(i=0; i<10; i++){j=1+(int)(10.0 * rand()/(RAND_MAX+1.0));printf("%d ",j);}}
产生介于 1 到 10 间的随机数值。这里的问题是为什么要加1.0?我的理解是如果分母取RAND_MAX,那么随机数范围就被归一化到[0,1],乘10后范围是[0,10],而我们的目标是先取得[0,9]的随机数再加1才能满足要求。注意到这里的加法和乘法都是float型,最后被强制转换成int型,其实这才是关键。分母取(最大值+1),使得随机数归一化后无法取到1,乘10之后范围是[0,10),最大值在9和10之间。而int强制转换是直接取浮点数的整数部分,这样我们就得到了范围在[0,9]的随机数。
P.s 浮点数到整型的转换,除了直接取整数部分,还有ceil函数和floor函数。Floor函数是取小于等于浮点数的整数,这一点与直接取整数部分在浮点数是负数时结果有区别。
最后,关于归一化方法和使用线性同余法得到想要的范围内的随机数的区别,有人说是前者是在多次随机出来的结果,前者理论上会更平均,而后者仅仅是和10求余得到的结果,没前面的结果来得平均。关于这个说法还不是很懂,有空可以再研究一下线性同余法。
Reference:
- rand函数https://blog.csdn.net/cmm0401/article/details/54599083
- 醍醐灌顶https://blog.csdn.net/wenxinwukui234/article/details/42119265
- 类型转换https://zhidao.baidu.com/question/1964506016596059780.html
- 牛客网:https://www.nowcoder.com/questionTerminal/12796031452e4ced8a16255bb02c4168
- 最后https://zhidao.baidu.com/question/561525713.html?qbl=relate_question_0&word=rand%20%BC%D31