【素数で遊ぼう】C言語で割り算からエラトステネスのふるいまで|定番問題をコードパズルにしてみた | UNIX Cafe

* 当サイトでは、コンテンツの一部に広告を掲載しています。

System Note $ cat /proc/ai-disclosure

本記事の構成および論理分析にはAI(人工知能)を使用しています。情報の正確性は、システム管理者(UNIXユーザー)による手動検証済みです。

【素数で遊ぼう】C言語で割り算からエラトステネスのふるいまで|Python版も補足で紹介 | UNIX Cafe

プログラミングを学んでいると、何度も出てくる数に 素数 があります。

素数は、235711 のように、1と自分自身でしか割り切れない整数です。言葉だけ見ると数学っぽいですが、コードにすると「割り切れるかどうかを試す」小さなパズルになります。

今回は、この素数を C言語 で遊びながら見ていきます。関数、for文、余り演算、配列を使いながら、まずは全部の割り算を試すところから始めて、最後はエラトステネスのふるいまで進みます。

最後に、同じ考え方を Python で書くとどう表せるかも補足します。この記事ではC言語を主役にして、Pythonは見比べるための別表現として扱います。

目次

素数ってなに?

素数は、約数が 1 と自分自身だけの整数です。

素数?理由
1いいえ素数ではない特別な数
2はい1と2でしか割り切れない
3はい1と3でしか割り切れない
4いいえ2で割り切れる
5はい1と5でしか割り切れない
9いいえ3で割り切れる

ここで大事なのは、1は素数ではない という点です。

「1と自分自身で割り切れるなら、1も素数では?」と思いたくなりますが、現在の数学では 1 は素数に入れません。素数を使った分解のルールをきれいに保つためです。

素数判定の基本ルール

今回のルールを、コードで考えやすい形にするとこうなります。

  • 1 以下は素数ではない
  • 2 以上の数を調べる
  • 途中で割り切れる数が見つかったら素数ではない
  • 最後まで割り切れなければ素数

C言語では、割り切れるかどうかを % で調べられます。

n % i == 0

これは「ni で割った余りが 0」という意味です。つまり、きれいに割り切れます。

その1:まずは全部割ってみる

まずは一番わかりやすい方法から始めます。

n が素数かどうかを調べるには、2 から n - 1 までで割ってみます。途中で割り切れたら、その数は素数ではありません。

prime_basic.c という名前で、次のコードを作ってみます。

#include <stdio.h>

int is_prime(int n) {
    if (n <= 1) {
        return 0;
    }

    for (int i = 2; i < n; i++) {
        if (n % i == 0) {
            return 0;
        }
    }

    return 1;
}

int main(void) {
    int numbers[] = {1, 2, 3, 4, 5, 9, 11, 15, 17, 21};
    int size = sizeof(numbers) / sizeof(numbers[0]);

    for (int i = 0; i < size; i++) {
        if (is_prime(numbers[i])) {
            printf("%d: prime\n", numbers[i]);
        } else {
            printf("%d: not prime\n", numbers[i]);
        }
    }

    return 0;
}

このコードでは、素数なら 1、素数でなければ 0 を返す関数を作っています。

試す数字を変えてみる

int numbers[] = {1, 2, 3, 4, 5, 9, 11, 15, 17, 21}; の部分は、素数と素数でない数が混ざるように選んだサンプルです。

67810 などを飛ばしていることに、特別な数学的な理由はありません。出力が長くなりすぎないように、いくつか抜き出しています。

自分で試したい場合は、この配列の中身を自由に変えて大丈夫です。

int numbers[] = {6, 7, 8, 10, 13, 19, 25, 31};

数字を増やしたり、順番を変えたりしても動きます。配列の個数は自動的に数えているので、size を手で直す必要はありません。

sizeof で配列の個数を数える

int size = sizeof(numbers) / sizeof(numbers[0]); は、配列 numbers に入っている要素の個数を数えています。

C言語では、配列に「何個入っているか」を直接教えてくれる機能がありません。そこで、配列全体のサイズを、配列の1個分のサイズで割ります。

sizeof(numbers)     // 配列全体のサイズ
sizeof(numbers[0])  // 配列の1個分のサイズ

つまり、考え方はこうです。

配列全体のサイズ / 1個分のサイズ = 要素の個数

たとえば int が4バイトの環境で、numbers に10個の数字が入っているなら、配列全体は 40 バイト、1個分は 4 バイトです。

40 / 4 = 10

これで size には 10 が入ります。そのため、for (int i = 0; i < size; i++) と書けば、配列の最後まで順番に調べられます。

return 0 と return 1 の使い方

C言語には、Python の TrueFalse のような書き方もありますが、ここではシンプルに 1 を「はい」、0 を「いいえ」として使っています。

  • return 1; は「素数です」
  • return 0; は「素数ではありません」

このくらいの小さな関数なら、条件判定の練習にもなります。

コンパイルと実行

macOS のターミナルなら、次のようにコンパイルできます。

gcc -std=c99 prime_basic.c -o prime_basic

実行します。

./prime_basic

実行結果はこうなります。

1: not prime
2: prime
3: prime
4: not prime
5: prime
9: not prime
11: prime
15: not prime
17: prime
21: not prime

その2:平方根まで調べれば十分

さきほどのコードはわかりやすいのですが、少し無駄があります。

たとえば 100 が素数かどうかを調べるとき、2 で割り切れることがすぐにわかります。もし割り切れる数があるなら、片方の約数は必ず平方根以下に出てきます。

そこで、ループの条件を i < n から i * i <= n に変えます。

for (int i = 2; i * i <= n; i++)

sqrt 関数を使ってもよいのですが、今回は i * i <= n と書くことで、数学関数を使わずに済ませます。

36の約数で折り返しを見てみる

なぜ平方根まででよいのかは、36 の約数ペアを見るとわかりやすいです。

約数のペア見方
2 * 18 = 36小さい側は2
3 * 12 = 36小さい側は3
4 * 9 = 36小さい側は4
6 * 6 = 36ここが折り返し地点
9 * 4 = 364 * 9 の逆向き
12 * 3 = 363 * 12 の逆向き
18 * 2 = 362 * 18 の逆向き

6 * 6 = 366 が、36 の平方根です。ここを超えると、91218 のような大きい約数は、すでに出てきた 432 とペアになっています。

つまり、平方根を超えたところに新しい小さい約数は出てきません。だから、i * i <= n まで調べれば十分です。

たとえば 17 を調べる場合、全部試す方法なら 2 から 16 まで確認します。一方、i * i <= 17 なら、調べるのは 234 までです。5 * 5 = 2517 を超えるので、そこでループを抜けます。

今度は、1 から 100 までの素数を表示してみましょう。prime_list.c という名前で作ります。

#include <stdio.h>

int is_prime(int n) {
    if (n <= 1) {
        return 0;
    }

    for (int i = 2; i * i <= n; i++) {
        if (n % i == 0) {
            return 0;
        }
    }

    return 1;
}

int main(void) {
    for (int n = 1; n <= 100; n++) {
        if (is_prime(n)) {
            printf("%d ", n);
        }
    }

    printf("\n");
    return 0;
}

コンパイルします。

gcc -std=c99 prime_list.c -o prime_list

実行します。

./prime_list

実行結果はこうなります。

2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 

ここでは、素数を空白区切りで横に並べています。

その3:双子素数を探してみる

素数を判定できるようになると、少し遊び方を変えられます。

たとえば、差が 2 の素数のペアは 双子素数 と呼ばれます。

  • 35
  • 57
  • 1113

前に見つけた素数を覚えておいて、次の素数との差が 2 なら表示します。twin_prime.c という名前で作ります。

#include <stdio.h>

int is_prime(int n) {
    if (n <= 1) {
        return 0;
    }

    for (int i = 2; i * i <= n; i++) {
        if (n % i == 0) {
            return 0;
        }
    }

    return 1;
}

int main(void) {
    int previous = 2;

    for (int n = 3; n <= 100; n++) {
        if (is_prime(n)) {
            if (n - previous == 2) {
                printf("(%d, %d)\n", previous, n);
            }
            previous = n;
        }
    }

    return 0;
}

main関数の読み方

この main 関数では、previous に「前に見つけた素数」を入れておきます。

int previous = 2;

最初の素数は 2 なので、まず previous2 を入れています。そのあと、3 から 100 まで順番に調べます。

for (int n = 3; n <= 100; n++)

is_prime(n) が本当だったときだけ、つまり n が素数だったときだけ、前の素数との差を見ます。

if (n - previous == 2)

差が 2 なら、previousn は双子素数です。そのため、printf でペアとして表示します。

最後に、今見つけた素数 nprevious に入れ直します。

previous = n;

これで、次の素数が見つかったときに「ひとつ前の素数」と比べられます。双子素数探しでは、この previous がメモの役割をしています。

コンパイルします。

gcc -std=c99 twin_prime.c -o twin_prime

実行します。

./twin_prime

実行結果はこうなります。

(3, 5)
(5, 7)
(11, 13)
(17, 19)
(29, 31)
(41, 43)
(59, 61)
(71, 73)

ここでは 100 までしか調べていません。100010000 に変えると、見つかるペアが増えます。

その4:エラトステネスのふるい

最後に、少し有名な方法を見てみます。エラトステネスのふるい です。

これは、最初に「全部素数かもしれない」と考えておき、あとから倍数を消していく方法です。

  • 2 の倍数を消す
  • 3 の倍数を消す
  • 5 の倍数を消す
  • 消されずに残った数が素数

同じような割り算を何度もする代わりに、表に印をつけていくイメージです。

prime_sieve.c という名前で作ります。

#include <stdio.h>

#define LIMIT 100

int main(void) {
    int is_prime[LIMIT + 1];

    for (int i = 0; i <= LIMIT; i++) {
        is_prime[i] = 1;
    }

    is_prime[0] = 0;
    is_prime[1] = 0;

    for (int p = 2; p * p <= LIMIT; p++) {
        if (is_prime[p]) {
            for (int multiple = p * p; multiple <= LIMIT; multiple += p) {
                is_prime[multiple] = 0;
            }
        }
    }

    for (int n = 1; n <= LIMIT; n++) {
        if (is_prime[n]) {
            printf("%d ", n);
        }
    }

    printf("\n");
    return 0;
}

main関数の読み方

この main 関数では、is_prime という配列を使って、数ごとに「素数かもしれない」という印を持たせています。

int is_prime[LIMIT + 1];

LIMIT100 なので、is_prime[0] から is_prime[100] まで使える配列を作っています。LIMIT + 1 にしているのは、添字 100 まで使いたいからです。

最初は、すべての数を「素数かもしれない」と考えて、配列の中身を 1 にします。

for (int i = 0; i <= LIMIT; i++) {
    is_prime[i] = 1;
}

ただし、01 は素数ではありません。ここは先に 0 にしておきます。

is_prime[0] = 0;
is_prime[1] = 0;

次のループが、エラトステネスのふるいの中心です。

for (int p = 2; p * p <= LIMIT; p++)

p がまだ素数候補として残っているなら、その倍数を消していきます。消す、というのは is_prime[multiple] = 0; にすることです。

for (int multiple = p * p; multiple <= LIMIT; multiple += p) {
    is_prime[multiple] = 0;
}

multiple += p は、p の倍数を順番に進むための書き方です。たとえば p3 なら、9121518 のように進みます。

最後に、まだ 1 のまま残っている数を表示します。これが、ふるいにかけたあとに残った素数です。

コンパイルします。

gcc -std=c99 prime_sieve.c -o prime_sieve

実行します。

./prime_sieve

実行結果は、さきほどの prime_list.c と同じです。

2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97 

4つの方法を比べてみる

方法特徴向いている場面
全部割る一番わかりやすい素数判定の最初の練習
i * i <= n まで調べる無駄な割り算を減らせる1つの数を調べるとき
双子素数を探す素数判定を使って遊べる条件を変えて試す練習
エラトステネスのふるい倍数をまとめて消すたくさんの素数を探すとき

最初から一番速い方法だけを覚えるより、まずは全部試すコードを書いてから、少しずつ無駄を減らしていくと理解しやすいです。

C言語で試すときの注意点

  • for (int i = ...) の書き方を使うので、gcc -std=c99 を付けてコンパイルすると安心
  • 1 は素数ではないので、最初に n <= 1 をチェックする
  • i * i <= n は便利だが、とても大きな数では整数の範囲にも注意する
  • LIMIT を大きくしすぎると配列も大きくなるので、まずは 1001000 で試す

改造して遊ぶポイント

素数のコードは、値を少し変えるだけで遊び方が増えます。

  • 100 までではなく 500 まで調べる
  • 双子素数ではなく、差が 4 の素数ペアを探す
  • 素数が何個あったかを数える
  • 一番大きな素数だけを表示する

「条件を1つ変えると、結果がどう変わるか」を見るのが、この手のコードパズルの楽しいところです。

Pythonではどう書ける?

ここまでC言語で見てきた素数判定は、Pythonでも同じ考え方で書けます。Pythonでは TrueFalse をそのまま返せるので、戻り値の意味が少し読みやすくなります。

まず、平方根まで調べる素数判定は次のように書けます。

def is_prime(n):
    if n <= 1:
        return False

    i = 2
    while i * i <= n:
        if n % i == 0:
            return False
        i += 1

    return True


for n in range(1, 21):
    if is_prime(n):
        print(n, end=" ")

エラトステネスのふるいも、リストを使うと短く書けます。

def sieve(limit):
    is_prime = [True] * (limit + 1)
    is_prime[0] = False
    is_prime[1] = False

    p = 2
    while p * p <= limit:
        if is_prime[p]:
            for multiple in range(p * p, limit + 1, p):
                is_prime[multiple] = False
        p += 1

    return is_prime


limit = 100
prime_flags = sieve(limit)

for n in range(2, limit + 1):
    if prime_flags[n]:
        print(n, end=" ")

C言語版とPython版の違い

同じ素数判定のコードでも、C言語では配列や整数の扱いを意識しながら書きます。一方、Pythonではリストや真偽値を使って、同じ流れを少し短く表せます。

観点C言語Python
真偽値10 を返して判定することが多いTrueFalse を返せる
配列int is_prime[LIMIT + 1] のようにサイズを意識する[True] * (limit + 1) でリストを作れる
繰り返しfor文で初期値、条件、増分を書くrange()while で短く書ける
学びやすい点配列や整数の扱いが見えやすい素数判定の流れを短いコードで確認しやすい

このシリーズでは、まずC言語で仕組みを丁寧に扱い、必要に応じてPythonでどう書けるかを補足していきます。Python版を別の記事として分けるよりも、同じ記事の中で比較したほうが、コードの違いが見えやすくなるからです。

まとめ:素数は割り算で遊べるコードパズル

今回は、C言語で素数を調べるコードを書きました。

  • 素数は、1と自分自身でしか割り切れない数
  • 1 は素数ではない
  • % を使うと、割り切れるかどうかを調べられる
  • i * i <= n まで調べれば、無駄な割り算を減らせる
  • たくさんの素数を探すなら、エラトステネスのふるいも使える
  • Pythonでは同じ考え方を短く書けるので、処理の流れを確認しやすい

FizzBuzz で使った % は、素数判定でも主役になります。倍数を見つける、割り切れるか調べる、条件を変えて結果を見る。ここまでできると、ただの割り算が立派なプログラミングの遊び道具になります。

C言語の次にUnityやUnreal Engineを学ぶ選択肢

用途ごとに選ぶ C言語のおすすめ本

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

のいのアバター のい UNIX Cafe マスター

Macintosh Color Classicから始まった旅は、長いWindows時代を経て、Windows10のサポート終了をきっかけにUNIXの世界へ戻ってきました。UNIX Cafeでは、UNIX・Linux・そしてMacな世界を、むずかしい言葉を使わず、物語のように書いています。プログラミングは、アイデアをコンピューターに伝えるための言葉です。簡単な単語と文法を覚えれば、誰でもコマンドを使えます。ぜひ一度、やさしいプログラミングの世界をのぞいてみてください。

目次