如何生成一个不以 0 开头且具有唯一数字的随机 4 位数字?

Posted

技术标签:

【中文标题】如何生成一个不以 0 开头且具有唯一数字的随机 4 位数字?【英文标题】:How to generate a random 4 digit number not starting with 0 and having unique digits? 【发布时间】:2017-09-21 10:35:26 【问题描述】:

这几乎可以正常工作,但数字有时以 0 开头:

import random
numbers = random.sample(range(10), 4)
print(''.join(map(str, numbers)))

我找到了很多示例,但没有一个可以保证序列不会以 0 开头。

【问题讨论】:

您可以在range() 中指定起始值。在您的情况下,它可能是 1 这几乎可以正常工作,但数字有时会从 0 开始。如果你运行它足够多的次数,大概有 10% 的时间。 Related PPCG challenge 安全相关问答:When choosing a numeric PIN, does it help or hurt to make each digit unique? 致 OP:请撤回您对已接受答案的接受。它不应该是公认的答案。它是前六个答案中最慢的,而且差距很大。 【参考方案1】:

我们生成 1 - 9 范围内的第一个数字,然后从剩余的数字中取出接下来的 3:

import random

# We create a set of digits: 0, 1, .... 9
digits = set(range(10))
# We generate a random integer, 1 <= first <= 9
first = random.randint(1, 9)
# We remove it from our set, then take a sample of
# 3 distinct elements from the remaining values
last_3 = random.sample(digits - first, 3)
print(str(first) + ''.join(map(str, last_3)))

生成的数字是等概率的,一步就能得到一个有效的数字。

【讨论】:

@Claudio 生成的数字的独立性是一个好的伪随机生成器必须满足的基本属性之一。我认为 Python 的随机性已经足够好,以至于后续调用可以被认为是独立的。 请注意,虽然此方法确实以相同的概率生成每个可能的输出,但这一事实并非显而易见。它确实产生等概率原因的关键原因是,在选择了第一个数字之后,无论选择哪个第一个数字,剩余的可能数字的数量都是相同的。特别是,如果您尝试反转该过程(即首先选择最低的三个不同数字,然后从其余七个中选择一个非零第一个数字),则输出将不是是等概率的。 (读者练习:为什么?) 投反对票,因为这是最慢的最佳答案。这结合了集合算术和random.sample,两者都很慢。 @DavidHammen 为什么值得一票否决?在我的机器上,这个答案比 Austin Hastings 的答案慢了不到 4 微秒,而且问题中甚至没有提到性能。我会犹豫将建设性方法与拒绝抽样技术一起称为“无用”。 @Mitch - 主要是为了反驳我认为由于对拒绝抽样概念缺乏理解而对其他答案完全无效的反对票。理解拒绝抽样的简单方法:假设我要求您生成所有没有重复数字且以 1 开头的四位数字。您可以想出一个复杂的算法来做到这一点,或者您可以使用 [n for n in range(1000,10000) if len(set(str(n)))==4] . (续)【参考方案2】:

循环直到你有你喜欢的东西:

import random

numbers = [0]
while numbers[0] == 0:
    numbers = random.sample(range(10), 4)

print(''.join(map(str, numbers)))

【讨论】:

@Claudio 对于所有结果,概率仍然相等,所以我不确定你所说的不随机是什么意思 @Claudio 很抱歉,但在建议所有答案都不正确之前,您可能应该查阅某种关于接受-拒绝抽样的参考并查看条件概率。 这可能永远循环。几乎不是一个通用的解决方案。 @BoundaryImposition 和所有其他反对者:这是一个非常通用的解决方案。它甚至还有一个名字,“拒绝采样”。大多数正态分布的实现都使用拒绝抽样。 @BoundaryImposition 和所有其他反对者:在 Austin Hastings、MSeifert、karakfa 和 Thierry Lathuille 的回答中,这是 python 2.7 和 python 3.4 中最快的。事实上,接受的答案是最慢的,比这个答案(python 2.7)慢 40%。【参考方案3】:

这与其他答案非常相似,但您可以在 1000-9999 范围内绘制一个随机整数,而不是 sampleshuffle,直到您得到一个仅包含唯一数字的整数:

import random

val = 0  # initial value - so the while loop is entered.
while len(set(str(val))) != 4:  # check if it's duplicate free
    val = random.randint(1000, 9999)

print(val)

正如@Claudio 在 cmets 中指出的那样,该范围实际上只需要 1023 - 9876,因为该范围之外的值包含重复的数字。

通常random.randint 会比random.shufflerandom.choice 快得多,因此即使需要多次绘制(如@karakfa 所指出的那样),它也比任何shuffle 快3 倍, choice 的方法也需要join 个位数。

【讨论】:

@MSeifert 当然,这是 4 位数字。你的拒绝空间会爆炸并用 7 位数减慢你的速度(我知道这不是问题:))。但实际上,我最喜欢这个答案。 为了使其更加完美,您可以使用例如 random.randint(1023, 9876) 进一步限制选择范围,对吗? @DavidHammen 请注意,使用 python-3 的时间看起来非常不同:ideone.com/ous7zZ。我的解决方案似乎在 python2 上有点慢,但在 python3 上要快得多? @MSeifert - 我没有注意到 python3 标签。我已经对 python2 和 python3 进行了试验,并注意到 python3 的性能通常非常糟糕。你的是一个例外,在 python3 而不是 python2 中仅慢 10%(其他算法需要两倍或更长的时间)。两种语言版本的共同点是公认的答案很糟糕。 @MSeifert -- 即使你的算法在 python3 中也比在 python2 中慢一点。我在 ideone 上减少了number 的时间,以免过度滥用他们的计算机。扩大这个数字,很明显,python3 中的每个算法都比每个算法都慢。我怀疑python3极大地提高了lensetstr的性能,但不知何故降低了random的性能,特别注意降低了random.samplerandom.shuffle的性能.【参考方案4】:

我不是很了解 Python,但是类似

digits=[1,2,3,4,5,6,7,8,9] <- no zero
random.shuffle(digits)
first=digits[0] <- first digit, obviously will not be zero
digits[0]=0 <- used digit can not occur again, zero can
random.shuffle(digits)
lastthree=digits[0:3] <- last three digits, no repeats, can contain zero, thanks @Dubu

一个更有用的迭代,实际上是创建一个数字:

digits=[1,2,3,4,5,6,7,8,9]   # no zero
random.shuffle(digits)
val=digits[0]                # value so far, not zero for sure
digits[0]=0                  # used digit can not occur again, zero becomes a valid pick
random.shuffle(digits)
for i in range(0,3):
  val=val*10+digits[i]       # update value with further digits
print(val)

从其他解决方案中窃取片段后,加上来自@DavidHammen 的提示:

val=random.randint(1,9)
digits=[1,2,3,4,5,6,7,8,9]
digits[val-1]=0
for i in random.sample(digits,3):
  val=val*10+i
print(val)

【讨论】:

我不知道为什么这被否决了,它很短而且很组合。这正是我在阅读标题时想到的做法。在获得第一个数字后将first 设置为 0 是“聪明的”,我会在列表中添加零。这看起来与克劳迪奥的相同,后者是 tl;dr 且可读性较差。这可以生成所有 4536 个答案,而无需进行不必要的循环或测试。 这很好,除了它应该读取lastthree=digits[0:3],因为 Python 的不寻常的数组索引。 @toddkaufmann 我想先这样做,只是我不知道附加项目的语法,然后我意识到无论如何都不需要它 @tevemadar -- 虽然是正确的,但速度很慢,在一个九元素列表上两次调用 random.shuffle @DavidHammen 我不知道其他随机函数,例如 random.sample (尽管使用几乎相同的变量名称,但我没有发现后来接受的答案,还有更多失败的答案周围的尝试)。顺便添加第三个变体【参考方案5】:

[固定] 将所有四位数字移到一个位置是不正确的。用固定位置交换前导零也是不对的。但是前导零与九个位置中的任何一个随机交换是正确的,并且概率相等:

""" Solution: randomly shuffle all numbers. If 0 is on the 0th position,
              randomly swap it with any of nine positions in the list.

  Proof
    Lets count probability for 0 to be in position 7. It is equal to probability 1/10 
  after shuffle, plus probability to be randomly swapped in the 7th position if
  0 come to be on the 0th position: (1/10 * 1/9). In total: (1/10 + 1/10 * 1/9).
    Lets count probability for 3 to be in position 7. It is equal to probability 1/10
  after shuffle, minus probability to be randomly swapped in the 0th position (1/9)
  if 0 come to be on the 0th position (1/10) and if 3 come to be on the 7th position
  when 0 is on the 0th position (1/9). In total: (1/10 - 1/9 * 1/10 * 1/9).
    Total probability of all numbers [0-9] in position 7 is:
  9 * (1/10 - 1/9 * 1/10 * 1/9) + (1/10 + 1/10 * 1/9) = 1
    Continue to prove in the same way that total probability is equal to
  1 for all other positions.
    End of proof. """

import random
l = [0,1,2,3,4,5,6,7,8,9]
random.shuffle(l)
if l[0] == 0:
    pos = random.choice(range(1, len(l)))
    l[0], l[pos] = l[pos], l[0]
print(''.join(map(str, l[0:4])))

【讨论】:

但并非每个可能的值都以相同的概率绘制。因为例如123401234 产生相同的结果,而包含非前导零的数字(例如1023)只有一种可能被绘制。 @MSeifert 啊,你是对的。如果将零与列表中的随机位置交换,应该绘制一个以零为前导的数字吗?然后在这种情况下,我们在样本空间中均匀地重新分配。 例如random.shuffle(l); if l[0] == 0: pos = random.choice(range(1, 10)); l[0], l[pos] = l[pos], l[0] @MSeifert -- 让我们举个更简单的例子:列出 [0,1,2] 和两个不以 0 开头的唯一数字。有 3!==6 种可能性。每种可能性的概率为 1/6。有 2 个前导零:[0,1,2] 和 [0,2,1]。如果随机将前导 0 与其余 2 个位置交换,则有四种组合:[0,1,2]==>([1,0,2] 和 [2,1,0]) ; [0,2,1]==>([2,0,1] 和 [1,2,0])。这四种组合中的每一种都具有相同的概率,等于 1/6 * 1/2 == 1/12。但这 4 种组合与其余 4 种可能性相吻合。所以随机交换后的每种可能性应该有概率== 1/6 + 1/12。 @MSeifert -- 添加了数学证明。【参考方案6】:

拒绝抽样方法。从 10 位数字中创建一个 4 位数字随机组合,如果不符合条件则重新采样。

r4=0    
while r4 < 1000:
    r4=int(''.join(map(str,random.sample(range(10),4))))

注意到这与@Austin Haskings 的answer 基本相同

【讨论】:

这种情况的概率为零...您会惊讶于这种技术在实践中的普遍使用程度。 概率肯定为零。 lim 10^(-n) = 0;对于n -&gt; Infinity(又名永远) 这个概率在所有实际应用中都可以忽略不计。例如,循环 1000 次而不产生有效样本(在我的计算机上需要 16 毫秒)是概率 10^(-1000) 事件。 @BoundaryImposition - 你在这里叫错了树。拒绝抽样是一种应用非常广泛的技术。这个答案的唯一问题是奥斯汀哈斯金在两分钟前写了一个非常相似的答案(可能是在 karakfa 写这个答案的时候)并且奥斯汀哈斯金的答案要好一些。【参考方案7】:

你可以对 3 个数字使用全范围,然后在其余数字中选择前导数字:

import random
numbers = random.sample(range(0,10), 3)
first_number = random.choice(list(set(range(1,10))-set(numbers)))
print(''.join(map(str, [first_number]+numbers)))

如果需要重复选择(并且如果您在位数上保持合理),另一种方法是使用 itertools.permutations 预先计算可能输出的列表,过滤掉前导零的输出,并从中构建一个整数列表:

import itertools,random

l = [int(''.join(map(str,x))) for x in itertools.permutations(range(10),4) if x[0]]

这是一些计算时间,但之后你可以调用:

random.choice(l)

你想要多少次。它速度非常快,并且提供了均匀分布的随机性。

【讨论】:

除非允许的结果不太可能;例如,有两种方法可以得到 1230,但只有一种方法可以得到 1234。 @TimPeters 你是对的。添加了替代解决方案。 如果你得到0023,那么你有同样的问题 但您不能拥有0023。只有一个0【参考方案8】:

我不懂 Python,所以我将针对这个特定问题发布一个伪代码式的解决方案:

创建一个包含从 0 开始的数字列表的查找变量:

lu = [1, 2, 3, 4, 5, 6, 7, 8, 9]

生成四个从 0 开始的随机数,如下所示:

r1 = random number between 0 and 8
r2 = random number between 0 and 8
r3 = random number between 0 and 7
r4 = random number between 0 and 6

使用查找变量将随机数一一转换为数字。每次查找后,通过删除已使用的数字来改变查找变量:

d1 = lu[r1]
lu.remove(d1)
lu.insert(0)

d2 = lu[r2]
lu.remove(d2)

d3 = lu[r3]
lu.remove(d3)

d4 = lu[r4]
lu.remove(d4)

打印结果:

print concatenate(d1, d2, d3, d4)

可以稍微概括一下这个想法。例如,您可以创建一个接受列表(数字)和数字(所需的结果长度)的函数;该函数将返回数字并通过删除已用完的数字来改变列表。以下是此解决方案的 javascript 实现:

function randomCombination(list, length) 
    var i, rand, result = "";
    for (i = 0; i < length; i++) 
        rand = Math.floor(Math.random() * list.length);
        result += list[rand];
        list.splice(rand, 1);
    
    return result;


function desiredNumber() 
    var list = [1, 2, 3, 4, 5, 6, 7, 8, 9],
        result;
    result = randomCombination(list, 1);
    list.push(0);
    result += randomCombination(list, 3);
    return result;


var i;
for (i = 0; i < 10; i++) 
    console.log(desiredNumber());

【讨论】:

【参考方案9】:

我会这样做

while True:
    n = random.randrange(1000, 10000)
    if len(set(str(n))) == 4: # unique digits
        return n

更一般地,给定一个生成器,您可以使用内置的 filternext 来获取满足某些测试功能的第一个元素。

numbers = iter(lambda: random.randrange(1000, 10000), None) # infinite generator
test = lambda n: len(set(str(n))) == 4
return next(filter(test, numbers))

【讨论】:

return 在函数外是 python 中的语法错误。另请注意,您可以使用从10239876 的范围,正如here 所指出的那样。【参考方案10】:

将生成器与next 结合起来

Pythonic 的编写方式是使用 2 个嵌套生成器和 next

from random import randint
from itertools import count

print(next(i for i in (randint(1023, 9876) for _ in count()) if len(set(str(i))) == 4))
# 8756

它基本上是@MSeifert's answer 的单行变体

预处理所有可接受的数字

如果您需要许多随机数,您可以投入一些时间和内存来预处理所有可接受的数字:

import random    
possible_numbers = [i for i in range(1023, 9877) if len(set(str(i))) == 4]

10239877 用作边界,因为小于 1023 或大于 9876 的 int 不能有 4 个唯一的区别数字。

那么,您只需要random.choice 即可快速生成:

print(random.choice(possible_numbers))
# 7234

【讨论】:

【参考方案11】:

免责声明:这是一种糟糕的反 Python 方法,仅限于基准测试部分(请参阅 @DavidHammen 的 cmets 和 http://ideone.com/qyopLF) 这个想法是一步生成数字的序列号,然后修复任何冲突:

rnd=random.randint(0,4535)
(rnd,d1)=divmod(rnd,9)
(rnd,d2)=divmod(rnd,9)
#(rnd,d3)=divmod(rnd,8)
#(rnd,d4)=divmod(rnd,7)
(d4,d3)=divmod(rnd,8) # miracle found: 1 divmod happens to run faster than 2

现在我们有 d1=0..8, d2=0..8, d3=0..7, d4=0..6,可以通过运行 sn-p 来测试 rnd=4535 (4535 =9*9*8*7-1,顺便说一下)

首先,必须修补 d1

d1=d1+1 # now d1 = 1..9

如果需要,d2 必须“跳过”d1

if d2>=d1
  d2=d2+1 # now d2 = 0..9 "-" d1

那么剩下的数字也要做同样的事情,很快就会变得丑陋:

if d3>=d1:
  d3=d3+1    # now d3 = 0..8 "-" d1
  if d3>=d2:
    d3=d3+1  # now d3 = 0..9 "-" d1,d2
elif d3>=d2: # this branch prepares for the other variant
  d3=d3+1
  if d3>=d1: # ">=" is preserved for consistency, here "==" may occur only
    d3=d3+1

最后一部分是灾难性的:

if d4>=d1:
  d4=d4+1
  if d4>=d2:
    d4=d4+1
    if d4>=d3:
      d4=d4+1
  elif d4>=d3:
    d4=d4+1
    if d4>=d2:
      d4=d4+1
elif d4>=d2:
  d4=d4+1
  if d4>=d1:
    d4=d4+1
    if d4>=d3:
      d4=d4+1
  elif d4>=d3:
    d4=d4+1
    if d4>=d1:
      d4=d4+1
elif d4>=d3:
  d4=d4+1
  if d4>=d2:
    d4=d4+1
    if d4>=d1:
      d4=d4+1
  elif d4>=d1:
    d4=d4+1
    if d4>=d2:
      d4=d4+1

对于更长的数字,使用位域可能会更快,但我没有看到一个简单的方法。 (检查 >= 关系一次是不够的,因为在进行增量后很容易发生冲突。 例如d1=1, d2=2, d3=1: d3 与 d1 碰撞,但最初不与 d2 碰撞。然而,在 1 处“打洞”后,d3 变为 2,现在它与 d2 发生碰撞。没有简单的方法可以提前发现这种碰撞)

由于代码臭得要命,我在最后放了一个验证步骤

val = d1*1000 + d2*100 + d3*10 + d4
#if len(set(str(val))) != 4: print(str(val)+" "+str(o1)+","+str(o2)+","+str(o3)+","+str(o4))
if len(set(str(val))) != 4: print(val)

它已经比其他真正快速的代码快了(注释验证显示了在 divmod-s 之后保留的原始数字,用于调试目的。这不是那种立即起作用的代码......)。评论这两个验证可以加快速度。

编辑:关于检查这个和那个

这是一种在有效输入 (0...4535) 和有效输出(9*9*8*7 可能的具有不同数字的 4 位数字,而不是 -从-0 开始)。所以一个简单的循环可以并且应该生成所有的数字,它们可以被一个一个地检查,并且它们可以被收集到一个集合中,例如,看看它们是否都是不同的结果

实际上:

collect=set()
for rnd in range(0,4536):
    (rnd,d1)=divmod(rnd,9)
    ... rest of the code, also the verification step kept active ...
    collect.add(val)
print(len(collect))

1) 它不会在循环中打印任何内容(所有结果都是具有不同数字的 4 位数字)

2) 最后会打印 4536(所有结果都是不同的)

可以为第一个数字 (d1) 添加验证,此时我只是假设"(something mod 9)+1" 不会是 0。

【讨论】:

有趣。你检查结果是否均匀分布? @EricDuminil 有效输出集的分布遵循 randint() 的分布。我检查的是将输入映射到输出的正确性,现在添加了一些关于它的行【参考方案12】:

这将允许在第一个数字之后出现零 -

numbers = random.sample(range(1,10),1) + random.sample(range(10),3)

【讨论】:

它也将允许单个重复数字。 是否需要将第一个数字乘以 1000,然后才能将两者相加?还是加号只是将它们连接起来?

以上是关于如何生成一个不以 0 开头且具有唯一数字的随机 4 位数字?的主要内容,如果未能解决你的问题,请参考以下文章

生成唯一且随机的整数

如何在Java程序中写一个方法可随机生成12位数字,且永远不会重复,唯一的。

生成具有概率的随机整数

如何做一个随机且唯一的生成器?

生成订单号,要求是唯一的,如何实现?

在 0 和 'x' 之间生成唯一的随机数(整数)