[C言語のはじめ方] Part29: 基本的なソート(バブル・選択・挿入)

C言語

基本的なソート(バブル・選択・挿入)

プログラミングの世界へようこそ! C言語の学習を進めていく中で、データを効率的に扱うための基本的なテクニックを学ぶことは非常に重要です。今回はその中でも特に基本的な「ソート」(整列)アルゴリズムについて学んでいきましょう。コンピューターがどのようにデータを並べ替えるのか、その基本的な考え方を3つの代表的なアルゴリズムを通して理解します。 🤔

ソートとは、たくさんのデータを特定のルール(例えば、数字の小さい順や大きい順、アルファベット順など)に従って並べ替える操作のことです。ソートを学ぶことで、後のステップで学ぶデータ検索や、より複雑な問題を解決するための基礎体力が身につきます。💪

ここでは、以下の3つの基本的なソートアルゴリズムを取り上げます。

  • バブルソート
  • 選択ソート
  • 挿入ソート

これらのアルゴリズムは、効率の面では最新のアルゴリズムに劣る部分もありますが、ソートの基本的な考え方やプログラミングの練習として非常に有用です。それでは、一つずつ見ていきましょう!

1. バブルソート (Bubble Sort) 🫧

バブルソートは、隣り合う要素を比較して、もし順序が逆であれば入れ替える、という操作を繰り返すことで全体を整列させるアルゴリズムです。この操作を繰り返すと、軽い要素(小さい値)が泡(バブル)のように上っていくように見えることから、この名前がついています。

アルゴリズムの手順

  1. リストの先頭から末尾に向かって、隣り合う要素を比較します。
  2. 昇順(小さい順)に並べる場合、左の要素が右の要素より大きければ、それらを交換します。
  3. これをリストの末尾まで繰り返します。この1回の走査で、最も大きい要素がリストの末尾に移動します。
  4. 交換が発生しなくなるまで、またはリストの要素数-1回、上記の操作を繰り返します。

C言語による実装例

整数配列を昇順にソートするバブルソートの関数の例です。


#include <stdio.h>

// 配列の要素を交換する関数
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

// バブルソートを行う関数
void bubbleSort(int arr[], int n) {
    int i, j;
    int swapped; // 交換が行われたかを記録するフラグ
    for (i = 0; i < n - 1; i++) {
        swapped = 0; // 各パスの開始時にフラグをリセット
        // 末尾から順に比較・交換 (末尾からi個の要素は既にソート済み)
        for (j = 0; j < n - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                swap(&arr[j], &arr[j + 1]);
                swapped = 1; // 交換が行われた
            }
        }
        // このパスで交換が一度も行われなかったら、ソート完了
        if (swapped == 0) {
            break;
        }
    }
}

// 配列を表示する関数
void printArray(int arr[], int size) {
    int i;
    for (i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() {
    int arr[] = {64, 34, 25, 12, 22, 11, 90};
    int n = sizeof(arr) / sizeof(arr[0]);

    printf("ソート前の配列: \n");
    printArray(arr, n);

    bubbleSort(arr, n);

    printf("ソート後の配列: \n");
    printArray(arr, n);

    return 0;
}
        

特徴

  • 実装が簡単: アルゴリズムの考え方がシンプルで、コードに落とし込みやすいです。
  • 安定ソート: 同じ値を持つ要素の相対的な順序が、ソート前後で変わりません。
  • 計算量:
    • 最悪計算時間: O(n2) – データが逆順に並んでいる場合など。
    • 平均計算時間: O(n2)
    • 最良計算時間: O(n) – データが既にソート済みの場合(上記のコードのように、交換がなければ早期終了する最適化を行った場合)。
  • 効率: 要素数が増えると計算時間が急激に増加するため、大きなデータセットには向きません。主に教育目的や、ごく少数の要素をソートする場合に使われます。

2. 選択ソート (Selection Sort) 👇

選択ソートは、リストの中から最小値(または最大値)を持つ要素を探し出し、それをリストの未ソート部分の先頭要素と交換する、という操作を繰り返すアルゴリズムです。

アルゴリズムの手順

  1. リスト全体から最小値を持つ要素を探します。
  2. 見つかった最小値要素を、リストの先頭要素(まだソートされていない部分の先頭)と交換します。
  3. リストの先頭要素はソート済みとみなし、残りの未ソート部分に対して同様の操作(最小値探索と交換)を繰り返します。
  4. 未ソート部分がなくなるまで繰り返します。

C言語による実装例

整数配列を昇順にソートする選択ソートの関数の例です。


#include <stdio.h>

// 配列の要素を交換する関数 (バブルソートと同じものを使用)
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

// 選択ソートを行う関数
void selectionSort(int arr[], int n) {
    int i, j, min_idx;

    // 配列の要素を一つずつ前に進める (n-1回繰り返す)
    for (i = 0; i < n - 1; i++) {
        // 未ソート部分の最小値のインデックスを探す
        min_idx = i;
        for (j = i + 1; j < n; j++) {
            if (arr[j] < arr[min_idx]) {
                min_idx = j;
            }
        }
        // 最小値要素を未ソート部分の先頭と交換
        // (最小値が既に先頭にある場合は交換しない最適化も可能だが、ここでは単純な実装)
        if (min_idx != i) {
             swap(&arr[min_idx], &arr[i]);
        }
    }
}

// 配列を表示する関数 (バブルソートと同じものを使用)
void printArray(int arr[], int size) {
    int i;
    for (i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() {
    int arr[] = {64, 25, 12, 22, 11};
    int n = sizeof(arr) / sizeof(arr[0]);

    printf("ソート前の配列: \n");
    printArray(arr, n);

    selectionSort(arr, n);

    printf("ソート後の配列: \n");
    printArray(arr, n);

    return 0;
}
        

特徴

  • 実装が比較的簡単: バブルソートと同様に、アルゴリズムの考え方は理解しやすいです。
  • 不安定ソート: 同じ値を持つ要素の相対的な順序が、ソート前後で変わる可能性があります。
  • 計算量:
    • 最悪計算時間: O(n2)
    • 平均計算時間: O(n2)
    • 最良計算時間: O(n2) – データの初期状態に関わらず、常に比較回数は同じです。
  • 交換回数が少ない: 各パスで最大1回の交換しか行わないため、要素の交換コストが高い場合に有利になる可能性があります(最大でもn-1回)。
  • 効率: バブルソートよりは一般的に少し速いとされますが、やはりO(n2)なので大きなデータセットには不向きです。

3. 挿入ソート (Insertion Sort) 🃏

挿入ソートは、リストの未ソート部分から要素を一つずつ取り出し、それをソート済み部分の適切な位置に挿入していくアルゴリズムです。トランプを手札で整理するときに、新しいカードを適切な場所に挿入していく動作に似ています。

アルゴリズムの手順

  1. リストの最初の要素はソート済みとみなします。
  2. 未ソート部分の先頭要素(これをキーと呼びます)を取り出します。
  3. キーをソート済み部分の要素と後ろから比較していき、キーより大きい要素は一つ右にずらします。
  4. キー以下の要素が見つかるか、ソート済み部分の先頭に達したら、その位置にキーを挿入します。
  5. 未ソート部分がなくなるまで、2から4の操作を繰り返します。

C言語による実装例

整数配列を昇順にソートする挿入ソートの関数の例です。


#include <stdio.h>

// 挿入ソートを行う関数
void insertionSort(int arr[], int n) {
    int i, key, j;
    // 2番目の要素から順に見ていく (最初の要素はソート済みとみなす)
    for (i = 1; i < n; i++) {
        key = arr[i]; // 挿入する要素 (キー)
        j = i - 1;

        // キーをソート済み部分の適切な位置に挿入する
        // キーより大きい要素を一つずつ後ろにずらす
        while (j >= 0 && arr[j] > key) {
            arr[j + 1] = arr[j];
            j = j - 1;
        }
        arr[j + 1] = key; // 見つけた位置にキーを挿入
    }
}

// 配列を表示する関数 (バブルソートと同じものを使用)
void printArray(int arr[], int size) {
    int i;
    for (i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() {
    int arr[] = {12, 11, 13, 5, 6};
    int n = sizeof(arr) / sizeof(arr[0]);

    printf("ソート前の配列: \n");
    printArray(arr, n);

    insertionSort(arr, n);

    printf("ソート後の配列: \n");
    printArray(arr, n);

    return 0;
}
        

特徴

  • 実装が比較的簡単: これも理解しやすいアルゴリズムの一つです。
  • 安定ソート: 同じ値を持つ要素の相対的な順序が、ソート前後で変わりません。
  • 計算量:
    • 最悪計算時間: O(n2) – データが逆順に並んでいる場合。
    • 平均計算時間: O(n2)
    • 最良計算時間: O(n) – データが既にソート済みの場合、内側のループがほとんど実行されないため高速です。
  • 効率: データがほとんどソートされている場合には非常に高速に動作します。また、要素数が少ない場合にも効率的です。オンラインアルゴリズム(データが逐次的に与えられる場合にも適用可能)としても知られています。
  • 適応性: データの初期状態によってパフォーマンスが大きく変わります。

4. 各ソートアルゴリズムの比較 📊

これまで見てきた3つの基本的なソートアルゴリズムの特徴を比較してみましょう。

特徴バブルソート選択ソート挿入ソート
計算量(最悪)O(n2)O(n2)O(n2)
計算量(平均)O(n2)O(n2)O(n2)
計算量(最良)O(n) ※最適化ありO(n2)O(n)
安定性安定不安定安定
メモリ使用量O(1) (インプレース)O(1) (インプレース)O(1) (インプレース)
主な利点実装が最も単純交換回数が少ないほぼソート済みのデータに高速、オンライン処理可能
主な欠点遅い、比較・交換回数が多い遅い、不安定逆順データに遅い

安定ソートとは?
ソートアルゴリズムが「安定」であるとは、同じ値を持つ要素の相対的な順序が、ソート前とソート後で変わらないことを意味します。例えば、「(年齢, 名前)」のペアを年齢でソートする場合、安定ソートなら同じ年齢の人たちの名前の順序は保たれますが、不安定ソートでは順序が変わってしまう可能性があります。

O(n2) や O(n) とは? (計算量)
これはアルゴリズムの計算時間(または必要なメモリ量)が、入力データ数 n に対してどのように増加するかを表す指標です。「オーダー」と呼ばれます。

  • O(n2) (オーダ n二乗): データ数が2倍になると、計算時間は約4倍になります。比較的遅いアルゴリズムです。
  • O(n) (オーダ n): データ数が2倍になると、計算時間も約2倍になります。データ数に比例して時間が増える、効率的なアルゴリズムです。
  • O(n log n): さらに効率的なソートアルゴリズム(マージソートやクイックソートなど)で達成される計算量です。
計算量の詳細は今後の学習でより深く理解していくことになりますが、今は「数字が小さいほど速い傾向がある」と覚えておけば良いでしょう。

まとめ ✨

今回は、C言語における基本的なソートアルゴリズムとして、バブルソート、選択ソート、挿入ソートを学びました。それぞれのアルゴリズムには特徴があり、得意な状況と不得意な状況があります。

  • バブルソート: 最もシンプルですが、効率は良くありません。
  • 選択ソート: 交換回数は少ないですが、効率はO(n2)で、不安定です。
  • 挿入ソート: ほぼソート済みのデータに強く、安定ですが、最悪計算量はO(n2)です。

これらの基本的なソートアルゴリズムは、実際の開発現場で大量のデータを扱う際に使われることは少ないかもしれません(より高速なO(n log n)のアルゴリズム、例えばクイックソートやマージソートなどが使われることが多いです)。しかし、アルゴリズムの基本的な考え方、ループや条件分岐、配列操作といったプログラミングの基礎を学ぶ上で非常に重要です。

今回学んだ知識を土台として、次はより効率的な探索アルゴリズムや、他のデータ構造についても学んでいきましょう!🚀

参考情報:
ソートアルゴリズムについてさらに詳しく知りたい場合は、以下のサイトなどが参考になります。

コメント

タイトルとURLをコピーしました