是否可以Math.random
在 JavaScript 中设置随机数生成器 ( ) 的种子?
在 Javascript 中播种随机数生成器
我已经在纯 JavaScript 中实现了许多优秀、简短且快速的伪随机数生成器(PRNG) 函数。所有这些都可以播种并提供高质量的数字。
首先,注意正确初始化您的 PRNG。为了简单起见,下面的生成器没有内置的种子生成程序,而是接受一个或多个 32 位值作为PRNG的初始种子状态。相似或稀疏的种子(例如 1 和 2 的简单种子)具有低熵,并且会导致相关性或其他质量问题,导致输出具有相似的属性(例如随机生成的级别相似)。为避免这种情况,最佳做法是使用分布良好的高熵种子初始化 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;
}
}
对返回函数的每次后续调用都会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),然后将其转换为 0-1(包括 0,不包括 1)之间的浮点数,等效于Math.random()
,如果您想要随机数对于特定范围,请阅读MDN 上的这篇文章。如果您只想要原始位,只需删除最后的除法运算。
注意:JavaScript 数字只能表示高达 53 位分辨率的整数。而当使用按位运算时,这减少到 32。其他语言中的现代 PRNG 通常使用 64 位运算,这在移植到 JS 时需要垫片,这会大大降低性能。这里的算法只使用了 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;
}
}
桑树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的Xorshift 家族的新成员(Vigna 教授还负责 Xorshift128+ 算法,支持大多数底层Math.random
实现)。它是最快的生成器,可提供 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的小快)
这是 JSF 或 Bob Jenkins (2007) 的“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 编写的解决方案一样快。
不,但这是一个简单的伪随机生成器,这是我从维基百科改编的乘法进位的实现(此后已被删除):
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;
}
Antti Sykäri 的算法很好也很简短。我最初做了一个变体,Math.random
当你调用时替换 JavaScript 的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 没有的另一个功能:多个独立的随机生成器。如果您希望同时运行多个可重复的模拟,这一点尤其重要。