Reputation: 155
I am unsure if this is one of those problems that is impossible or not, in my mind it seems like it should be possible. Edit - We more or less agree it is impossible to do so randomly, but psuedo-random is possible
Given a range specified by two integers (i.e. n1 ... n2
), is it possible to create a python generator that yields a random integer from the range WITHOUT repetitions and WITHOUT loading the list of options into memory (i.e. list(range(n1, n2))
).
Expected usage would be something like this:
def random_range_generator(n1, n2):
...
gen = random_range_generator(1, 6)
for n in gen:
print(n)
Output:
4
1
5
3
2
Upvotes: 2
Views: 209
Reputation: 17506
I thought it was impossible to do this without any kind of recordkeeping. Then I found this stack overflow answer which describes a C++ implementation of a parametrizable sequence that is a covering of the natural numbers but quasi-random.
We create a sequence using a prime number and one of its primitive roots modulo n that visits each number in an interval exactly once. We have to pick our prime number a little larger than the product len(a)*len(b)
, so we have to account for the cases in which we'd get index errors.
import random
from math import gcd
def next_prime(number):
if number < 0:
raise ValueError('Negative numbers can not be primes')
# Base case
if number <= 1:
return 2
# if even go back 1
if number % 2 == 0:
number -= 1
while True:
# only odds
number += 2
#only need to check up to and including the sqrt
max_check = int(math.sqrt(number))+2
# don't need to check even numbers
for divider in range(3, max_check, 2):
# if 'divider' divides 'number', then 'number' is not prime
if number % divider == 0:
break
# if the for loop didn't break, then 'number' is prime
else:
return number
def is_primitive_root(a, n):
phi = n - 1
factors = set()
for i in range(2, int(phi ** 0.5) + 1):
if phi % i == 0:
factors.add(i)
factors.add(phi // i)
for factor in factors:
if pow(a, factor, n) == 1:
return False
return True
def find_random_primitive_root(n):
candidates = list(range(2,n))
random.shuffle(candidates)
for a in candidates:
if gcd(a, n) == 1 and is_primitive_root(a, n):
return a
def advance_state(state, close_prime, root):
# This walks the entire space without repetition
state = (state * root) % close_prime
return state
def sampler(l):
close_prime = next_prime(l)
state = root = find_random_primitive_root(close_prime)
while state > l:
state = advance_state(state, close_prime, root)
yield state - 1
for i in range(l - 1):
state = advance_state(state, close_prime, root)
while state > l:
state = advance_state(state, close_prime, root)
yield state - 1
def _random_order(sequence):
sequence = sampler(len(sequence))
for state in sequence:
yield state
>>> list(random_order(list(range(20))))
[14, 17, 16, 1, 6, 12, 10, 3, 13, 2, 7, 4, 5, 15, 9, 11, 18, 8, 19, 0]
Upvotes: 2
Reputation: 179
This is simple enough to do, keeping it in memory. Seems to me impossible otherwise:
import random
def random_range_generator(n1, n2):
r = list(range(n1, n2))
random.shuffle(r)
yield from r
gen = random_range_generator(1, 6)
for n in gen:
print(n)
Upvotes: 4
Reputation: 8163
How can we shuffle something?
The Idea:
unsorted = [(random(), x) for x in range(n1,n2) ]
sorted = sort(unsorted, key = lambda x : x[0])
result = [p[1] for p in sorted]
Note: I haven't tested this but you get the idea. This is a useful method that can be applied/adapted also to similar problems of reordering one thing, based on the ordering of another list.
Upvotes: 3