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


当前回答

这里有很多很好的答案,但我有一个类似的问题,即我希望Java的随机数生成器和我最终在JavaScript中使用的任何东西之间的可移植性。

我找到了java-random包

假设种子相同,这两段代码有相同的输出:

Java:

Random randomGenerator = new Random(seed);
int randomInt;
for (int i=0; i<10; i++) {
    randomInt = randomGenerator.nextInt(50);
    System.out.println(randomInt);
}

JavaScript:

let Random = require('java-random');
let rng = new Random(seed);
for (let i=0; i<10; i++) {
    let val = rng.nextInt(50);
    console.log(val);
}

其他回答

对于一个固定的种子有一个简单的方法:

function fixedrandom(p){
    const seed = 43758.5453123;
    return (Math.abs(Math.sin(p)) * seed)%1;
}

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

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

不可能在Math中植入种子。随机函数,但是用Javascript实现一个高质量的RNG是可能的,只需很少的代码。

Javascript数字是64位浮点精度,可以表示小于2^53的所有正整数。这给我们的算法带来了一个硬限制,但在这些限制内,您仍然可以为高质量的Lehmer / LCG随机数生成器选择参数。

function RNG(seed) {
    var m = 2**35 - 31
    var a = 185852
    var s = seed % m
    return function () {
        return (s = s * a % m) / m
    }
}

Math.random = RNG(Date.now())

如果你想要更高质量的随机数,代价是速度慢10倍,你可以使用BigInt进行算术,并选择m刚好适合双精度的参数。

function RNG(seed) {
    var m_as_number = 2**53 - 111
    var m = 2n**53n - 111n
    var a = 5667072534355537n
    var s = BigInt(seed) % m
    return function () {
        return Number(s = s * a % m) / m_as_number
    }
}

参见Pierre l’ecuyer的这篇论文,了解上述实现中使用的参数: https://www.ams.org/journals/mcom/1999-68-225/S0025-5718-99-00996-5/S0025-5718-99-00996-5.pdf

无论你做什么,避免使用Math.sin的所有其他答案!