一、随机种子
参考随机数与随机种子
随机这一命令是如何实现的?
rand()可以生成一个0~RAND_MAX之间的一个随机数,返回值是一个unsigned int类型值。如下代码:
#include <iostream>
#include <stdlib.h>
#include <time.h>
using namespace std;
void main()
{
for (int i = 0; i < 10; ++i)
{
cout<<rand()<<" ";
}
cout<<endl;
}
运行两次,发现一个问题,两次循环调用rand()所产生的随机数序列是一样的。是的,这就是伪随机数了,就好像是在系统中已经有了一个0~RAND_MAX的一个乱序序列,我们调用rand()的时候都是参照这个序列和随机种子的,这里没有设置随机种子,因此随机种子为1
随机种子希望可以是不可预测的,我们可以取为当前时间,用time()函数来获取当前时间作为随机种子,然后与序列与当前时间进行计算得出随机数,则每次调用rand()的时候随机种子就是变化的,因此,我们产生的随机数就是不可预测的了。srand生成一个随机种子。代码如下:
#include <iostream>
#include <stdlib.h>
#include <time.h>
using namespace std;
void main()
{
srand((int)time(0));
for (int i = 0; i < 10; ++i)
{
cout<<rand()<<" ";
}
cout<<endl;
}
在Java中,也有类似的方法。参考不要随便设置随机种子
Random类的默认种子(无参构造)是System.nanoTime()的返回值(JDK 1.5版本以前默认种子是System. currentTimeMillis()的返回值),注意这个值是距离某一个固定时间点的纳秒数,不同的操作系统和硬件有不同的固定时间点,也就是说不同的操作系统其纳秒值是不同的,而同一个操作系统纳秒值也会不同,随机数自然也就不同了。
new Random(1000)显式地设置了随机种子为1000,运行多次,虽然实例不同,但都会获得相同的三个随机数。所以,除非必要,否则不要设置随机种子。
二、js中的随机
我们平时在开发过程中,经常会遇到随机数问题,例如,随机抽奖,微信飞机大战中,随机产生敌人位置等等。但实际上这些都是伪随机,用C语言开发的时候,使用random函数的时候,会发现,当我们直接调用这个方法的时候,每次运行都产生相同的随机数,所以,在调用这个方法的时候,都会用时间来做随机种子。当然,在js中直接调用Math.random方法,就能直接产生随机数,这里不需要设置时间种子,JS底层已经为我们设置了随机种子,而且,每次是不一样的。既然是伪随机,那么,我们可以根据这个特性,每次设置相同的随机数,每次运行的时候,都产生相同的随机数。
这时有人会问,为什么要每次产生相同的随机数呢?其实,这里有很大的用处,例如,我们在玩一个游戏的时候,随机产生了几个敌人的位置,这个时候,如果网络不好,重新连接进来,会发现,敌人的位置变了,此时,玩家看起来,就会发小屏幕上敌人位置发生了跳动,从玩家体验的角度来说,这是一种很差的体验。有人会说,为什么不在恢复现场的时候,直接将敌人的位置全部发送回来呢?下面,我来举一个简单的例子,来说明这个问题:
如果我们将敌人定义为1~10种类型,那么,正常情况下,如果我们为了在重新进入游戏的时候,继续上面的进度进行游戏,我们会将敌人类型和敌人位置都传回来,此时,我们传回来的数据格式,我用JSON简单来表示,大概应该是这种:
<span style="font-size: 18px;">{
'enemyList':[{
'enemyType':1,
'x':33,
'y':25
},{
'enemyType':2,
'x':45,
'y':67
},{
'enemyType':3,
'x':84,
'y':12
}]
}</span>
那么,如果我们需要在服务器记录所有敌人的位置,如果用户量比较大,其实这数据量就挺大的了,而且,在重新进入游戏的时候,其实传回来的数据也是不少的。那下面,我们换一种思路,这次,我们每次计算玩家的位置都用随机数来计算,然后,当我们重新进入游戏的时候,服务器将相同的随机数种子发送到客户端,让服务器自己计算位置,同时将敌人的类型发回来,大概的数据格式如下:
<span style="font-size: 18px;">{
'seed': 12345,
'enemyList':[1,2,3]
}</span>
有没有觉得很精简,这样,不仅服务器储存的数据量大大减少,而且,服务器给客户端发包的时候,数据量也较少了好多,差不多是之前的1/3,这在开发中,真的好处很多的,玩家网络不好,中间网络断了,重新连接,可以恢复玩家当前的场景,网络不好,重连的时候,数据量减少了好多,如果用第一种办法,那重连了数据量反倒挺大的,这并不符合开发的要求啊,所以,在这里,随机数就真的起了大的作用。
Math.seed = 5;
Math.seededRandom = function(max, min) {
max = max || 1;
min = min || 0;
Math.seed = (Math.seed * 9301 + 49297) % 233280;
var rnd = Math.seed / 233280.0;
return min + rnd * (max - min);
};
for (var i= 0; i<10; i++) {
console.log(Math.seededRandom());
}
运行如上代码你会发现如果种子Math.seed不变,那么生成的随机数是不会变化的.
(Math.seed * 9301 + 49297) % 233280,为什么会是这三个值,而不是其它的到底这三个数字有什么神秘的来历呢?
参考网上常能见到的一段 JS 随机数生成算法如下,为什么用 9301, 49297, 233280 这三个数字做基数
rand = (function(){
var today = new Date();
var seed = today.getTime();
function rnd(){
seed = ( seed * 9301 + 49297 ) % 233280;
return seed / ( 233280.0 );
};
return function rand(number){
// return Math.ceil(rnd(seed) * number);
return Math.ceil(rnd() * number);
};
})();
myNum = (rand(5));
像Math.seededRandom这种伪随机数生成器叫做线性同余生成器(LCG, Linear Congruential Generator),几乎所有的运行库提供的rand都是采用的LCG,形如:I n+1=aI n+c(mod m)
生成的伪随机数序列最大周期m,范围在0到m-1之间。要达到这个最大周期,必须满足:
1.c与m互质
2.a - 1可以被m的所有质因数整除
3.如果m是4的倍数,a - 1也必须是4的倍数
以上三条被称为Hull-Dobell定理。作为一个伪随机数生成器,周期不够大是不好意思混的,所以这是要求之一。因此才有了:a=9301, c = 49297, m = 233280这组参数,以上三条全部满足。
三、梅森旋转
上面讲了线性同余,还有一个叫梅森旋转。
参考【随机数生成算法系列】线性同余法和梅森旋转法
常用的快速随机数产生算法是线性同余算法和梅森旋转算法。平均分布的伪随机算法都是周期性的,在一个周期内各个数字的分布概率相等。Windows和Linux的C运行库都采用线性同余算法,Window C运行库的随机序列周期是65536,Linux的C运行库是2^31,在一个周期内一个数字只出现一次。而设置随机数种子就相当于设置当前的a[n],下一个随机数从这里开始计算产生。
但是线性同余的周期太短了,后来出现了梅森旋转算法。梅森旋转算法各项指标非常优秀,现在一般使用的是产生器有2^19937-1长的周期,而且在1-623维均匀分布的(映射到一维直线均匀分布,二维平面内均匀分布,三维空间内均匀分布...)。
不过上面这两种算法还都不适合用于加密,因为攻击者如果收集了足够长的一段序列,就可以从数学上猜解出整个序列。可以用于加密的随机数生成算法有RSA随机数生成算法等。而产生高斯分布的随机数产生算法,据我所知有box-mueller算法。
梅森旋转算法(Mersenne twister)是一个伪随机数发生算法。由松本真和西村拓士在1997年开发,基于有限二进制字段上的矩阵线性递归F。可以快速产生高质量的伪随机数, 修正了古典随机数发生算法的很多缺陷。
梅森旋转算法是R,Python,Ruby,IDL,Free Pascal,PHP,Maple,Matlab,GMP和GSL的默认伪随机数产生器。从C++11开始,C++也可以使用这种算法。在Boost C++,Glib和NAG数值库中,作为插件提供。 在SPSS中,梅森选旋转算法是两个PRNG中的一个:另一个是产生器仅仅为保证旧程序的兼容性,梅森旋转被描述为”更加可靠“。梅森旋转在SAS中同样是PRNG中的一个,另一个产生器是旧时的且已经被弃用。
最为广泛使用Mersenne Twister的一种变体是MT19937,可以产生32位整数序列。具有以下的优点:
- 有219937 − 1的非常长的周期。在许多软件包中的短周期—232 随机数产生器在一个长周期中不能保证生成随机数的质量。
- 在1 ≤ k ≤ 623的维度之间都可以均等分布(参见定义).
- 除了在统计学意义上的不正确的随机数生成器以外, 在所有伪随机数生成器法中是最快的(当时)
古老的LCG(linear congruential generator)代表了最好的伪随机数产生器算法。主要原因是容易理解,容易实现,而且速度快。LCG不能用于随机数要求高的场合,例如不能用于Monte Carlo模拟,不能用于加密应用。LCG有一些严重的缺陷,例如如果LCG用做N维空间的点坐标,这些点最多位于m1/n超平面(Marsaglia定理),这是由于产生的相继X(n)值的关联所致。
另外一个问题就是如果m设置为2的指数,产生的低位序列周期远远小于整体。一般而言,输出序列的基数b中最低n位,bk = m (k是某个整数),最大周期bn.有些场合LCG有很好的应用,例如内存很紧张的嵌入式中,电子游戏控制台用的小整数,使用高位可以胜任。
如果需要高质量的伪随机数,内存充足(约2kb),Mersenne twister算法是个不错的选择。Mersenne twister产生随机数的质量几乎超过任何LCG。不过一般Mersenne twister的实现使用LCG产生种子。常见的有两个变种Mersenne Twister MT19937和Mersenne Twister MT19937-64。Mersenne Twister有很多长处,例如:周期2^19937 - 1对于一般的应用来说,足够大了,序列关联比较小,能通过很多随机性测试。
五、伪随机数和真随机数
参考看上去随机
伪随机数会从一个随机的种子(比如系统时间)开始,然后用一套固定的公式进行迭代产生一系列数字。这样生成的数列通常具有周期性,但是周期特别大。比如在C++和Python等多种软件中默认使用的随机数算法叫梅森旋转算法,其周期高达219937-1(这是一个梅森素数,即形如2n-1这样的素数),远大于通常需要使用的随机数的个数。这类算法所生成的并不是通常意义上的随机数:因为如果你拿到了随机数种子,就可以完全复现接下来的所有随机数——这在具体问题上可能是好事,也可能是坏事。
而真随机数则正好相反。它会用一系列确实无法预测的事件作为输入。比如点击鼠标的间隔,鼠标划过的长度,敲击键盘的频率,网络事件的突发事件等等。一些系统驱动通常会在对这些数据进行处理之后将它们存进一个被称为熵池的存储区。需要读取随机数的时候就从熵池里面取数据。这样得到的确实是无法预测的随机数,但是随机数的数量是有限的,不能超过随机事件的频率。熵池内的数据被用光了而又没有新的随机数存进去的话,那么暂时就没法调取真随机数了。Linux系统所使用的熵事件所产生的随机数频率在每秒2-5比特左右。除此以外,还有一些专门的硬件真随机数生成设备,使用类似热力学噪声、光电效应、量子效应等在理论上不可预测的物理过程来制造真随机数,例如Whitewood公司提供的量子随机数生成器,它生成随机数的速度可以达到每秒100M。
实际上,不管是真随机数还是伪随机数都会保证算法生成出的数列满足统计上的随机性,因此在使用时的效果上没什么本质差别。当然,也有一种说法认为除了量子效应以外其他的“随机”事件并不是真的随机,因此即使是“真随机数”,也只是一种足够好的近似而已。
六、golang 随机数
1.基本用法
参考Go生成随机数
Go的math/rand包提供了生成随机数的API,重要的API如下:
// 该函数设置随机种子
// 若不调用此函数设置随机种子,则默认的种子值为1,由于随机算法是固定的,
// 如果每次都以1作为随机种子开始产生随机数,则结果都是一样的,因此一般
// 都需要调用此函数来设置随机种子,通常的做法是以当前时间作为随机种子
// 以保证每次随机种子都不同,从而产生的随机数也不通
// 该函数协程安全
func Seed(seed int64)
// 以下函数用来生成相应数据类型的随机数,带n的版本则生成[0,n)的随机数。
// 注意生成的随机数都是非负数
func Float32() float32
func Float64() float64
func Int() int
func Int31() int32 // 注意该函数只返回int32表示范围内的非负数,位数为31,因此该函数叫做Int31
func Int31n(n int32) int32
func Int63() int64
func Int63n(n int64) int64
func Intn(n int) int
func Uint32() uint32
func Uint64() uint64
// 另外,rand包还提供了一个类,接口和上面的大致相同:
type Rand struct {
// ...
}
// 创建一个以seed为种子的源,注意该源不是协程安全的
func NewSource(seed int64) Source
// 以src为源创建随机对象
func New(src Source) *Rand
// 设置或重置种子,注意该函数不是协程安全的
func (r *Rand) Seed(seed int64)
// 下面的函数和全局版本的函数功能一样
func (r *Rand) Float32() float32
func (r *Rand) Float64() float64
func (r *Rand) Int() int
func (r *Rand) Int31() int32
func (r *Rand) Int31n(n int32) int32
func (r *Rand) Int63() int64
func (r *Rand) Int63n(n int64) int64
func (r *Rand) Intn(n int) int
func (r *Rand) Uint32() uint32
func (r *Rand) Uint64() uint64
生成随机数时,以当前时间作为随机种子是个很好的选择,可以用time包生成当前时间:
// 返回当前时间
func Now() Time
// 为了将Time类型转换为int64类型以作为随机种子
// 可以使用如下两个函数:
// 返回从1970年1月1日到t的秒数
func (t Time) Unix() int64
// 返回从1970年1月1日到t的纳秒数
func (t Time) UnixNano() int64
例子:
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
//
// 全局函数
//
rand.Seed(time.Now().Unix())
fmt.Println(rand.Int()) // int随机值,返回值为int
fmt.Println(rand.Intn(100)) // [0,100)的随机值,返回值为int
fmt.Println(rand.Int31()) // 31位int随机值,返回值为int32
fmt.Println(rand.Int31n(100)) // [0,100)的随机值,返回值为int32
fmt.Println(rand.Float32()) // 32位float随机值,返回值为float32
fmt.Println(rand.Float64()) // 64位float随机值,返回值为float64
// 如果要产生负数到正数的随机值,只需要将生成的随机数减去相应数值即可
fmt.Println(rand.Intn(100) - 50) // [-50, 50)的随机值
//
// Rand对象
//
r := rand.New(rand.NewSource(time.Now().Unix()))
fmt.Println(r.Int()) // int随机值,返回值为int
fmt.Println(r.Intn(100)) // [0,100)的随机值,返回值为int
fmt.Println(r.Int31()) // 31位int随机值,返回值为int32
fmt.Println(r.Int31n(100)) // [0,100)的随机值,返回值为int32
fmt.Println(r.Float32()) // 32位float随机值,返回值为float32
fmt.Println(r.Float64()) // 64位float随机值,返回值为float64
// 如果要产生负数到正数的随机值,只需要将生成的随机数减去相应数值即可
fmt.Println(r.Intn(100) - 50) // [-50, 50)的随机值
}
2.并发时,种子递增上锁
参考golang 生成随机数,时间种子改进型
加一个计数,这是使用时间作为种子的简单改进型,在同一线程多次调用及并发的时候都表现良好。
var (
randSeek = int64(1)
l sync.Mutex
)
//获取指定长度的随机字符串
//@params num int 生成的随机字符串的长度
//@params str string 可选,指定随机的字符串
func GetRandomSring(num int, str ...string) string {
s := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
if len(str) > 0 {
s = str[0]
}
l := len(s)
r := rand.New(rand.NewSource(getRandSeek()))
var buf bytes.Buffer
for i := 0; i < num; i++ {
x := r.Intn(l)
buf.WriteString(s[x : x+1])
}
return buf.String()
}
func getRandSeek() int64 {
l.Lock()
if randSeek >= 100000000 {
randSeek = 1
}
randSeek++
l.Unlock()
return time.Now().UnixNano() + randSeek
}