在Python中生成非重复随机数

时间:2010-01-16 09:27:22

标签: python random numbers

好吧这是比听起来更棘手的问题之一,所以我转向堆栈溢出因为我想不出一个好的答案。这就是我想要的:我需要Python以随机顺序生成一个简单的0到1,000,000,000的数字列表,用于序列号(使用随机数,这样你就无法分辨已经分配了多少或做了时间攻击很容易,即猜测下一个会出现的问题)。这些数字与链接到它们的信息一起存储在数据库表(索引)中。生成它们的程序不会永远运行,因此它不能依赖于内部状态。

没什么大不了的?只需生成一个数字列表,将它们推入一个数组并使用Python“random.shuffle(big_number_array)”,我们就完成了。问题是我想避免必须存储一个数字列表(从而读取文件,弹出一个顶部,保存文件并关闭它)。我宁愿在飞行中生成它们。问题是我能想到的解决方案有问题:

1)生成一个随机数,然后检查它是否已被使用。如果已经使用它生成一个新的数字,检查,根据需要重复,直到找到一个未使用的数字。这里的问题是,在获得未使用的数字之前,我可能会感到不幸并生成大量使用过的数字。可能的解决办法:使用一个非常大的数字池来减少这种情况的可能性(但最后我得到了很长的数字)。

2)生成一个随机数,然后检查它是否已被使用。如果已经使用了从数字中添加或减去一个并再次检查,请继续重复,直到我点击未使用的数字。问题是这不再是一个随机数,因为我引入了偏差(最终我会得到一堆数字,你可以预测下一个数字,并有更大的成功机会)。

3)生成一个随机数,然后检查它是否已被使用。如果它已被使用添加或减去另一个随机生成的随机数并再次检查,问题是我们回到简单生成随机数并检查解决方案1。

4)将其取出并生成随机列表并保存,让守护进程将它们放入队列中,以便有可用的数字(并避免不断打开和关闭文件,改为对其进行批处理)。

5)生成更大的随机数并对它们进行哈希处理(即使用MD5)以获得更小的数值,我们应该很少发生冲突,但最终我的结果大于所需的数字。

6)在随机数(即unix时间戳)之前添加或附加基于时间的信息,以减少发生碰撞的可能性,同样,我得到的数字也超出了我的需要。

任何人都有任何聪明的想法,可以减少“碰撞”的机会(即生成已经采取的随机数),但也可以让我保持数字“小”(即少于10亿(或千万为你的欧洲人=))。

答案以及我接受它的原因:

所以我会简单地选择1,并希望它不是一个问题,但是如果是的话,我会选择生成所有数字并存储它们的确定性解决方案,以便有一个获得新的随机数的保证,我可以使用“小”数字(即9位数而不是MD5 /等)。

17 个答案:

答案 0 :(得分:25)

这是一个很好的问题,我已经考虑了一段时间(解决方案类似于Sjoerd's),但最后,这就是我的想法:

使用你的观点1)并且不要担心。

假设真正的随机性,之前已经选择了随机数的概率是先前选择的数字的计数除以您的池的大小,即最大数量。

如果你说你只需要十亿个数字,即9个数字:给自己增加3个数字,所以你有12个数字的序列号(这是三组四个数字 - 很好看,可读)。

即使您之前已经选择了十亿个数字,您的新号码的可能性仍然只有0.1%。

执行第1步并再次绘制。您仍然可以检查“无限”循环,比如不要尝试超过1000次左右,然后回退到添加1(或其他东西)。

在使用后备版之前,你将赢得彩票。

答案 1 :(得分:12)

您可以使用Format-Preserving Encryption加密计数器。你的计数器从0开始向上,加密使用你选择的一个键将它变成一个看似随机的值,无论你想要什么基数和宽度。

分组密码通常具有固定的块大小,例如, 64或128位。但格式保留加密允许您采用像AES这样的标准密码,并制作一个小宽度的密码,无论你想要什么基数和宽度(例如基数10,问题参数的宽度9),算法仍然是密码学上强大。

保证永远不会发生冲突(因为加密算法会创建1:1映射)。它也是可逆的(双向映射),因此您可以获取结果数字并返回到您开始时的计数器值。

AES-FFX是一种提出标准的方法来实现这一目标。

我已经尝试了AES-FFX的一些基本Python代码 - see Python code here(但请注意,它并不完全符合AES-FFX规范)。它可以是例如将计数器加密为随机查看的7位十进制数。 E.g:

0000000   0731134
0000001   6161064
0000002   8899846
0000003   9575678
0000004   3030773
0000005   2748859
0000006   5127539
0000007   1372978
0000008   3830458
0000009   7628602
0000010   6643859
0000011   2563651
0000012   9522955
0000013   9286113
0000014   5543492
0000015   3230955
...       ...

对于Python中的另一个示例,使用另一种非AES-FFX(我认为)方法,请参阅使用Feistel密码进行FPE的this blog post "How to Generate an Account Number"。它生成从0到2 ^ 32-1的数字。

答案 2 :(得分:8)

使用一些模块化的算术和素数,你可以创建0到大素数之间的所有数字,不按顺序。 如果你仔细选择你的数字,下一个数字很难猜测。

modulo = 87178291199 # prime
incrementor = 17180131327 # relative prime

current = 433494437 # some start value
for i in xrange(1, 100):
    print current
    current = (current + incrementor) % modulo

答案 3 :(得分:6)

如果你不需要加密安全的东西,但只是“充分混淆”......

Galois Fields

您可以尝试Galois Fields中的操作,例如GF(2) 32 ,将简单的递增计数器 x 映射到看似随机的序列号 y

x = counter_value
y = some_galois_function(x)

其中许多操作都有反转,这意味着,根据您的序列号,您可以计算从中得到的原始计数器值。

至于为Galois Field寻找Python的库...好问题。如果你不需要速度(你不需要速度),那么你可以自己动手。我没试过这些:

GF(2)中的矩阵乘法

在GF(2)中选择合适的32×32可逆矩阵,并将32位输入计数器乘以它。这在概念上与LFSR相关,如S.Lott's answer中所述。

<强> CRC

相关的可能性是使用CRC计算。基于GF(2)中的不可约多项式的长除的余数。 Python代码随时可用于CRC(crcmodpycrc),尽管您可能希望选择与通常使用的不同的不可约多项式,以用于您的目的。我对理论有点模糊,但我认为32位CRC应该为每个可能的4字节输入组合生成一个唯一值。检查一下。通过将输出反馈到输入中并检查它是否产生一个长度为2 32 -1的完整循环(零只是映射到零),很容易通过实验检查这一点。您可能需要摆脱CRC算法中的任何初始/最终XOR,以使此检查起作用。

答案 4 :(得分:6)

如果它们不必是随机的,但不是明显线性的(1,2,3,4 ......),那么这是一个简单的算法:

选择两个素数。其中一个将是您可以生成的最大数字,因此它应该是大约十亿。另一个应该相当大。

max_value = 795028841
step = 360287471
previous_serial = 0
for i in xrange(0, max_value):
    previous_serial += step
    previous_serial %= max_value
    print "Serial: %09i" % previous_serial

每次只存储上一个序列,以便您知道上次停止的位置。我无法通过数学证明这是有效的(自那些特定的类以来已经太长了),但它对于较小的素数来说是明显正确的:

s = set()
with open("test.txt", "w+") as f:
    previous_serial = 0
    for i in xrange(0, 2711):
        previous_serial += 1811
        previous_serial %= 2711
        assert previous_serial not in s
        s.add(previous_serial)

你也可以通过9位数的素数来证明它,它只需要更多的工作(或者更多的记忆)。

这确实意味着只要给出一些序列号,就有可能弄清楚你的价值是多少 - 但只有九位数,不管怎么说,你不太可能会选择不合适的数字。

答案 5 :(得分:5)

我认为你高估了方法1)的问题。除非您有硬实时要求,否则只需通过随机选择即可快速终止。需要多次迭代的概率呈指数衰减。输出100M数字(10%fillfactor),您将有十亿次机会需要超过9次迭代。即使有50%的数字被采用,你平均需要2次迭代,并且有十分之一的机会需要超过30次检查。或者甚至已经采用了99%的数字的极端情况可能仍然是合理的 - 你将平均100次迭代并且需要2062次迭代的十亿次变化中有1次

答案 6 :(得分:4)

标准线性同余随机数发生器的种子序列不能重复,直到生成了起始种子值的全部数字。然后它必须精确重复。

内部种子通常很大(48或64位)。生成的数字较小(通常为32位),因为整个位组不是随机的。如果您遵循种子值,它们将形成独特的非重复序列。

问题基本上是找到一个产生“足够”数字的好种子。您可以选择一个种子,并生成数字,直到您回到起始种子。这是序列的长度。它可能是数百万或数十亿的数字。

Knuth中有一些指导方针可以选择合适的种子,这些种子会产生很长的独特数字序列。

答案 7 :(得分:1)

如果你每次只减少一个随机间隔,你可以运行1)而不会遇到太多错误随机数的问题。

要使此方法有效,您需要保存已经给出的数字(无论如何要保存),并保存所拍摄的数字。

很明显,在收集了10个数字后,您的可能随机数池将减少10个。因此,您不能选择介于1和1.000.000之间但介于1和999.990之间的数字。当然这个数字不是实数而只是一个指数(除非收集的10个数字是999.991,999.992,......);你现在必须从1中省略所有已收集的数字。

当然,你的算法应该比从1到1.000.000计算更聪明,但我希望你理解这个方法。

我不喜欢绘制随机数字,直到我找到一个适合的任意数字。这只是感觉不对。

答案 8 :(得分:1)

我的解决方案https://github.com/glushchenko/python-unique-id,我认为您应该扩展矩阵以获得1,000,000,000种变化并享受乐趣。

答案 9 :(得分:0)

你需要这个加密安全还是难以猜测?碰撞有多糟糕?因为如果它需要加密强大并且没有碰撞,那么遗憾的是,这是不可能的。

答案 10 :(得分:0)

我会重新考虑问题本身......你似乎没有按照数字顺序做任何事情......而且你的列上有一个索引。他们实际上需要数字吗?

考虑一个sha hash ...你实际上并不需要整个事情。做git或其他url缩短服务做什么,并采取散列的前3/4/5字符。鉴于每个角色现在有36个可能的值而不是10个,你有2,176,782,336个组合而不是999,999个组合(六个数字)。将其与快速检查组合是否存在(纯索引查询)和种子如时间戳+随机数相结合,几乎可以用于任何情况。

答案 11 :(得分:0)

我开始尝试编写下面使用的方法的解释,但只是实现它更容易,更准确。这种方法有一种奇怪的行为,即你生成的数字越多,它就越快。但它有效,并且它不需要您提前生成所有数字。

作为一个简单的优化,您可以轻松地让这个类使用概率算法(生成一个随机数,如果它不在使用过的数字集合中,将它添加到集合中并返回它),请先跟踪碰撞率,一旦碰撞率变差,切换到此处使用的确定性方法。

import random

class NonRepeatingRandom(object):

    def __init__(self, maxvalue):
        self.maxvalue = maxvalue
        self.used = set()

    def next(self):
        if len(self.used) >= self.maxvalue:
            raise StopIteration
        r = random.randrange(0, self.maxvalue - len(self.used))
        result = 0
        for i in range(1, r+1):
            result += 1
            while result in self.used:
                 result += 1
        self.used.add(result)
        return result

    def __iter__(self):
        return self

    def __getitem__(self):
        raise NotImplemented

    def get_all(self):
        return [i for i in self]

>>> n = NonRepeatingRandom(20)
>>> n.get_all()
[12, 14, 13, 2, 20, 4, 15, 16, 19, 1, 8, 6, 7, 9, 5, 11, 10, 3, 18, 17]

答案 12 :(得分:0)

如果一个偶然的观察者无法猜测下一个值就足够了,你可以使用像linear congruential generator甚至是简单的linear feedback shift register之类的东西来生成值并保持状态。数据库,以防您需要更多值。如果使用这些权限,则值将不会重复,直到Universe结束。您会在list of random number generators中找到更多想法。

如果您认为可能有人对猜测下一个值感兴趣,您可以使用数据库序列来计算您生成的值,并使用加密算法或其他加密强完美函数对其进行加密。但是,如果你能够掌握一系列你生成的连续数字,你需要注意加密算法是不容易破解的 - 例如,一个简单的RSA因为{{Franklin-Reiter Related Message Attack而不会这样做。 3}}

答案 13 :(得分:0)

迟到的答案,但我没有在任何地方看到这个建议。

为什么不使用uuid模块创建globally unique identifiers

答案 14 :(得分:0)

要生成定义阈值内的完全随机数列表,如下所示:

plist=list()
length_of_list=100
upbound=1000
lowbound=0
while len(pList)<(length_of_list):
     pList.append(rnd.randint(lowbound,upbound))
     pList=list(set(pList))

答案 15 :(得分:0)

我遇到了同样的问题并在开始之前打开了question with a different titleMy solution是区间[0,maximal)中的索引(即非重复数字)的随机样本生成器,称为itersample。以下是一些使用示例:

import random
generator=itersample(maximal)
another_number=generator.next() # pick the next non-repeating random number

import random
generator=itersample(maximal)
for random_number in generator:
    # do something with random_number
    if some_condition: # exit loop when needed
        break

itersample生成非重复随机整数,存储需求仅限于选取的数字,选择n数字所需的时间应该是(如某些测试所确认的)O(n log(n)),regardelss maximal

以下是itersample的代码:

import random
def itersample(c): # c = upper bound of generated integers
    sampled=[]
    def fsb(a,b): # free spaces before middle of interval a,b
        fsb.idx=a+(b+1-a)/2
        fsb.last=sampled[fsb.idx]-fsb.idx if len(sampled)>0 else 0
        return fsb.last
    while len(sampled)<c:
        sample_index=random.randrange(c-len(sampled))
        a,b=0,len(sampled)-1
        if fsb(a,a)>sample_index:
            yielding=sample_index
            sampled.insert(0,yielding)
            yield yielding
        elif fsb(b,b)<sample_index+1:
            yielding=len(sampled)+sample_index
            sampled.insert(len(sampled),yielding)
            yield yielding
        else: # sample_index falls inside sampled list
            while a+1<b:
                if fsb(a,b)<sample_index+1:
                    a=fsb.idx
                else:
                    b=fsb.idx
            yielding=a+1+sample_index
            sampled.insert(a+1,yielding)
            yield yielding

答案 16 :(得分:-2)

您声明将数字存储在数据库中。

那么在那里存储所有数字会不会更容易,并向数据库询问随机未使用的数字? 大多数数据库都支持这样的请求。

实施例

MySQL的:

SELECT column FROM table
ORDER BY RAND()
LIMIT 1

的PostgreSQL:

SELECT column FROM table
ORDER BY RANDOM()
LIMIT 1