您正在寻找一个线性同余发生器 https://en.wikipedia.org/wiki/Linear_congruential_generator具有完整的周期。这将使您能够获得目标数字范围内的非重复数字的伪随机序列。
实现 LCG 实际上非常简单,如下所示:
def lcg(a, c, m, seed = None):
num = seed or 0
while True:
num = (a * num + c) % m
yield num
然后,只需选择正确的值a
, c
, and m
以保证 LCG 将生成完整的周期(这是您获得不重复数字的唯一保证)。正如维基百科文章所解释的,需要满足以下三个条件:
-
m
and c
需要是相对素数。
-
a - 1
可以被所有素因数整除m
-
a - 1
能被 4 整除,如果m
也能被4整除。
第一个很容易通过简单地选择素数来保证c
。另外,这是最后可以选择的值,这最终将使我们能够稍微混淆序列。
之间的关系a - 1
and m
不过更复杂。在全周期 LCG 中,m
是周期的长度。或者换句话说,它是您的数字来自的数字范围。所以这就是你通常首先选择的。在你的情况下,你想要m
在身边1000000
。准确选择你的最大数量可能很困难,因为这限制了你很多(在你的选择中)a
并且c
),因此您也可以选择大于该数字的数字,然后跳过范围之外的所有数字。
我们来选择一下m = 1000000
但现在。主要因素为m
are 2
and 5
。显然它也可以整除4
。因此对于a - 1
,我们需要一个数的倍数2 * 2 * 5
满足条件2和3。让我们选择a - 1 = 160
, so a = 161
.
For c
,我们使用一个介于我们范围之间的随机素数:c = 506903
将其放入我们的 LCG 中即可得到我们想要的序列。我们可以从范围(0 <= seed <= m
)作为我们序列的起点。
那么让我们尝试一下并验证我们的想法是否确实有效。为此,我们只是从一组生成器中收集所有数字,直到找到重复的数字。到那时,我们应该有m = 1000000
集合中的数字:
>>> g = lcg(161, 506903, 1000000)
>>> numbers = set()
>>> for n in g:
if n in numbers:
raise Exception('Number {} already encountered before!'.format(n))
numbers.add(n)
Traceback (most recent call last):
File "<pyshell#5>", line 3, in <module>
raise Exception('Number {} already encountered before!'.format(n))
Exception: Number 506903 already encountered before!
>>> len(numbers)
1000000
这是正确的!所以我们确实创建了一个伪随机数字序列,使我们能够从我们的范围中获取不重复的数字m
。当然,根据设计,这个序列将始终相同,因此当您选择这些数字时它只会随机一次。您可以切换以下值a
and c
但是,只要保持上述属性即可获得不同的序列。
这种方法的最大好处当然是您不需要存储所有先前生成的数字。它是一个恒定空间算法,因为它只需要记住初始配置和之前生成的值。
当您进一步了解序列时,它也不会恶化。这是一个普遍的问题,解决方案只是不断生成随机数,直到发现一个以前从未遇到过的新随机数。这是因为生成的数字列表越长,使用均匀分布的随机算法命中不在该列表中的数字的可能性就越小。因此,获得第 1000000 个数字可能需要很长时间才能使用基于内存的随机生成器生成。
但是,当然,这种简单的算法只执行一些乘法和一些加法,看起来并不是很随机。但您必须记住,这实际上是大多数伪随机数生成器的基础。所以random.random()
在内部使用类似的东西。只是那m
is 大很多,所以你不会注意到它在那里。