在 Python 中生成非重复随机数

Posted

技术标签:

【中文标题】在 Python 中生成非重复随机数【英文标题】:Generating non-repeating random numbers in Python 【发布时间】:2011-01-05 19:38:02 【问题描述】:

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

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

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

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

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

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

5) 生成更大的随机数并对它们进行散列(即使用 MD5)以获得更小的数值,我们应该很少会发生冲突,但我最终会再次得到大于需要的数字。

6) 将基于时间的信息添加或附加到随机数(即 unix 时间戳)以减少冲突的机会,我再次得到比我需要的更大的数字。

任何人都有任何聪明的想法可以减少“冲突”的机会(即生成一个已经被采用的随机数),但也可以让我保持数字“小”(即小于十亿(或为您的欧洲人提供一亿 =))。

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

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

【问题讨论】:

不是 Python 但请参阅 ***.com/questions/1617630/… 问题是如果程序运行,停止,运行,停止(如前所述),它就不起作用。 我很好奇,人们正在为这个问题加注主角,但没有投票,这是怎么回事? 感谢您向我们欧洲人解释什么是十亿——我们不知道落后国家有这么大的数字!但说真的——你想识别多少个物体?如果 10^9/number_of_objects 足够大(也许是 100 个)为什么要大惊小怪?很多 SOers 有很多聪明的想法,但你没有告诉我们所有必要的数据来提供一个好的解决方案。 西班牙的十亿是 1.000.000.000.000(百万),而美国的十亿是 1.000.000.000(千万),所以我认为这一点很重要。 ;-) 我不确定其他国家/地区。 【参考方案1】:

您可以使用Format-Preserving Encryption 来加密计数器。您的计数器只是从 0 向上,加密使用您选择的密钥将其转换为您想要的任何基数和宽度的看似随机的值。

块密码通常具有固定的块大小,例如64 位或 128 位。但是格式保留加密允许您采用像 AES 这样的标准密码,并使用您想要的任何基数和宽度(例如问题参数的基数 10,宽度 9)制作更小宽度的密码,算法仍然是加密稳健。

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

AES-FFX 是实现这一目标的一种建议标准方法。

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

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(我认为)方法,请参阅this blog post "How to Generate an Account Number",它使用 Feistel 密码进行 FPE。它生成从 0 到 2^32-1 的数字。

【讨论】:

【参考方案2】:

我遇到了同样的问题并打开了question with a different title,然后才开始处理这个问题。 My 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)),与 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

【讨论】:

【参考方案3】:

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

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))

【讨论】:

【参考方案4】:

我的解决方案https://github.com/glushchenko/python-unique-id,我认为您应该为 1,000,000,000 种变化扩展矩阵并玩得开心。

【讨论】:

请提供有关您的解决方案性质的更多信息。【参考方案5】:

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

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

【讨论】:

【参考方案6】:

如果对你来说,随便的观察者无法猜测下一个值就足够了,你可以使用 linear congruential generator 甚至简单的 linear feedback shift register 来生成值并将状态保存在数据库中以防万一你需要更多的价值观。如果你正确地使用这些,这些值将不会重复,直到宇宙的尽头。您可以在list of random number generators 中找到更多想法。

如果您认为可能有人非常有兴趣猜测下一个值,您可以使用数据库序列来计算您生成的值,并使用加密算法或其他加密强大的完美函数对它们进行加密。但是,您需要注意,如果可以获取您生成的一系列连续数字,则加密算法不容易被破解 - 例如,一个简单的RSA 不会因为Franklin-Reiter Related Message Attack 而做到这一点。

【讨论】:

【参考方案7】:

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

伽罗瓦域

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

x = counter_value
y = some_galois_function(x)
乘以一个常数 逆是乘以常数的倒数 Raise to a power:xn 倒数x-1 提升权力的特殊情况n 它是它自己的逆 Exponentiation 原始元素:ax 请注意,这没有易于计算的逆(离散对数) 确保 a 是primitive element,又名generator

其中许多操作都有逆运算,这意味着,给定您的序列号,您可以计算得出它的原始计数器值。

至于为 Python 的 Galois Field 找到一个库……好问题。如果您不需要速度(您不需要速度),那么您可以自己制作。这些我没试过:

NZMATH Finite field Python package Sage,虽然它是一个完整的数学计算环境,但不仅仅是一个 Python 库

GF(2) 中的矩阵乘法

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

CRC

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

【讨论】:

【参考方案8】:

只要每次将随机间隔减一,就可以运行 1) 而不会遇到错误随机数过多的问题。

要使此方法起作用,您需要保存已经给出的数字(无论如何您都想这样做)并保存所用数字的数量。

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

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

我不喜欢画随机数,直到我得到一个合适的数字。就是感觉不对。

【讨论】:

【参考方案9】:

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

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

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]

【讨论】:

【参考方案10】:

使用一些模数和素数,您可以创建 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

【讨论】:

+1 简洁,不必存储数字列表,对于任何对其进行逆向工程的人来说都会非常满意! :) 如果一个人有三本连续发行的连续剧,逻辑推理不是很容易吗? @Johan:如果素数真的很大,则不会。我所知道的所有现代密码算法都依赖于素数难以分解这一事实。 @Johan:你是对的。如果你有三个或更多连续剧,无论你的质数是多少,下一个数字都相当容易猜到。我被另一个算法弄错了。 @jbochi:这与这个算法无关。另外:我将在 O(1) 中分解任何素数,无需计算机。但我想我们知道你的意思:)【参考方案11】:

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

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

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

Knuth 中有一些指导原则,用于挑选合适的种子,这些种子将生成非常长的唯一数字序列。

【讨论】:

【参考方案12】:

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

【讨论】:

【参考方案13】:

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

使用你的观点 1) 并停止担心。

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

如果您说您只需要 10 亿个数字,即 9 位数字:请多给自己 3 位数字,这样您就有 12 位数字序列号(即三组四位数字 - 美观且易读)。

即使您之前已经选择了十亿个号码,您的新号码已经被选中的概率仍然只有 0.1%。

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

您将在该备用方案被使用之前中奖。

【讨论】:

这也是我所提倡的。 如果您想更加确定,只需再添加其他 3 位数字! ;-) 令人烦恼的是,如果我创建一个列表、随机播放和存储,那么它是确定性的,使用已使用数字的机会为 0,而且我不必使用过长的数字。 @bigredbob:我明白了。但是 1) 12 位数字并不算太长,并且 2) 我只是说最简单的解决方案的缺点并没有你想象的那么大。不要忘记概率会成倍增加。【参考方案14】:

您需要它是加密安全的还是难以猜测的?碰撞有多严重?因为如果它需要加密强度高并且零冲突,那么遗憾的是,这是不可能的。

【讨论】:

既然他在谈论序列号,并且有一个存储它们的数据库表,我认为冲突会很烦人(比如“混淆两个不同的客户”)。 当然,但如果它们更像是软件密钥,那么这可能只是一个烦恼,而不是一个严重的错误。 为什么要强加密排除零冲突?例如,如果您使用序列并使用输出范围为 0-1000000000 的算法加密每个值,那么您两者都有。不过,要找到一个强大的算法来做到这一点并不容易。【参考方案15】:

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

选择两个素数。其中之一将是您可以生成的最大数字,因此应该在 10 亿左右。另一个应该相当大。

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 位素数凭经验证明它,只是需要更多的工作(或更多的内存)。

这确实意味着,只要给定几个序列号,就有可能弄清楚你的值是什么——但只有九位数字,无论如何你都不太可能得到无法猜测的数字。

【讨论】:

(呃。为什么这个网站让成熟的用户随机输入验证码?这很荒谬。)【参考方案16】:

您说您将数字存储在数据库中。

然后将所有数字存储在那里,然后向数据库询问一个随机未使用的数字不是更容易吗? 大多数数据库都支持这样的请求。

例子

mysql

SELECT column FROM table
ORDER BY RAND()
LIMIT 1

PostgreSQL:

SELECT column FROM table
ORDER BY RANDOM()
LIMIT 1

【讨论】:

您需要用十亿行填充一个表,这是非常低效的。不要忘记在使用时需要删除每个数字。【参考方案17】:

我会重新考虑问题本身...您似乎没有对数字做任何连续的事情...而且您在包含它们的列上有一个索引。他们真的需要数字吗?

考虑一个 sha 哈希......你实际上并不需要整个东西。做 git 或其他 url 缩短服务所做的事情,并取散列的前 3/4/5 个字符。假设每个字符现在有 36 个可能的值而不是 10 个,那么您有 2,176,782,336 个组合而不是 999,999 个组合(六位数)。将其与快速检查组合是否存在(纯索引查询)和诸如时间戳+随机数之类的种子相结合,它应该适用于几乎任何情况。

【讨论】:

它们需要是人类可读的(所以序列号比(68123erghdqwoc 更好,它可以让我用 16 个字符表示 MD5)。如果我任意缩短 SHA1,我会像我一样经常遇到冲突随机数并在解决方案 1 中遇到问题。就像我说的,我希望将数字保持在 1 到 10 亿之间。 不保证 SHA 哈希(尤其是缩短的)是唯一的。 我认为你应该注意你的语气。使用长哈希绝对是一个可行的解决方案。如果你这么聪明,那就尝试让 SHA256 散列中断。 很抱歉,当您想要一个真正唯一的数字时,建议“取散列的前 3/4/5 个字符”愚蠢的。更不用说无论如何您都必须获取某些东西的哈希值,因此某些东西本身仍然必须是唯一的...... 嗯。不要在这里陷入激烈的战争,但是当您想要能够为许多项目分配不同的键时,您可以做到。可能还值得研究 URL 缩短服务和 Git,这两者已经做了很长一段时间了。

以上是关于在 Python 中生成非重复随机数的主要内容,如果未能解决你的问题,请参考以下文章

R在数据框中生成非重复对,避免相同的组成员

在 Python 3 中生成具有随机长度的类随机唯一字符串的最快方法

在 C# 中生成随机数 [重复]

我需要在 C 中生成随机数 [重复]

如何获得某些数据的“形状”,以便可以在 numpy/scipy 中生成类似的随机数 [重复]

如何在Java中生成随机字符串[重复]