How to generate a random 4 digit number not starti

2019-03-11 15:11发布

问题:

This works almost fine but the number starts with 0 sometimes:

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

I've found a lot of examples but none of them guarantee that the sequence won't start with 0.

回答1:

We generate the first digit in the 1 - 9 range, then take the next 3 from the remaining digits:

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

The generated numbers are equiprobable, and we get a valid number in one step.



回答2:

Just loop until you have something you like:

import random

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

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


回答3:

This is very similar to the other answers but instead of sample or shuffle you could draw a random integer in the range 1000-9999 until you get one that contains only unique digits:

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)

As @Claudio pointed out in the comments the range actually only needs to be 1023 - 9876 because the values outside that range contain duplicate digits.

Generally random.randint will be much faster than random.shuffle or random.choice so even if it's more likely one needs to draw multiple times (as pointed out by @karakfa) it's up to 3 times faster than any shuffle, choice approach that also needs to join the single digits.



回答4:

I do not know Python well, but something like

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

A more useful iteration, actually creating a number:

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)

After stealing pieces from other solutions, plus applying the tip from @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)


回答5:

[Fixed] Shift all four digits on one position is not right. Swap leading zero with fixed position is not right too. But random swap of the leading zero with any of nine positions is correct and gives equal probability:

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


回答6:

rejection sampling method. Create a 4 digit random combination from 10 digits and resample if it doesn't match the criteria.

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

noticed that this is essentially the same as @Austin Haskings's answer



回答7:

You could use full range for 3 numbers, then choose the leading number among the remaining numbers:

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

Another way if the choice needs to be repeated (and if you remain reasonable on the number of digits), is to pre-compute the list of the possible outputs using itertools.permutations, filtering out the ones with a leading zero, and building a list of integers from it:

import itertools,random

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

That's some computation time, but after than you can call:

random.choice(l)

as many times you want. It's very fast and provides an evenly distributed random.



回答8:

I don't know Python so I will post a pseudo-code-ish solution for this specific problem:

  • Create a lookup variable containing a 0-based list of digits:

    lu = [1, 2, 3, 4, 5, 6, 7, 8, 9]
    
  • Generate four 0-based random numbers as follows:

    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
    
  • Use the lookup variable to convert random numbers to digits one-by-one. After each lookup, mutate the lookup variable by removing the digit that has been used:

    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 the result:

    print concatenate(d1, d2, d3, d4)
    

It is possible to generalize this idea a little. For example you can create a function that accepts a list (of digits) and a number (desired length of result); the function will return the number and mutate the list by removing used-up digits. Below is a JavaScript implementation of this solution:

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:

Here's how I'd do it

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

More generally, given a generator, you can use the built-ins filter and next to take the first element that satisfies some test function.

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


回答10:

Combine generators with next

A Pythonic way to write would be to use 2 nested generators and 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

It's basically a one-liner variant of @MSeifert's answer

Preprocess all the acceptable numbers

If you need many random numbers, you could invest some time and memory for preprocessing all the acceptable numbers:

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

1023 and 9877 are used as boundaries because no int lower than 1023 or greater than 9876 can have 4 unique, distince numbers.

Then, you'd just need random.choice for a very fast generation:

print(random.choice(possible_numbers))
# 7234


回答11:

Disclaimer: this is a terrible anti-Python approach, strictly for the benchmarking part (see @DavidHammen's comments around, and http://ideone.com/qyopLF) The idea is to generate the sequence numbers of the digits in one step, and then fix any collisions:

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

Now we have d1=0..8, d2=0..8, d3=0..7, d4=0..6, it can be tested via running the snippet with rnd=4535 (4535=9*9*8*7-1, by the way)

First, d1 has to be patched up

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

Then d2 has to "skip" d1 if necessary

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

Then the same has to be done with the remaining digits, getting ugly fast:

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

And the final part is the catastrophic one:

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

For longer numbers, it might work faster with bitfields, but I do not see a trivial way. (Checking the >= relations once is not enough, because the collision can easily occur after doing an incrementation. e.g. d1=1, d2=2, d3=1: d3 collides with d1, but it does not collide with d2 initially. However after "puching the hole" at 1, d3 becomes 2 and now it collides with d2. There is no trivial way to spot this collision in advance)

As the code stinks as hell, I put a verification step at the end

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)

It is already faster than the other really fast code (the commented verification displayed the original digits preserved after the divmod-s, for debugging purposes. This is not the kind of code which works immediately...). Commenting both verifications makes it even faster.

EDIT: about checking this and that

This is an approach maintaining an 1:1 relation between the minimal set of valid inputs (0...4535) and valid outputs (the 9*9*8*7 possible 4-digit numbers with distinct digits, not-starting-with-0). So a simple loop can and should generate all the numbers, they can be checked one-by-one and they can be collected into a set for example in order to see if they are all distinct results

Practically:

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) It will not print anything in the loop (all results are 4-digit numbers with distinct digits)

2) It will print 4536 at the end (all results are distinct)

One can add a verification for the first digit (d1), here and now I just assume that
"(something mod 9)+1" will not be 0.



回答12:

This will allow zeros after the first digit -

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