バブルソートを図を使って分かりやすく解説(C言語サンプルプログラム付き)

バブルソートの解説ページアイキャッチ

このページでは、ソートアルゴリズムの1つである「バブルソート」について解説していきます。

まずバブルソートの解説を行い、その次にバブルソートを行うC言語のサンプルプログラムの紹介を行います。

また、(他のソートアルゴリズムでも同様ですが)バブルソートは様々な実装方法で実現することができるため、今回は実装方法を変えた色々なバブルソート関数を紹介していきたいと思います。

バブルソートの考え方の解説は、どのプログラミング言語を利用されている方にも理解していただける内容だと思います!

バブルソートとは

まずはバブルソートがどのようなアルゴリズムであるかを解説します。

バブルソートの特徴

バブルソートには下記のような特徴があります。

  • ソート速度は遅い
  • 使用メモリが少ない(配列一つ分でソートできる)
  • アルゴリズムが簡単

ソート速度は遅いですが、個人的には一番「実装が簡単」なソートアルゴリズムだと思っています。考え方も非常にシンプルです。

スポンサーリンク

バブルソートの考え方

ではバブルソートの考え方について解説していきます。今回はデータの集合を “昇順” にソートすることを前提に解説していきます。

バブルソートは、隣り合うデータの大小関係が逆の場合その隣り合うデータを交換していくことでソートを行うアルゴリズムになります。

バブルソートの概念図

「大小関係が逆」とは、例えば昇順にソートする場合であれば、”前側のデータの方が後ろ側のデータよりも大きい” ことを言います。

「大小関係が逆」の意味を示す図

逆に降順にソートする場合であれば、”前側のデータの方が後ろ側のデータよりも小さい” ことを言います。

で、隣り合うデータで「大小関係が逆」の場合にデータを交換すれば、これらの大小関係が整います。

交換により大小関係が整う様子

バブルソートでは、この「隣り合うデータの交換」をデータの集合の “先頭から最後尾まで” 繰り返し行います。前述の通り、この交換は「大小関係が逆」の場合だけ行います。

先頭から最後尾までデータの交換を繰り返す様子

例えばソートしたいデータの集合が下の図のようなものであるとしましょう。

ソートを行うデータの例

まずは先頭から1番目のデータと2番目のデータの大小関係を確認し、「大小関係が逆」であればデータを交換します。この例だと「大小関係が逆」なので、データを交換します。

1番目と2番目のデータを交換する様子

続いては、先頭から2番目のデータと3番目のデータのデータの大小関係を確認し、同様に「大小関係が逆」であればデータを交換します。この例だとデータは交換しなくて良いことになりますね。

2番目と3番目のデータを交換しない図

以降も、大小関係を確認する「隣り合うデータ」を1つずつずらしながら、データの集合の最後尾まで同様の処理を行います。

先頭から最後尾までの交換を繰り返す様子

交換を行う前のデータの並びと、先頭から最後尾まで上記の交換を行なった後のデータの並びはそれぞれ下の図のようになります。矢印は交換によってどの位置に移動したかを示しています。

交換前後のデータの違い

見ていただければ分かる通り、小さいデータは前側に、大きいデータは後ろ側に寄せられていることが分かります。

つまり、一段階ソートが進んだと考えられます。

ソートが一段階進んだ様子

このソート後のデータの集合に対し、先ほどと同様のことを行って見ましょう!つまり、隣り合うデータの交換をデータの集合の先頭から最後尾まで繰り返し行います。

先頭から最後尾までの交換を繰り返す様子2

これにより、データの並びが下の図のようになり、また一段階ソートが進んだことが確認できます。

先頭から最後尾までの交換を繰り返す様子2

要はこれの繰り返しです。この繰り返しで、ソートをどんどん進めていきます。

で、いずれは隣り合うデータの交換をデータの集合の先頭から最後尾まで行おうとしても、一度も交換が発生しないケースが発生します。

先頭から最後尾までの間で一度も交換が発生しない様子

なぜ交換が行われないかというと、「大小関係が逆」となる隣り合うデータの組み合わせが存在しなくなったからです。

全ての隣り合うデータの大小関係が整った様子

つまり、ソートが完了したことになります!なので、この時点でソート処理を終了することができます。

要はバブルソートとは、“大小関係が逆” の隣り合うデータの交換を繰り返すことでソートを行うアルゴリズムになります。

一連の処理の詳細をまとめると、バブルソートとは、下記のように処理を行うことでソートを行うアルゴリズムになります。

  1. 下記をデータの集合の先頭から最後尾まで1つずつデータをずらしながら繰り返し行う
    • 隣り合うデータの大小関係を比較する
      • 大小関係が逆の場合:その2つのデータを交換する
  2. 一度でも交換が行われたのであれば、1. に戻る

大きい “泡” ほど速く浮かび上がるように、大きいデータほど速くデータの後ろ側に寄せられていくことから「バブルソート」と呼ばれるようです。

ただ、個人的にはですが、バブルソートだと「ソートアルゴリズム名」と「アルゴリズム自体」がパッと結びつかないので、「バブルソート = 交換法」と覚えた方が良いかなぁと思います。

バブルソートのプログラム

では、ここまで解説してきた「バブルソート」を行うプログラムのソースコードを紹介していきたいと思います。

バブルソートプログラムのソースコード

下記がそのソースコードになります。具体的には、bubbleSort 関数がバブルソートを行う関数になります。

おそらくバブルソートの関数としては、かなり “分かりやすく” 実装されていると思います!

バブルソート
/*
 * バブルソートを行う関数
 * a:ソートしたいデータを格納した配列
 * num:ソートしたいデータの個数
 */
void bubbleSort(int a[], int num) {

    int exchange; /* 交換回数 */
    int i;
    int tmp;

    do {
        exchange = 0;

        /* 先頭のデータから最後尾の1つ手前のデータまでループ */
        for (i = 0; i < num - 1; i++) {

            /* 隣り合うデータの大小関係を確認 */
            if (a[i] > a[i + 1]) {
                /* 大小関係が逆なら2つのデータを交換 */
                tmp = a[i];
                a[i] = a[i + 1];
                a[i + 1] = tmp;

                /* 交換が行われた回数をカウントアップ */
                exchange++;
            }
        }

    } while(exchange > 0); /* 交換が発生している間はソート続ける */

}

スポンサーリンク

ソースコードの解説

bubbleSort 関数に関して少し解説をしておきます。

bubbleSort 関数は、ほぼバブルソートの考え方で解説した内容をそのまま実装しているだけのものになります。

ポイントを上げるとすると下記の2つだと思います。

  • for 文でのループ範囲
  • データの交換
  • ソート完了の判断(while ループの継続条件)

for 文でのループ範囲

bubbleSort 関数における for 文は下記のようになっています。

forループ
/* 先頭のデータから最後尾の1つ手前のデータまでループ */
for (i = 0; i < num - 1; i++) {

    /* 隣り合うデータの大小関係を確認 */
    if (a[i] > a[i + 1]) {
        /* 略 */
    }
}

i の最大値が num - 2 になっているところがポイントです。

ここで inum - 1 まで増加するように for 文を組んでしまうと、for 内部での a[i + 1] へのアクセス時に配列の外にアクセスし、アクセス違反が発生することになるので注意してください。

i が num - 2 の時には、a[num - 2]a[num - 1] の交換が行われるので、 i の最大値が num - 2(つまりデータの最後尾の1つ手前)であっても、最後尾のデータも含めて交換を行うことができます。

データの交換

データの交換は下記で行っています。

データの交換
/* 隣り合うデータの大小関係を確認 */
if (a[i] < a[i + 1]) {
    /* 大小関係が逆なら2つのデータを交換 */
    tmp = a[i];
    a[i] = a[i + 1];
    a[i + 1] = tmp;

    /* 交換が行われた回数をカウントアップ */
    exchange++;
}

隣り合うデータは a[i] とその1つ後ろのデータ(つまり a[i + 1])として交換を行なっています。

データの交換を行う場合は、交換を行うデータの一方を一旦退避しておく必要がある点に注意してください(上記では tmpa[i] を退避させています)。

また、「大小関係が逆」であるかどうかは下記で判断しています。

昇順に比較する際の比較
if (a[i] < a[i + 1]) {

上記は “昇順にソートする” 場合の判断になりますが、下記のように変更することで降順にソートすることも可能です。

降順に比較する際の比較
if (a[i] > a[i + 1]) {

ソート完了の判断(while ループの継続条件)

また、バブルソートの考え方でも説明した通り、先頭から最後尾の間の全ての隣り合うデータにおいて、交換が一度も発生しなくなったらソートが完了したことになるので、その時点でソートを終了することができます。

そのため、交換が発生した回数を exchange 変数でカウントするようにし、exchange 変数が 0 の時、つまり、先頭から最後尾の間の全ての隣り合うデータにおいて交換が一度も発生しなかった場合は while ループを抜けてソートを終了するようにしています。

バブルソートの様々な実装

で、おそらく上記の bubbleSort 関数は他のサイトさんが公開されている関数とは異なる実装になっていると思います(私は上記の bubbleSort 関数が一番わかりやすい bubbleSort 関数の実装だとは思っていますが…)。

何が言いたいかというと、アルゴリズムを実現するための実装方法は1つではなく、たくさん存在するということです。

ということで、ここではバブルソートの様々な実装を紹介していきたいと思います。

MEMO

どの実装方法でもバブルソートは実現できますし、一応全てソートができることは確認しています

ただ、効率の良し悪しやコードの分かりやすさの違いもありますし、例えば課題などでバブルソート関数を実装するような場合は、その課題で求められている実装方法が存在する可能性もあります

その場面場面で、どの実装方法が良いかを判断して使い分けることが重要です!

ソート範囲を考慮して効率アップさせる

先ほどお見せした bubbleSort 関数は非常にシンプルで私は好きなのですが、実はバブルソートの特性を考慮するとちょっと無駄な処理を行ってしまっています…。

前述の通り、バブルソートは「隣り合うデータの大小関係が逆の場合に、その2つのデータを交換する」ことでソートをしていくソートアルゴリズムになります。

で、上記を先頭から最後尾のデータまで繰り返せば、昇順にソートする場合は必ず一番大きいデータが最後尾に移動することになります。

先頭から最後尾までデータの交換を行うことで一番大きいデータが最後尾に来る様子

なぜなら、隣り合うデータの “前側のデータ” が一番大きいデータなのであれば、必ず「大小関係が逆」になり、データの交換が行われるからです。

一番大きいデータが最後尾に来る理由1

そして、データ交換後は、この一番大きいデータがまた次の隣り合うデータの “前側のデータ” となり、またデータの交換が行われます。

一番大きいデータが最後尾に来る理由2

つまり、一番大きいデータは一度交換が行われると、それ以降は毎回交換が行われ、最終的に最後尾のデータとなります。

一番大きいデータが最後尾に移動したということは、その時点で一番大きいデータの位置が確定したことになります。ですので、この時点でソートする範囲をデータ1つ分狭めることができます。

最後尾のデータの位置が確定する様子1

そして、この新たなソート範囲に対して先頭から最後尾のデータに対してデータの交換を繰り返せば、今度はそのソート範囲で一番大きいデータ(つまりデータ全体で2番目に大きいデータ)が、そのソート範囲の最後尾に移動します。

先頭から最後尾までデータの交換を行うことで一番大きいデータが最後尾に来る様子2

この時点で2番目に大きいデータの位置が確定します。つまり、またソートする範囲をデータ1つ分狭めることができます。

最後尾のデータの位置が確定する様子2

こんな感じで、バブルソートでは先頭から最後尾までのデータの交換が完了した際にソートする範囲を1つ狭めることが可能です。

そして、このソート範囲を狭めることで比較回数を減らすことができるので、ソートの効率が向上します。

上記の説明に基づき、バブルソートプログラムのソースコードで紹介した bubbleSort 関数を、先頭から最後尾までの一連のデータの交換が完了する度に “データ1つ分ソート範囲を狭める” ように変更したものが下記になります。

1つずつ範囲を狭めながらソートする実装
/*
 * バブルソートを行う関数
 * a:ソートしたいデータを格納した配列
 * num:ソートしたいデータの個数
 */
void bubbleSort(int a[], int num) {

    int exchange; /* 交換回数 */
    int i;
    int tmp;
    int size; /* ソート範囲のデータの個数 */

    /* 最初はソートする範囲のデータの個数 = 全体のデータの個数 */
    size = num;

    do {
        exchange = 0;

        /* 先頭のデータから最後尾の1つ手前のデータまでループ */
        for (i = 0; i < size - 1; i++) {

            /* 隣り合うデータの大小関係を確認 */
            if (a[i] > a[i + 1]) {
                /* 大小関係が逆なら2つのデータを交換 */
                tmp = a[i];
                a[i] = a[i + 1];
                a[i + 1] = tmp;

                /* 交換が行われた回数をカウントアップ */
                exchange++;
            }
        }

        /* ソートする範囲をデータ1つ文狭める */
        size--;

    } while(exchange > 0); /* 交換が発生している間はソート続ける */

}

ソート範囲を終了条件にする

また、ソート範囲のデータ数が 1 になった時点でソートが完了することになるので、その時点でソートを完了させることも可能です。

ソート範囲のデータ数をソートの終了条件とした場合、交換回数 = 0 をソートの終了条件にしなくてもソートを完了させることも可能です。

上記に基づき、ソート範囲を考慮して効率アップさせるで紹介した bubbleSort 関数を、ソート範囲のみを終了条件とするように変更したものは下記のようになります。

ソート範囲を終了条件とする実装
void bubbleSort(int a[], int num) {

    int i;
    int tmp;
    int size; /* ソート範囲のデータの個数 */

    /* 最初はソートする範囲のデータの個数 = 全体のデータの個数 */
    size = num;

    do {

        /* 先頭のデータから最後尾の1つ手前のデータまでループ */
        for (i = 0; i < size - 1; i++) {

            /* 隣り合うデータの大小関係を確認 */
            if (a[i] > a[i + 1]) {
                /* 大小関係が逆なら2つのデータを交換 */
                tmp = a[i];
                a[i] = a[i + 1];
                a[i + 1] = tmp;

            }
        }

        /* ソートする範囲をデータ1つ文狭める */
        size--;

    } while(size > 1); /* ソートする範囲が1になるまでソートを続ける */

}

これもシンプルで分かりやすいですね!

ただ交換が発生しなくなっても(つまりソートが完了しても)、一連の処理が最後まで実行されることになるので、その点は若干非効率です。

また「バブルソートの終了条件 = ソート範囲が 1 になるまで」と説明されることも多く、この場合はこの実装(もしくは次の節で紹介している実装)が期待されている可能性も高いです。

最後に交換したデータまでを次のソート範囲とする

ソート範囲を考慮して効率アップさせるで、データの交換を先頭から最後尾のデータまで繰り返すことで、”必ず” 一番大きいデータの位置が確定すると説明しました(つまり一番大きいデータが最後尾に移動する)。

先頭から最後尾までデータの交換を行うことで一番大きいデータが最後尾に来る様子の復習

“必ず” 位置が確定するのは「一番大きいデータ」のみなのですが、それ以外のデータでも、たまたま偶然位置が確定する場合があります。

例えば下図のようなデータをソートすることを考えてみましょう。

ソートするデータの例

この場合、データの交換を先頭から最後尾のデータまで繰り返せば、下図のようにソートが一段階進みます。

複数のデータの位置が一気に確定する様子

見ていただければわかるとおり、最後尾から 6 のデータまでは位置が既に確定していますね!

つまり、6 のデータ以降はソートが完了していると考えることができ、次のソートの範囲(データの交換を行う範囲)は “データの先頭から 6 のデータの1つ前まで” のみで良いことになります。

次にソートを行えば良い範囲

このように、先頭から最後尾のデータまでデータの交換を繰り返すことで、最後尾から複数のデータの位置が一気に確定することがあります。

なので、ソート範囲を考慮して効率アップさせるで説明したようにソート範囲をデータ1つ分狭めるだけでなく、複数のデータ分狭めることができる場合があります。

じゃあ、実際に “どこからのデータがソートが完了しているか” をどうやって判断すれば良いかというと、これは “最後に交換したデータの位置” から判断できます。より具体的には最後に交換したデータの “後ろ側” の位置以降はソートが完了していると判断できます。

ソートが完了した範囲を最後に交換したデータの後ろ側の位置から判断する様子

昇順ソートの場合、先頭から順に隣り合うデータの交換を行えば、最後に交換したデータの “後ろ側” の位置に「そこまで交換を行なってきた範囲の中で一番大きいデータ」が来ることになります。

データの交換により、そこまで交換を行なってきたデータのなかで一番大きいデータが後ろ側に来る様子

また、その最後の交換以降、一度も交換が発生していないということは、最後に交換したデータ以降のデータにおいては、隣り合うデータの組み合わせ全てで「大小関係が既に整っている」と言えます(交換が発生していないので)。

最後の交換の後ろ側の位置以降で既にソートが完了している様子

つまり、最後に交換したデータ以降は既にソート済み、つまりデータの位置が確定していると判断できます。

なので、次のソートの範囲を「前回のソート時の最後に交換したデータの “後ろ側” の位置の1つ前まで」とすることが可能です。

ソートを行う範囲を最後に交換した後ろ側の位置の1つ手前までとする様子

そして、このようにソート範囲を狭めていくことで、ソートの効率をアップさせることが可能です。

上記の説明に基づき、ソート範囲を終了条件にするで紹介した bubbleSort 関数において、次のソート範囲を「前回のソートの時の最後に交換したデータの “後ろ側” の位置の1つ前まで」とするように変更したものが下記となります。

複数データ分範囲を狭めながらソートする実装
/*
 * バブルソートを行う関数
 * a:ソートしたいデータを格納した配列
 * num:ソートしたいデータの個数
 */
void bubbleSort(int a[], int num) {

    int i;
    int tmp;
    int size; /* ソート範囲のデータの個数 */
    int last_exchange; /* 交換したデータの後ろ側の位置*/

    /* 最初はソートする範囲のデータの個数 = 全体のデータの個数 */
    size = num;

    do {
        /* 一旦0で初期化 */
        last_exchange = 0;

        /* 先頭のデータから最後尾の1つ手前のデータまでループ */
        for (i = 0; i < size - 1; i++) {

            /* 隣り合うデータの大小関係を確認 */
            if (a[i] > a[i + 1]) {
                /* 大小関係が逆なら2つのデータを交換 */
                tmp = a[i];
                a[i] = a[i + 1];
                a[i + 1] = tmp;

                /* 交換時の後ろ側のデータの位置を記憶 */
                last_exchange = i + 1;

            }
        }

        /* 最後に交換した後ろの位置以降はソート範囲から省く */
        size = last_exchange;

    } while(size > 1); /* ソートする範囲が1になるまでソートを続ける */
}

while ループを for ループで実現する

あとは do while ループの部分を for ループで実現している実装例を紹介されているサイトさんも多いですね!

ソート範囲を終了条件にするで紹介した bubbleSort 関数の do while ループを for ループに置き換えると下記のようになります。

forループのみでの実装
/*
 * バブルソートを行う関数
 * a:ソートしたいデータを格納した配列
 * num:ソートしたいデータの個数
 */
void bubbleSort(int a[], int num) {

    int i;
    int tmp;
    int size; /* ソート範囲のデータの個数 */

    /* ソートする範囲を狭めながらソート */
    for (size = num; size > 1; size--) {

        /* 先頭のデータから最後尾の1つ手前のデータまでループ */
        for (i = 0; i < size - 1; i++) {

            /* 隣り合うデータの大小関係を確認 */
            if (a[i] > a[i + 1]) {
                /* 大小関係が逆なら2つのデータを交換 */
                tmp = a[i];
                a[i] = a[i + 1];
                a[i + 1] = tmp;

            }
        }

    }

}

再帰呼び出しを用いる

また、2重ループの形を取らなくても、再帰呼び出しを行ってソートするやり方もあります。

バブルソートプログラムのソースコードで紹介した bubbleSort 関数を、再帰呼び出しを用いて実現したものが下記になります。

再帰呼び出しを用いた実装
/*
 * バブルソートを行う関数
 * a:ソートしたいデータを格納した配列
 * num:ソートしたいデータの個数
 */
void bubbleSort(int a[], int num) {

    int exchange; /* 交換回数 */
    int i;
    int tmp;

    exchange = 0;

    /* 先頭のデータから最後尾の1つ手前のデータまでループ */
    for (i = 0; i < num - 1; i++) {

        /* 隣り合うデータの大小関係を確認 */
        if (a[i] > a[i + 1]) {
            /* 大小関係が逆なら2つのデータを交換 */
            tmp = a[i];
            a[i] = a[i + 1];
            a[i + 1] = tmp;

            /* 交換が行われた回数をカウントアップ */
            exchange++;
        }
    }

    if (exchange > 0) {
        /* 交換が発生している間はソート続ける */
        bubbleSort(a, num);
    } 

}

まとめ

このページでは、ソートアルゴリズムの1つである「バブルソート」について解説しました。

バブルソートとは、下記のように処理を行うことでソートを行うアルゴリズムになります。

  1. 下記をデータの集合の先頭から最後尾まで1つずつデータをずらしながら繰り返し行う
    • 隣り合うデータの大小関係を比較する
      • 大小関係が逆の場合:その2つのデータを交換する
  2. 一度でも交換が行われたのであれば、1. に戻る

要は「隣り合うデータの大小関係が逆の場合に、その2つのデータを交換する」を繰り返すことでデータをソートするアルゴリズムになります。

このページでも紹介したように実装自体は非常に簡単に行うことができます。

また様々な方法で実装したバブルソート関数を紹介することで、一つのアルゴリズムを実現するにもいろんな実装が存在することも実感していただけたのでは無いかと思います!

ソートアルゴリズムにはこのバブルソート以外にも様々なものが存在し、本サイトでは、バブルソート以外にも下記のソートについても解説しています。

  • クイックソート
  • マージソート
  • 選択ソート
  • 挿入ソート
  • ヒープソート

下記リンク先でそれぞれについて解説していますので、他のソートアルゴリズムにも興味のある方は是非こちらも読んでみてください!

クイックソート解説ページのアイキャッチクイックソートを図を使って分かりやすく解説(C言語サンプルプログラム付き) マージソート解説ページのアイキャッチマージソートを図を使って分かりやすく解説(C言語サンプルプログラム付き) 選択ソート解説ページのアイキャッチ選択ソートを図を使って分かりやすく解説(C言語サンプルプログラム付き) 挿入ソートの解説ページアイキャッチ挿入ソートを図を使って分かりやすく解説(C言語サンプルプログラム付き) ヒープソートの解説ページアイキャッチヒープソートを図を使って分かりやすく解説(C言語サンプルプログラム付き)

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です