Disclaimer: This review will mention improvements to your code that have probably been provided by other answers. At the end, I will suggest a completely different algorithm to solve this problem.
Better usage of the sieve
Using a sieve is a good idea especially because we have a known upper-bound for the values we are interested in. However, the conversion from the sieve into a list makes it slower to use: it makes all other prime checks expensive as one need to perform a linear search in the list. A much faster alternative is to use it as is: an array mapping number to their primality.
In practice, you just need to return nums
from the sieve function and then:
def circular_prime(n):
cp = []
s = sieve(n)
for i, is_prime in enumerate(s):
if is_prime:
r = rotations(i)
flags = [False] * len(r)
for k, j in enumerate(r):
if s[j]:
flags[k] = True
if all(flags):
print(i)
cp.append(i)
return len(cp)
This simple change leads to a huge performance improvement.
Flags
The logic involving flags can be simplified a lot and be: if all(s[j] for j in rotations(i)):
.
Perform a smaller number of prime test
For every circular number found, we check all its rotations. Eventually, all numbers have been checked many times. A different strategy could be to check whether the number we are considering is the smallest among its rotations. If it is and if it is indeed a circular primes, all its rotations can be added to the result at once.
r = set(rotations(i))
if i == min(r) and all(s[j] for j in r):
print(r)
cp.extend(r)
A different algorithm
A few mathematical observations can be performed for the number (bigger than 9) we are looking for:
primes numbers (bigger than 9) will not end in 0, 2, 4, 6, 8 or 5. This leads to a last digit being 1, 3, 7, 9.
thus, circular primes will not contain any of these numbers because they would correspond to the last digit of a rotation. Circular primes are only permutations of 1, 3, 7 and 9.
We can limit the search space by looking for these permutations: when considering numbers with n digits, you'll only consider 4ⁿ numbers instead of 10ⁿ (when n = 6 for instance, it makes the difference between 4096 and 100000).
We get something like:
def circular_prime(nb_dig_max):
cp = [2, 3, 5, 7]
final_numbers = {'1', '3', '7', '9'}
s = sieve(10 ** nb_dig_max)
for l in range(2, nb_dig_max + 1):
for p in itertools.product(final_numbers, repeat=l):
p_int = int(''.join(p))
perm = set(rotations(p_int))
if p_int == min(perm) and all(s[n] for n in perm):
cp.extend(perm)
return len(cp)
At this stage, we've limited the number of prime checks to such a small number that using a sieve leads to performance no better than a simple prime check function:
def is_prime(n):
"""Checks if a number is prime."""
if n < 2:
return False
return all(n % i for i in range(2, int(math.sqrt(n)) + 1))
def circular_prime(nb_dig_max):
cp = [2, 3, 5, 7]
final_numbers = {'1', '3', '7', '9'}
for l in range(2, nb_dig_max + 1):
for p in itertools.product(final_numbers, repeat=l):
p_int = int(''.join(p))
perm = set(rotations(p_int))
if p_int == min(perm) and all(is_prime(n) for n in perm):
cp.extend(perm)
return len(cp)