在Javascript中植入随机数生成器


372

是否可以在Javascript中植入随机数生成器(Math.random)?


尚不清楚是要播种它,以便在不同的测试运行中重复获得相同的结果,还是要为每个用户使用“唯一的东西”为它播种,以提高使用之间的随机性,目前尚不清楚。
simbo1905

2
不,很遗憾,这是不可能的。jsrand是我在需要播种的PRNG时编写的一个小库。您还可以找到其他更复杂的库。
Domenico De Felice

4
再加上一个问题:提供PRNG而没有种子的方式可能是个好主意吗?有什么充分的理由吗?
艾伦(Alan)

Answers:



159

注意:尽管(或由于其简洁)和明显的优雅,但就随机性而言,此算法绝不是一种高质量的算法。寻找例如该答案中列出的那些以获得更好的结果。

(最初从评论中提出的一个聪明的想法改编为另一个答案。)

var seed = 1;
function random() {
    var x = Math.sin(seed++) * 10000;
    return x - Math.floor(x);
}

您可以将其设置seed为任何数字,只需避免为零(或Math.PI的任何倍数)即可。

该解决方案的优雅,在我看来,来自于没有任何“神奇”的数字(除了10000,这代表你必须扔掉,以避免单模式数字的最小量-看到值结果101001000)。简洁也不错。

它比Math.random()慢2到3倍,但我相信它的速度与其他用JavaScript编写的解决方案一样快。


20
有没有办法证明该RNG生成均匀分布的数字?从实验上看,
Nathan Breit

6
6,000,000 ops / second的速度相当快,我不打算为每次点击带来超过3,000,000的收入。开玩笑,这真是太好了。
AMK 2014年

59
-1,这根本不是一个统一的采样器-偏向0和1(请参阅jsfiddle.net/bhrLT/17,这可能需要一段时间才能计算出来)。连续值是相关的-每355个值,甚至每710个值都相关。请使用更仔细考虑的东西!
斯潘塞·内尔森

37
问题不在于创建加密安全的随机数生成器,而是在于可在javascript中工作,对快速演示有用的东西等。我将简单快速地介绍一下,以便为此目的对一百万个随机数进行良好分配。
2014年

15
小心。Math.sin()可以在客户端和服务器上给出不同的结果。我使用Meteor(在客户端和服务器上使用javascript)。
Obiwahn 2015年

145

我已经在纯JavaScript中实现了许多良好,简短和快速的伪随机数生成器(PRNG)函数。所有这些都可以播种并提供高质量的编号。

首先,请注意正确初始化PRNG。下面的大多数生成器没有内置的种子生成过程(为简单起见),但是接受一个或多个32位值作为PRNG 的初始状态。相似的种子(例如1和2的简单种子)可以在较弱的PRNG中引起相关性,导致输出具有相似的属性(例如,随机生成的水平相似)。为避免这种情况,最佳做法是使用分布良好的种子初始化PRNG。

幸运的是,哈希函数非常擅长从短字符串为PRNG生成种子。一个好的哈希函数即使两个字符串相似,也会产生非常不同的结果。这是一个基于MurmurHash3的混合函数的示例:

function xmur3(str) {
    for(var i = 0, h = 1779033703 ^ str.length; i < str.length; i++)
        h = Math.imul(h ^ str.charCodeAt(i), 3432918353),
        h = h << 13 | h >>> 19;
    return function() {
        h = Math.imul(h ^ h >>> 16, 2246822507);
        h = Math.imul(h ^ h >>> 13, 3266489909);
        return (h ^= h >>> 16) >>> 0;
    }
}

每个后续对return函数的调用都会xmur3产生一个新的“随机” 32位哈希值,以用作PRNG中的种子。使用方法如下:

// Create xmur3 state:
var seed = xmur3("apples");
// Output four 32-bit hashes to provide the seed for sfc32.
var rand = sfc32(seed(), seed(), seed(), seed());

// Output one 32-bit hash to provide the seed for mulberry32.
var rand = mulberry32(seed());

// Obtain sequential random numbers like so:
rand();
rand();

或者,只需选择一些虚拟数据来填充种子,然后使生成器前进几次(12-20次迭代)以完全混合初始状态。这在PRNG的参考实现中经常看到,但是它确实限制了初始状态的数量。

var seed = 1337 ^ 0xDEADBEEF; // 32-bit seed with optional XOR value
// Pad seed with Phi, Pi and E.
// https://en.wikipedia.org/wiki/Nothing-up-my-sleeve_number
var rand = sfc32(0x9E3779B9, 0x243F6A88, 0xB7E15162, seed);
for (var i = 0; i < 15; i++) rand();

这些PRNG函数的输出会产生一个正的32位数字(0到2 32 -1)Math.random(),如果需要随机数,则将其转换为0-1(包括0,包括1,不包括在内)之间的浮点数。有关特定范围的信息,请阅读MDN上的这篇文章。如果只需要原始位,只需删除最后的除法操作即可。

要注意的另一件事是JS的局限性。数字只能表示高达53位分辨率的整数。并且在使用按位运算时,该值减少为32。这使得难以实现使用C或C ++编写的使用64位数字的算法。移植64位代码需要填充,这会大大降低性能。因此,为了简单和高效,我只考虑了使用32位数学的算法,因为它与JS直接兼容。

现在,继续到发电机。(我在这里维护完整列表,并提供参考资料)


sfc32(简单快速计数器)

sfc32PractRand随机数测试套件的一部分(当然它可以通过)。sfc32具有128位状态,并且在JS中非常快。

function sfc32(a, b, c, d) {
    return function() {
      a >>>= 0; b >>>= 0; c >>>= 0; d >>>= 0; 
      var t = (a + b) | 0;
      a = b ^ b >>> 9;
      b = c + (c << 3) | 0;
      c = (c << 21 | c >>> 11);
      d = d + 1 | 0;
      t = t + d | 0;
      c = c + t | 0;
      return (t >>> 0) / 4294967296;
    }
}

桑树32

Mulberry32是具有32位状态的简单生成器,但速度极快且具有良好的质量(作者声明它已通过gjrand测试套件的所有测试,并且具有完整的2 32周期,但我尚未验证)。

function mulberry32(a) {
    return function() {
      var t = a += 0x6D2B79F5;
      t = Math.imul(t ^ t >>> 15, t | 1);
      t ^= t + Math.imul(t ^ t >>> 7, t | 61);
      return ((t ^ t >>> 14) >>> 0) / 4294967296;
    }
}

如果您只需要一个简单但不错的 PRNG而不需要数十亿个随机数(请参阅生日问题),我将建议您这样做。

xoshiro128 **

截至2018年5月,xoshiro128 **是Vigna / Blackman(他也写了xoroshiro,在Chrome中使用)的Xorshift家族的新成员。它是提供128位状态的最快生成器。

function xoshiro128ss(a, b, c, d) {
    return function() {
        var t = b << 9, r = a * 5; r = (r << 7 | r >>> 25) * 9;
        c ^= a; d ^= b;
        b ^= c; a ^= d; c ^= t;
        d = d << 11 | d >>> 21;
        return (r >>> 0) / 4294967296;
    }
}

作者声称它很好地通过了随机性测试(尽管有警告)。其他研究人员指出,TestU01中的某些测试(尤其是LinearComp和BinaryRank)未通过。实际上,使用浮点数(例如这些实现)时不会引起问题,但是如果依赖原始低位,可能会引起问题。

JSF(詹金斯的小快车)

这是鲍勃·詹金斯(Bob Jenkins,2007年)的JSF或“ smallprng”,他是制作ISAACSpookyHash的人。它通过了 PractRand测试,应该很快,尽管不如SFC快。

function jsf32(a, b, c, d) {
    return function() {
        a |= 0; b |= 0; c |= 0; d |= 0;
        var t = a - (b << 27 | b >>> 5) | 0;
        a = b ^ (c << 17 | c >>> 15);
        b = c + d | 0;
        c = d + t | 0;
        d = a + t | 0;
        return (d >>> 0) / 4294967296;
    }
}

LCG(aka Lehmer / Park-Miller RNG或MCG)

LCG 非常快速和简单,但是其随机性的质量非常低,以至于使用不当实际上可能会导致程序中的错误!但是,它比建议使用Math.sinMath.PI!的答案好得多。不过这是单线的,很好:)。

var LCG=s=>()=>(2**31-1&(s=Math.imul(48271,s)))/2**31;

此实现称为Park-Miller在1988年和1993年提出的最低标准 RNG,并在C ++ 11中实现为。请记住,状态是31位(31位给出20亿个可能的状态,32位给出两倍的状态)。这是其他人试图替代的PRNG类型!minstd_rand

它会起作用,但是除非您确实需要速度并且不在乎随机性质量(无论如何是随机性),否则我不会使用它。非常适合游戏果酱或演示之类的东西。LCG具有种子相关性,因此最好丢弃 LCG 的第一个结果。而且,如果您坚持使用LCG,则增加一个增量值可能会改善结果,但是当存在更好的选择时,这可能是徒劳的。

似乎还有其他乘法器提供32位状态(增加的状态空间):

var LCG=s=>()=>(s=Math.imul(741103597,s)>>>0)/2**32;
var LCG=s=>()=>(s=Math.imul(1597334677,s)>>>0)/2**32;

这些LCG值来自:P. L'Ecuyer:不同大小和良好晶格结构的线性同余生成器表,1997年4月30日。


5
这是一个了不起的答案。我肯定会回到这一点。
DavidsKanal

1
我相信您在Pierre L'ecuyer的“线性同余生成器的表...”中引用的值可能会超过Javascript中的最大整数大小。(2 ^ 32-1)* 741103597≈3e18的最大种子,大于JavaScript的最大int大小≈9e15。我认为Pierre的书中的以下值在本机范围内具有最大的时期:seed = (seed * 185852 + 1) % 34359738337
拉赫曼斯基

1
@Lachmanski是正确的,但是它们受32位(和Park-Miller 31位)的约束。using Math.imul允许它溢出,就像在C中对32位整数使用乘法一样。您的建议是利用JS整数空间的整个范围的LCG,这绝对也是一个有趣的探索领域。:)
bryc

1
这太棒了!我可以将您的sfc32复制到LGPL程序中吗?
user334639

4
@ blobber2不确定您的意思,但是原始代码是从这里(与其他人员一起):github.com/bryc/code/blob/master/jshash/PRNGs.md。或多或少在仓库中的要点:-)
bryc

39

不,但这是一个简单的伪随机数生成器,它是 我从维基百科改编的随身携带实现(此后已删除):

var m_w = 123456789;
var m_z = 987654321;
var mask = 0xffffffff;

// Takes any integer
function seed(i) {
    m_w = (123456789 + i) & mask;
    m_z = (987654321 - i) & mask;
}

// Returns number between 0 (inclusive) and 1.0 (exclusive),
// just like Math.random().
function random()
{
    m_z = (36969 * (m_z & 65535) + (m_z >> 16)) & mask;
    m_w = (18000 * (m_w & 65535) + (m_w >> 16)) & mask;
    var result = ((m_z << 16) + (m_w & 65535)) >>> 0;
    result /= 4294967296;
    return result;
}

编辑:通过使其重置m_z固定种子函数
严重的实现缺陷已得到修复。


3
有人测试过此功能的随机性吗?
贾斯汀

3
这是一个携带时间很长的随身携带(MWC)随机发生器。摘自维基百科随机数生成器
Michael_Scharf 2014年

10
seed函数不会重置随机生成器,因为在调用mz_zrandom()会更改变量。因此,请mz_z = 987654321seed
Michael_Scharf

当我将其与随机颜色生成器(HSL)一起使用时,它仅生成绿色和青色。原始的随机生成器生成所有颜色。因此,它不相同或不起作用。
Tomas Kubes 2014年

@Michael_Scharf 1)种子更改m_w,不是m_z。2)两者m_wm_z都基于其先前的值进行更改,因此它确实会修改结果。
ESL

26

AnttiSykäri的算法很好而且很短。我最初做了一个变体,当您调用Math.seed(s)时,它替换了Javascript的Math.random,但随后Jason评论说,返回该函数会更好:

Math.seed = function(s) {
    return function() {
        s = Math.sin(s) * 10000; return s - Math.floor(s);
    };
};

// usage:
var random1 = Math.seed(42);
var random2 = Math.seed(random1());
Math.random = Math.seed(random2());

这为您提供了Javascript所没有的另一个功能:多个独立的随机生成器。如果要同时运行多个可重复的仿真,这一点尤其重要。


3
如果返回该函数而不是返回该函数,则该函数Math.random将允许您拥有多个独立的生成器,对吗?
杰森·古玛

1
如果对您来说很重要,请确保查看上面有关随机分布的评论:stackoverflow.com/questions/521295/…–
jocull

由此产生的随机数如何重复?它每次都在给出新的数字
SMUsamaShah '16

每次执行时,Math.seed(42);它都会重置功能,因此,如果var random = Math.seed(42); random(); random();得到0.70...,则0.38...。如果您通过var random = Math.seed(42);再次拨打电话将其重置,则下次拨打电话时,random()您将0.70...再次得到提示,下次拨打电话时,将再次得到提示0.38...
杀手StevenJones

1
请不要使用它。请花点时间使用命名为的局部变量,random而不是覆盖本机javascript函数。覆盖Math.random可能导致JIST编译器无法优化所有代码。
杰克·吉芬

11

请查看Pierre L'Ecuyer的工作可以追溯到1980年代末和1990年代初。还有其他。如果您不是专家,则独自创建一个(伪)随机数生成器是非常危险的,因为很有可能结果在统计上不是随机的或周期短。Pierre(和其他人)已经组合了一些易于实现的优质(伪)随机数生成器。我使用他的LFSR发生器之一。

https://www.iro.umontreal.ca/~lecuyer/myftp/papers/handstat.pdf

菲尔·特洛伊


1
很好的答案,但与javascript无关:)
Nikolay Fominyh

3
实现L'Ecuyer教授的工作的代码可在Java上公开获得,并且大多数程序员可以很容易地将其翻译成Javascript。
user2383235

6

结合一些先前的答案,这是您正在寻找的可播种随机函数:

Math.seed = function(s) {
    var mask = 0xffffffff;
    var m_w  = (123456789 + s) & mask;
    var m_z  = (987654321 - s) & mask;

    return function() {
      m_z = (36969 * (m_z & 65535) + (m_z >>> 16)) & mask;
      m_w = (18000 * (m_w & 65535) + (m_w >>> 16)) & mask;

      var result = ((m_z << 16) + (m_w & 65535)) >>> 0;
      result /= 4294967296;
      return result;
    }
}

var myRandomFunction = Math.seed(1234);
var randomNumber = myRandomFunction();

4
在序列开始时使用不同的种子会产生非常相似的结果。例如,Math.seed(0)()收益0.2322845458984375Math.seed(1)()回报0.23228873685002327。改变两者m_wm_z根据种子似乎有所帮助。 var m_w = 987654321 + s; var m_z = 123456789 - s;产生具有不同种子的第一个值的良好分布。
undefined 2016年

1
@undefined您所描述的问题已在上一次编辑中得到修复,这是MWC实施中的错误。
bryc

截至2020年1月,现在可以很好地工作。0的种子获得0.7322976540308446。1的种子,0.16818441334180534,2的种子:0.6040864314418286,3的种子:0.03998844954185188。谢谢你俩!
尤里卡

3

编写自己的伪随机生成器非常简单。

Dave Scotese的建议很有用,但正如其他人所指出的那样,它的分布并不均匀。

但是,这不是因为sin的整数参数。仅仅是因为罪恶的范围,恰好是圆的一维投影。如果采用圆角,则它将是均匀的。

因此,请使用arg(exp(i * x))/(2 * PI)代替sin(x)。

如果您不喜欢线性顺序,请将其与xor混合使用。实际因素也无关紧要。

要生成n个伪随机数,可以使用以下代码:

function psora(k, n) {
  var r = Math.PI * (k ^ n)
  return r - Math.floor(r)
}
n = 42; for(k = 0; k < n; k++) console.log(psora(k, n))

另请注意,当需要实数熵时,不能使用伪随机序列。


我不是专家,但是连续的种子遵循恒定的模式。彩色像素> = 0.5。我猜它只是在半径上反复遍历?
bryc


1

Math.random不,但是运行解决了这个问题。它几乎具有您可以想象的所有分布,并支持种子式随机数生成。例:

ran.core.seed(0)
myDist = new ran.Dist.Uniform(0, 1)
samples = myDist.sample(1000)

-1

我编写了一个返回种子随机数的函数,它使用Math.sin获得一个长随机数,并使用种子从中挑选数字。

采用 :

seedRandom("k9]:2@", 15)

它将返回您的种子编号,第一个参数是任何字符串值; 你的种子。第二个参数是将返回多少个数字。

     function seedRandom(inputSeed, lengthOfNumber){

           var output = "";
           var seed = inputSeed.toString();
           var newSeed = 0;
           var characterArray = ['0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','y','x','z','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','U','R','S','T','U','V','W','X','Y','Z','!','@','#','$','%','^','&','*','(',')',' ','[','{',']','}','|',';',':',"'",',','<','.','>','/','?','`','~','-','_','=','+'];
           var longNum = "";
           var counter = 0;
           var accumulator = 0;

           for(var i = 0; i < seed.length; i++){
                var a = seed.length - (i+1);
                for(var x = 0; x < characterArray.length; x++){
                     var tempX = x.toString();
                     var lastDigit = tempX.charAt(tempX.length-1);
                     var xOutput = parseInt(lastDigit);
                     addToSeed(characterArray[x], xOutput, a, i); 
                }                  
           }

                function addToSeed(character, value, a, i){
                     if(seed.charAt(i) === character){newSeed = newSeed + value * Math.pow(10, a)}
                }
                newSeed = newSeed.toString();

                var copy = newSeed;
           for(var i=0; i<lengthOfNumber*9; i++){
                newSeed = newSeed + copy;
                var x = Math.sin(20982+(i)) * 10000;
                var y = Math.floor((x - Math.floor(x))*10);
                longNum = longNum + y.toString()
           }

           for(var i=0; i<lengthOfNumber; i++){
                output = output + longNum.charAt(accumulator);
                counter++;
                accumulator = accumulator + parseInt(newSeed.charAt(counter));
           }
           return(output)
      }

1
由此产生的数字序列实际上并不近似于随机数序列的性质。例如,使用它生成15个数字,并且对于几乎所有键,结果字符串几乎总是以7开头。
加百利


-6

对于0到100之间的数字。

Number.parseInt(Math.floor(Math.random() * 100))

3
问题在于播种Math.random,以便每当Math.random用相同种子播种时,都会产生相同的连续连续随机数序列。可以说,这个问题不是关于的实际用法/演示Math.random
杰克·吉芬
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.