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


当前回答

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

戴夫·斯科塞斯的建议是有用的,但正如其他人指出的那样,它并不是完全均匀分布的。

然而,这并不是因为sin的整数参数。这只是因为sin的范围,恰好是一个圆的一维投影。如果取圆的角度,它就会是均匀的。

所以用arg(exp(i * x)) / (2 * PI)代替sin(x)

如果你不喜欢线性顺序,可以把它和异或混合一下。实际因素也没有那么重要。

要生成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))

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

其他回答

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

请看:这个相关的问题。

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

不,不可能给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;
    }
}

注意:尽管(或者说,因为)简洁和明显的优雅,这个算法在随机性方面绝不是一个高质量的算法。看看这个答案中列出的例子,会有更好的结果。

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

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

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

在我看来,这个解决方案的优雅之处在于没有任何“神奇”数字(除了10000,它代表了您必须丢弃的最小数字数量,以避免奇怪的模式-请参阅值为10,100,1000的结果)。简洁也很好。

它比Math.random()稍微慢一点(2或3倍),但我相信它与任何其他用JavaScript编写的解决方案一样快。

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() 但你可以安装外部包,为这做准备。我使用这些包,可以安装使用这些命令

npm i random-seed

该示例来自包文档。

var seed = 'Hello World',
rand1 = require('random-seed').create(seed),
rand2 = require('random-seed').create(seed);
console.log(rand1(100), rand2(100));

点击链接获取文档https://www.npmjs.com/package/random-seed