是否有可能在JavaScript中播种随机数生成器(Math.random) ?


当前回答

Antti Sykäri的算法很好,很短。我最初做了一个变种来取代JavaScript的Math。当你调用Math.seed(s)时是随机的,但随后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没有的另一个功能:多个独立的随机生成器。如果您希望同时运行多个可重复的模拟,这一点尤其重要。

其他回答

不,不可能为Math.random()提供种子,但是编写自己的生成器相当容易,或者更好的是使用现有的生成器。

请看:这个相关的问题。

另外,请参阅David Bau的博客了解更多关于播种的信息。

结合之前的一些答案,这是你正在寻找的可种子随机函数:

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();

对于0到100之间的数。

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

这里的大多数答案都会产生偏颇的结果。这是一个基于github的seerandom库的测试函数:

!function(f,a,c){var s,l=256,p="random",d=c.pow(l,6),g=c.pow(2,52),y=2*g,h=l-1;function n(n,t,r){function e(){for(var n=u.g(6),t=d,r=0;n<g;)n=(n+r)*l,t*=l,r=u.g(1);for(;y<=n;)n/=2,t/=2,r>>>=1;return(n+r)/t}var o=[],i=j(function n(t,r){var e,o=[],i=typeof t;if(r&&"object"==i)for(e in t)try{o.push(n(t[e],r-1))}catch(n){}return o.length?o:"string"==i?t:t+"\0"}((t=1==t?{entropy:!0}:t||{}).entropy?[n,S(a)]:null==n?function(){try{var n;return s&&(n=s.randomBytes)?n=n(l):(n=new Uint8Array(l),(f.crypto||f.msCrypto).getRandomValues(n)),S(n)}catch(n){var t=f.navigator,r=t&&t.plugins;return[+new Date,f,r,f.screen,S(a)]}}():n,3),o),u=new m(o);return e.int32=function(){return 0|u.g(4)},e.quick=function(){return u.g(4)/4294967296},e.double=e,j(S(u.S),a),(t.pass||r||function(n,t,r,e){return e&&(e.S&&v(e,u),n.state=function(){return v(u,{})}),r?(c[p]=n,t):n})(e,i,"global"in t?t.global:this==c,t.state)}function m(n){var t,r=n.length,u=this,e=0,o=u.i=u.j=0,i=u.S=[];for(r||(n=[r++]);e<l;)i[e]=e++;for(e=0;e<l;e++)i[e]=i[o=h&o+n[e%r]+(t=i[e])],i[o]=t;(u.g=function(n){for(var t,r=0,e=u.i,o=u.j,i=u.S;n--;)t=i[e=h&e+1],r=r*l+i[h&(i[e]=i[o=h&o+t])+(i[o]=t)];return u.i=e,u.j=o,r})(l)}function v(n,t){return t.i=n.i,t.j=n.j,t.S=n.S.slice(),t}function j(n,t){for(var r,e=n+"",o=0;o<e.length;)t[h&o]=h&(r^=19*t[h&o])+e.charCodeAt(o++);return S(t)}function S(n){return String.fromCharCode.apply(0,n)}if(j(c.random(),a),"object"==typeof module&&module.exports){module.exports=n;try{s=require("crypto")}catch(n){}}else"function"==typeof define&&define.amd?define(function(){return n}):c["seed"+p]=n}("undefined"!=typeof self?self:this,[],Math);

function randIntWithSeed(seed, max=1) {
  /* returns a random number between [0,max] including zero and max
  seed can be either string or integer */
  return Math.round(new Math.seedrandom('seed' + seed)()) * max
}

测试此代码的真正随机性:https://es6console.com/kkjkgur2/

不,不可能给Math.random()添加种子。ECMAScript规范故意在这个主题上含糊不清,既不提供播种的方法,也不要求浏览器使用相同的算法。因此,这样的函数必须由外部提供,谢天谢地,这并不太难。

我已经在纯JavaScript中实现了许多好的、短的、快速的伪随机数生成器(PRNG)函数。所有这些都可以播种,并提供高质量的数字。这些并不用于安全目的——如果您需要一个可播种的CSPRNG,请查看ISAAC。

First of all, take care to initialize your PRNGs properly. To keep things simple, the generators below have no built-in seed generating procedure, but accept one or more 32-bit numbers as the initial seed state of the PRNG. Similar or sparse seeds (e.g. a simple seed of 1 and 2) have low entropy, and can cause correlations or other randomness quality issues, sometimes resulting in the output having similar properties (such as randomly generated levels being similar). To avoid this, it is best practice to initialize PRNGs with a well-distributed, high entropy seed and/or advancing past the first 15 or so numbers.

有很多方法可以做到这一点,但这里有两种方法。首先,哈希函数非常擅长从短字符串中生成种子。即使两个字符串相似,一个好的哈希函数也会产生非常不同的结果,所以你不必在字符串上花太多心思。下面是一个哈希函数的例子:

function cyrb128(str) {
    let h1 = 1779033703, h2 = 3144134277,
        h3 = 1013904242, h4 = 2773480762;
    for (let i = 0, k; i < str.length; i++) {
        k = str.charCodeAt(i);
        h1 = h2 ^ Math.imul(h1 ^ k, 597399067);
        h2 = h3 ^ Math.imul(h2 ^ k, 2869860233);
        h3 = h4 ^ Math.imul(h3 ^ k, 951274213);
        h4 = h1 ^ Math.imul(h4 ^ k, 2716044179);
    }
    h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067);
    h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233);
    h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213);
    h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179);
    return [(h1^h2^h3^h4)>>>0, (h2^h1)>>>0, (h3^h1)>>>0, (h4^h1)>>>0];
}

调用cyrb128将从一个可用于PRNG种子的字符串中产生一个128位哈希值。下面是你如何使用它:

// Create cyrb128 state:
var seed = cyrb128("apples");
// Four 32-bit component hashes provide the seed for sfc32.
var rand = sfc32(seed[0], seed[1], seed[2], seed[3]);

// Only one 32-bit component hash is needed for mulberry32.
var rand = mulberry32(seed[0]);

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

注意:如果您想要稍微健壮一点的128位哈希,可以考虑MurmurHash3_x86_128,它更彻底,但适用于大型数组。

或者,简单地选择一些虚拟数据来填充种子,并预先将生成器推进几次(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到232-1),然后转换为一个0-1(0包含,1不包含)之间的浮点数,等效于Math.random(),如果您想要特定范围的随机数,请阅读MDN上的这篇文章。如果您只想要原始位,只需删除最后的除法操作。

JavaScript数字只能表示53位分辨率的整数。而当使用位操作时,它被减少到32。其他语言中的现代prng通常使用64位操作,这在移植到JS时需要shims,这会大大降低性能。这里的算法只使用32位操作,因为它与JS直接兼容。

现在,我们来谈谈发电机。(我在这里保留了完整的参考文献和许可信息列表)


sfc32(简单快速计数器)

sfc32是PractRand随机数测试套件的一部分(当然它通过了测试)。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;
    }
}

你可能想知道| 0和>>>= 0是干什么用的。它们本质上是32位整数强制转换,用于性能优化。JS中的Number基本上是浮点数,但在按位操作时,它们切换到32位整数模式。JS解释器可以更快地处理这种模式,但任何乘法或加法都会导致它切换回浮点数,从而导致性能下降。

Mulberry32

Mulberry32是一个32位状态的简单生成器,但是速度非常快,并且具有良好的随机性(作者声明它通过了gjrand测试套件的所有测试,并且具有完整的232周期,但我还没有验证)。

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**是Xorshift家族的新成员,由Vigna和Blackman开发(Vigna教授还负责为大多数数学提供支持的Xorshift128+算法。底层的随机实现)。它是最快的生成器,提供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 (Jenkins的小而快)

这是Bob Jenkins(2007)的JSF或“smallprng”,他还制作了ISAAC和SpookyHash。它通过了PractRand测试,应该非常快,尽管没有sfc32快。

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;
    }
}