在JavaScript中,是否可能对随机数生成器 (Math.random
) 进行种子初始化?
在JavaScript中,是否可能对随机数生成器 (Math.random
) 进行种子初始化?
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);
h1 ^= (h2 ^ h3 ^ h4), h2 ^= h1, h3 ^= h1, h4 ^= h1;
return [h1>>>0, h2>>>0, h3>>>0, h4>>>0];
}
调用 cyrb128
将从字符串生成一个 128 位哈希值,可用于种子 PRNG(伪随机数生成器)。以下是使用方法:
// 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次迭代)推进发生器以充分混合初始状态。这样做的好处是更简单,并且通常在随机数生成器的参考实现中使用,但会限制初始状态的数量:
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();
注意:这些伪随机数生成函数的输出产生一个正的32位数字(从0到232-1),然后转换为一个介于0-1之间(包括0,不包括1)的浮点数,相当于Math.random()
。如果你想要一个特定范围的随机数,请阅读这篇MDN上的文章。如果你只想要原始位,只需删除最后的除法运算。
JavaScript的数字只能表示最大为53位的整数。而在使用位操作时,这个范围被减小到32位。其他语言中的现代伪随机数生成器通常使用64位操作,在将其移植到JS时需要适配层,这可能会极大地降低性能。这里的算法只使用32位操作,因为它与JS直接兼容。
现在,进入生成器部分。(我在这里维护了完整的列表,包含参考和许可信息此处)
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是一个简单的生成器,具有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;
}
}
Math.random
实现)。它是提供128位状态的最快生成器。function xoshiro128ss(a, b, c, d) {
return function() {
var t = b << 9, r = b * 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)。在实践中,当使用浮点数时(例如在这些实现中),它不应引起问题,但如果依赖于原始的最低位,则可能会引起问题。
这是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.imul
允许它像在32位整数上使用乘法时一样溢出。您建议的是使用JS整数空间的完整范围来使用LCG,这绝对是一个有趣的探索领域。 :) - bryc注意:尽管这个算法的简洁和明显的优势,但在随机性方面并不是高质量的。请查看此答案中列出的更好的选择。
(原始想法改编自对另一个答案的评论。)
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;
}
3.469446951953614e-18
,非常均匀。这个 是我用来测试分布的脚本,只需要一个具有 next()
方法的对象。 - Qix - MONICA WAS MISTREATED安蒂·西卡里的算法很好而且很短。我最初做了一个变体,它在调用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所没有的另一种功能:多个独立的随机生成器。如果您想同时运行多个可重复模拟,则这尤其重要。
Math.random
,这将允许你拥有多个独立的生成器,对吗? - Jason Goemaatrandom
的局部变量代替覆盖原生的JavaScript函数。覆盖 Math.random
可能会导致JIST编译器取消所有代码的优化。 - Jack G结合之前的答案,这就是你要寻找的可种子化随机函数:
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.seed(0)()
返回 0.2322845458984375
,而 Math.seed(1)()
返回 0.23228873685002327
。根据种子改变 m_w
和 m_z
都能产生较好的效果。使用 var m_w = 987654321 + s; var m_z = 123456789 - s;
能够在不同的种子下生成具有良好分布的首个值。 - undefined>>> 0
感到好奇,想知道为什么它比更直接的 & mask
更好。 - President James K. Polk不能为内置的 Math.random 函数设置种子,但是可以用非常少的代码在 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刚好能够适合于double。
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
}
}
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
不行,但是ran库可以解决这个问题。它几乎包含了你能想到的所有分布,并支持种子随机数生成。例如:
ran.normal(0, 1)([42, 43, 44])
这将返回一个由平均值为0,标准差为1的正态分布生成的随机数数组,种子为[42, 43, 44]。
ran.core.seed(0)
myDist = new ran.Dist.Uniform(0, 1)
samples = myDist.sample(1000)