Python で素数のリストを得る方法 エラトステネスの篩


3 つの書き方を比較検討しました。

項番 項目 時間
1 簡単なもの 0.9157630139998219
2 高速化 0.16211261399985233
3 集合 0.5873824189993684

計測に使用したコード sieve_of_eratosthenes.py


1. 簡単なもの

def primes(n):
    is_prime = [True] * (n + 1)
    is_prime[0] = False
    is_prime[1] = False
    for i in range(2, n + 1):
        for j in range(i * 2, n + 1, i):
            is_prime[j] = False
    return [i for i in range(n + 1) if is_prime[i]]


参考にしたサイトは、下記のサイトから抜粋しました。
「ディビジョン・サム」問題解説|CodeIQ MAGAZINE


2. 高速版

5 倍くらい早くなる。

def primes(n):
    is_prime = [True] * (n + 1)
    is_prime[0] = False
    is_prime[1] = False
    for i in range(2, int(n**0.5) + 1):
        if not is_prime[i]:
            continue
        for j in range(i * 2, n + 1, i):
            is_prime[j] = False
    return [i for i in range(n + 1) if is_prime[i]]


for 文の方が while 文よりも速い気配があるので、for 文で書きました。その検証は下のサイトでやりました。for 文の方が綺麗に書けますしね。素因数分解するときは while 文の方が早かったです。なぜかは、わかりません。



3. 順序付き集合(OrderedDict で代用)

しかしそこまで速くならず、簡単なものより 1.5 倍速いくらい。
アルゴリズム的には一番速いはずだけど。

import collections


def primes_ordered_dict(n):
    #
    prime_list = []
    non_prime_list = []
    number_dict = collections.OrderedDict(
        ((i, None) for i in range(n + 1)))
    #
    del number_dict[0]
    del number_dict[1]
    while True:
        # get prime
        prime, _ = number_dict.popitem(False)
        prime_list.append(prime)
        # break
        if prime * prime > n:
            break
        # get non primes
        non_prime_list.append(prime * prime)
        for k in number_dict:
            if prime * k <= n:
                non_prime_list.append(prime * k)
            else:
                break
        # delete non primes
        for non_prime in non_prime_list:
            del number_dict[non_prime]
        non_prime_list.clear()

    # add rest of numbers as prime
    prime_list.extend(
        [prime for prime, _ in number_dict.items()])

    return prime_list


2. 高速版のアルゴリズムでも
既に素数でないと判定されたものにも複数回、
素数でないと判定しています。
is_prime[j] = False を実行しています。

例えば 3 * 4, 3 * 6, 3 * 8 のような数です。
素数 * 既に素数と判定された数の倍数

そこで順序付き集合を用いて
同じ処理を繰り返し行わないようにしたいと考えた。

Python の set は Ordered, 順序付きではないので
OrderedDict で代用した。

素数の判定を重複して行いません。1度だけしか行いません。
del number_dict[non_prime] を実行しています。

しかしそこまで速くならず、簡単なものより 2 倍速いくらい。
アルゴリズム的には一番速いはずだけど。

Python は、四則演算やらなんやらが遅いので
何が原因かはいまいちよくわからない。
Python を高速化したい

やったことないですが OrderedDict に該当するモジュールを
C 言語で書き直して比較したら、また違うのかなと思ったりもします。
1. C や C++ による Python の拡張
100万倍速いプログラムを書く | Qiita

その時は for 文の中でも del number_dict[non_prime]
できるようにしようかな。
そしたら Python 側のコードはもっと綺麗になる。

できいないので上のコードでは non_prime_list を用いて
一時的に素数ではない数をリストとして保存している。

(番外編)ワンライナー素数リストを得る

これはエラトステネスの篩では、ありません。速度的には 1 より少し遅いくらいでした。自分のパソコンだと i=3 を代入すると計算が終わらない。しかし、こんなやり方、思いつくなんて凄すぎる...。Twitter の "だよ〜" ってコメントなんだよ、軽すぎるよ (´;ω;`)ブワッ

# 素数リスト 2**i 桁以下
primes = lambda i: primes(i - 1) + [k for k in range(10**2**(i-1), 10**2**i) if all(k % p for p in primes(i - 1))] if i else [2, 3, 5, 7]
# 素数リスト 2**i 桁以下
def primes(i):
    if i:
        # 素数リスト 2**(i-1) 桁以下
        L = primes(i - 1)

        # 素数リスト 2**(i-1)+1 桁以上 2**i 桁以下
        n = range(10**2**(i-1), 10**2**i)
        M = [k for k in n if all(k % p for p in L)]

        # 素数リスト 2**i 桁以下
        return L + M
    else:
        # 素数リスト 2**0 桁以下
        return [2, 3, 5, 7]

pythonで素数表と素数判定するコードだよ~ - にんㄘ゛ん


◯ ポイント

def primes(i):
        ...
        # 素数リスト 2**(i-1)+1 桁以上 2**i 桁以下
        n = range(10**2**(i-1), 10**2**i)
        M = [k for k in n if all(k % p for p in L)]
        ...


2**(i-1)+1 桁以上 2**i 桁以下の合成数素数でない数)は、 2**(i-1) 桁以下の素数を素因数に持ちます。逆に言えば、2**(i-1)+1 桁以上 2**i 桁以下の合成数素数でない数)は、2**(i-1)+1 桁以上の素数を素因数に持ちません。

2 桁の合成数は、1 桁以下の素因数を持ちます。3 桁以上4 桁以下の合成数は、2桁以下の素因数を持ちます。5 桁以上 8 桁以下の合成数は、4 桁以下の素因数を持ちます。

例えば 2 桁の合成数で 1 桁の素因数を持たない数を考えて見てください。存在しません。逆に言えば、例えば 2 桁の素数を使って 2 桁の合成数を作って見てください。作ることはできません。

これは、任意の 2 桁の素数 p, q について考えると、必ず p * q >= 100 となるからです。