C言語の基本|アルゴリズムとソート

同じ結果でも手順が変われば動きも変わる。アルゴリズムとソートで、問題を順序立てて解く力を身につけよう。

アルゴリズムという言葉を聞くと、少し難しそうに感じるかもしれません。しかし、基本的な意味はとてもシンプルです。

アルゴリズムとは、問題を解決するための手順です。

コンピュータは、人間のように状況を見て判断したり、曖昧な指示から意図を読み取ったりすることができません。そのため、何を、どの順番で、どのような条件で実行するのかを、はっきり指示する必要があります。

たとえば、2つの整数を足して結果を表示するだけでも、次のような手順が必要です。

  1. 1つ目の整数を受け取る
  2. 2つ目の整数を受け取る
  3. 2つの整数を加算する
  4. 加算結果を表示する

このように、処理を順番に整理したものがアルゴリズムです。そして、そのアルゴリズムをC言語の文法を使って、コンピュータが実行できる形にする作業がプログラミングです。

アルゴリズムを学ぶ題材として分かりやすいものに、ソートがあります。ソートとは、複数のデータを決められた順番に並べ替える処理です。

同じ小さい順への並べ替えでも、比較する場所や交換するタイミングによって、処理の進み方が変わります。この記事では、基本的なバブルソートとセレクションソートを使って、アルゴリズムをC言語で表現する流れを見ていきます。

アルゴリズムとは

アルゴリズムは、目的を達成するために必要な処理を、実行できる順番に並べた手順書です。

大切なのは、処理の内容が曖昧になっていないことです。

人間なら、数字を小さい順にうまく並べてくださいと指示されても、意味を考えながら作業できます。しかし、コンピュータに同じ指示を与えても、具体的な手順がなければ処理できません。

並べ替えを実行させるには、少なくとも次の内容を明確にする必要があります。

  • どの要素から比較を始めるのか
  • どの要素とどの要素を比較するのか
  • どのような条件で交換するのか
  • 交換するときに値をどのように移動するのか
  • 比較をどこまで繰り返すのか
  • どの時点で並べ替えを終了するのか

これらの手順がはっきりしていれば、コンピュータはその順番に従って処理できます。

図1:アルゴリズムをC言語の処理に変換する流れ

この図から分かること

プログラムを作るときは、いきなりC言語のコードを書き始めるのではなく、最初に目的と処理手順を整理します。

たとえばソートでは、比較、交換、繰り返しという小さな処理に分解します。そのうえで、繰り返しをfor、交換条件の判定をifで記述すれば、アルゴリズムを実行可能なプログラムにできます。

複雑に見える問題でも、小さな処理に分けて順番を決めることで、コードに置き換えやすくなります。

アルゴリズムをプログラムにする流れ

頭の中で考えた手順を、コンピュータが実行できる形に置き換えるには、C言語の制御構文や変数を使います。

アルゴリズム上の処理C言語で使うもの役割
データを順番に調べるfor同じ処理を決められた回数だけ繰り返す
交換が必要か判断するif条件が成立した場合だけ処理する
複数の値を管理する配列同じ型のデータをまとめて保存する
値を入れ替える一時変数一方の値が失われないように一時保存する
結果を表示するprintf並べ替え前後の状態を確認する

ソートでは、配列の要素をforで順番に調べます。要素の大小関係をifで判定し、順番が正しくない場合は一時変数を使って交換します。

このように、アルゴリズム上の手順とC言語の処理を対応させることが、プログラムを組み立てる基本になります。

ソートがアルゴリズムの学習に適している理由

ソートは、複数のデータを一定の規則に従って並べ替える処理です。

数値を小さい順に並べることを昇順、大きい順に並べることを降順といいます。この記事では、数値を小さい順に並べる昇順を扱います。

同じ昇順でも、さまざまな並べ替え方があります。

  • 隣り合う要素を比較して交換する
  • 残っている要素から最小値を探す
  • 整列済みの範囲へ適切な位置に挿入する
  • データを複数の範囲に分割して並べ替える

どの方法でも、最終的には同じ並びになることがあります。しかし、比較する場所、交換する回数、確定する要素の位置などは異なります。

処理の途中経過を目で追いやすいため、ソートはアルゴリズムの違いを学ぶ題材に向いています。

ここでは、次の2つを取り上げます。

ソート方法基本的な考え方
バブルソート隣り合う要素を比較し、大きい値を右側へ移動させる
セレクションソート未整列の範囲から最小値を探し、左側へ移動させる

バブルソートとセレクションソートは、どちらも比較と交換を繰り返します。ただし、比較する要素と確定していく方向が異なります。

バブルソートの考え方

バブルソートは、隣り合う2つの要素を比較し、左側の値が右側の値より大きければ交換する方法です。

左から右へ比較を進めると、大きい値が少しずつ右側へ移動します。端まで比較すると、その範囲で最も大きい値が右端に確定します。

基本的な手順は次のとおりです。

  1. 配列の先頭から隣り合う2つの要素を比較する
  2. 左側の値が大きければ2つの値を交換する
  3. 比較位置を1つ右へ移動する
  4. 比較範囲の右端まで同じ処理を繰り返す
  5. 確定した右端を比較範囲から除外する
  6. 残りの範囲で比較と交換を繰り返す

外側の繰り返しが1回終わるたびに、右側の要素が1つずつ確定します。そのため、次の繰り返しでは、すでに確定した要素を比較する必要がありません。

バブルソートの動きを具体例で確認する

次の配列を小さい順に並べます。

[8, 3, 7, 1, 5]

最初の端までの比較では、隣り合う要素を左から順番に調べます。

  1. 8と3を比較する
    8のほうが大きいため交換します。
    [3, 8, 7, 1, 5]
  2. 8と7を比較する
    8のほうが大きいため交換します。
    [3, 7, 8, 1, 5]
  3. 8と1を比較する
    8のほうが大きいため交換します。
    [3, 7, 1, 8, 5]
  4. 8と5を比較する
    8のほうが大きいため交換します。
    [3, 7, 1, 5, 8]

これで、配列内の最大値である8が右端に移動しました。8の位置は確定したため、次の比較では右端を除外します。

2回目の端までの比較が終わると、次に大きい7が右から2番目に確定します。この処理を繰り返すことで、配列全体が小さい順に並びます。

図2:バブルソートで最大値が右端へ移動する流れ

この図から分かること

バブルソートでは、隣り合う2つの値だけを比較します。

最初は8と3を比較し、順番が逆なので交換します。その後も8と右隣の値を比較しながら交換するため、8が少しずつ右へ移動します。

1回の端までの比較が終わると、その範囲で最も大きい値が右端に確定します。次の繰り返しでは確定した右端を除外できるため、比較する範囲が少しずつ短くなります。

隣り合う値を比較して昇順に並べるプログラム
#include <stdio.h>

int main(void)
{
    int values[] = {8, 3, 7, 1, 5};
    int size = 5;
    int i, j, temp;

    printf("並べ替え前: ");
    for (i = 0; i < size; i++) {
        printf("%d ", values[i]);
    }
    printf("\n");

    /* バブルソートで大きい値を右側へ移動する */
    for (i = 0; i < size - 1; i++) {
        for (j = 0; j < size - 1 - i; j++) {
            if (values[j] > values[j + 1]) {
                temp = values[j];
                values[j] = values[j + 1];
                values[j + 1] = temp;
            }
        }
    }

    printf("並べ替え後: ");
    for (i = 0; i < size; i++) {
        printf("%d ", values[i]);
    }
    printf("\n");

    printf("バブルソートで大きい値を右側へ移動しました。\n");

    return 0;
}

ファイル名:7_1_1.c

実行結果の例
並べ替え前: 8 3 7 1 5
並べ替え後: 1 3 5 7 8
バブルソートで大きい値を右側へ移動しました。
バブルソートのプログラムを詳しく確認する

valuesは、並べ替える5つの整数を格納する配列です。

記述役割
int values[] = {8, 3, 7, 1, 5};並べ替える整数を配列に保存する
int size = 5;配列の要素数を保存する
int i, j, temp;繰り返しと値の交換に使う変数を宣言する
for (i = 0; i < size - 1; i++)配列全体の比較を繰り返す
for (j = 0; j < size - 1 - i; j++)隣り合う要素を左から右へ比較する
if (values[j] > values[j + 1])左側の値が大きいか判定する
return 0;プログラムが正常に終了したことを表す

最初のforでは、配列の要素を先頭から順番に表示しています。これにより、並べ替える前の状態を確認できます。

ソート処理では、forを二重に使っています。

外側のforは、端までの比較を繰り返すための処理です。要素数が5個の場合、最大で4回繰り返せばすべての位置を確定できます。そのため、iがsize - 1より小さい間だけ処理します。

内側のforは、隣り合う要素を比較するための処理です。values[j]が左側の要素、values[j + 1]が右側の要素を表します。

外側のforが1回終わるたびに、右端から1つずつ要素が確定します。そのため、内側のforの条件をsize - 1 - iとし、確定済みの要素を比較対象から除外しています。

ifでは、左側のvalues[j]が右側のvalues[j + 1]より大きいかを判定します。条件が成立した場合は、次の3段階で値を交換します。

  1. 左側の値をtempへ一時保存する
  2. 右側の値を左側へ代入する
  3. tempに保存していた値を右側へ代入する

一時変数tempを使わずに一方の値を上書きすると、交換前の値が失われてしまいます。2つの値を安全に入れ替えるためには、一方を一時的に保存する必要があります。

セレクションソートの考え方

セレクションソートは、まだ並び順が確定していない範囲から最小値を探し、その範囲の先頭と交換する方法です。

最小値を選択して前へ移動するため、基本選択法とも呼ばれます。

基本的な手順は次のとおりです。

  1. 未整列範囲の先頭を最小値の候補にする
  2. 候補と後ろの要素を順番に比較する
  3. より小さい値が見つかったら、その位置を記録する
  4. 未整列範囲の最後まで最小値を探す
  5. 見つけた最小値を未整列範囲の先頭と交換する
  6. 確定した先頭を除外して同じ処理を繰り返す

バブルソートでは右側から大きい値が確定しますが、セレクションソートでは左側から小さい値が確定します。

セレクションソートの動きを具体例で確認する

次の配列を小さい順に並べます。

[8, 5, 2, 7, 3]

最初は配列全体が未整列の範囲です。

  • 1回目は、8を最小値の候補として、5、2、7、3と順番に比較します。最小値は2なので、先頭の8と交換します。
    [2, 5, 8, 7, 3]
    これで、先頭の2が確定しました。
  • 2回目は、先頭の2を除いた範囲から最小値を探します。
    [5, 8, 7, 3]
    この範囲の最小値は3です。3を範囲の先頭にある5と交換します。
    [2, 3, 8, 7, 5]
  • 3回目は、8、7、5の中から最小値を探します。
    [ 8, 7, 5]
    この範囲の最小値は5です。最小値の5を8と交換します。
    [2, 3, 5, 7, 8]

このように、未整列範囲の最小値を探して先頭へ移動することで、左側から順番に位置が確定します。

未整列範囲の最小値を選んで昇順に並べるプログラム

ファイル名:7_1_2.c

#include <stdio.h>

int main(void)
{
    int values[] = {8, 5, 2, 7, 3};
    int size = 5;
    int i, j, min_index, temp;

    printf("並べ替え前: ");
    for (i = 0; i < size; i++) {
        printf("%d ", values[i]);
    }
    printf("\n");

    /* セレクションソートで最小値を左側へ移動する */
    for (i = 0; i < size - 1; i++) {
        min_index = i;

        for (j = i + 1; j < size; j++) {
            if (values[j] < values[min_index]) {
                min_index = j;
            }
        }

        /* 最小値が別の位置にある場合は交換する */
        if (min_index != i) {
            temp = values[i];
            values[i] = values[min_index];
            values[min_index] = temp;
        }
    }

    printf("並べ替え後: ");
    for (i = 0; i < size; i++) {
        printf("%d ", values[i]);
    }
    printf("\n");

    printf("セレクションソートで最小値を左側へ移動しました。\n");

    return 0;
}
実行結果の例
並べ替え前: 8 5 2 7 3
並べ替え後: 2 3 5 7 8
セレクションソートで最小値を左側へ移動しました。
セレクションソートのプログラムを詳しく確認する

valuesには、並べ替えの対象となる5つの整数が保存されています。sizeには配列の要素数である5を保存しています。

記述役割
int min_index最小値が見つかった位置を保存する
min_index = i;未整列範囲の先頭を最小値の候補にする
for (j = i + 1; j < size; j++)候補より後ろにある要素を調べる
if (values[j] < values[min_index])現在の候補より小さい値か判定する
min_index = j;新しく見つけた最小値の位置を保存する
if (min_index != i)最小値が未整列範囲の先頭以外にあるか判定する

外側のforで使用するiは、今回最小値を配置する位置を表しています。iが0なら配列の先頭、iが1なら左から2番目の位置を確定します。

各繰り返しの最初に、min_indexへiを代入します。これにより、未整列範囲の先頭を最小値の候補にします。

内側のforは、i + 1の位置から配列の最後まで調べます。現在のvalues[j]が最小値候補のvalues[min_index]より小さい場合は、min_indexをjへ変更します。

内側のforが終了した時点で、min_indexには未整列範囲内の最小値の位置が保存されています。

min_indexとiが異なる場合は、最小値が未整列範囲の先頭以外にあることを意味します。その場合はtempを使ってvalues[i]とvalues[min_index]を交換します。

最小値が最初から未整列範囲の先頭にあった場合は、min_indexとiが同じになります。この場合は交換する必要がないため、交換処理を実行しません。

バブルソートとセレクションソートの違い

観点バブルソートセレクションソート
基本動作隣り合う要素を比較して交換する未整列範囲から最小値を探して交換する
比較する相手すぐ隣の要素未整列範囲内の要素
1回の外側のforで確定する値比較範囲の最大値未整列範囲の最小値
確定していく方向右側から確定する左側から確定する
交換の特徴比較の途中で何度も交換する場合がある最小値を探し終えてから交換する
動きのイメージ大きい値を右へ押し出す小さい値を選んで左へ置く

バブルソートは、隣り合う要素の順番が逆になるたびに交換します。そのため、1回の端までの比較で複数回交換することがあります。

セレクションソートは、未整列範囲を最後まで調べてから最小値を先頭と交換します。そのため、バブルソートより交換回数が少なくなりやすいという違いがあります。

どちらもforを二重に使って比較を繰り返しますが、値を確定する方法と方向が異なります。

図3:バブルソートとセレクションソートの違い

この図から分かること

バブルソートは、隣り合う値を比較しながら大きい値を右へ移動させます。端まで比較すると、最大値が右側に確定します。

セレクションソートは、未整列範囲全体から最小値を探します。最小値を範囲の先頭と交換するため、小さい値が左側から確定します。

どちらも最終的には同じ昇順の配列を作れますが、比較する方法、交換するタイミング、確定していく方向が異なります。

ほかのソートアルゴリズム

ソートには、バブルソートとセレクションソート以外にもさまざまな方法があります。

ソート方法基本的な考え方
インサーションソート整列済みの範囲へ、次の要素を適切な位置に差し込む
クイックソート基準となる値を使ってデータを分割し、それぞれを並べ替える
マージソートデータを細かく分割し、順番を整えながら結合する

インサーションソートは、手元のカードを順番に並べるような考え方です。左側の整列済み範囲を広げながら、新しい値を適切な位置に挿入します。

クイックソートは、基準となる値より小さいグループと大きいグループに分けて並べ替えます。

マージソートは、データを小さな単位に分割し、順番を整えながら結合します。

それぞれ比較方法やデータの移動方法が異なるため、処理の特徴も変わります。まずはバブルソートとセレクションソートを通して、比較、条件分岐、交換、繰り返しという基本的な手順をC言語で表現できるようになることが大切です。