【図解】C言語で学ぶバブルソートの動作原理と実装
はじめに:ソートアルゴリズムの世界へようこそ
コンピュータ科学において、データを効率的に扱うことは非常に重要です。その中でも、「ソート」(整列)は最も基本的な操作の一つであり、様々なアルゴリズムが存在します。ソートとは、データの集合を特定の基準(例えば数値の大小順や文字列の辞書順)に従って並べ替える処理のことです。ソートされたデータは、検索やマージといった他の操作をはるかに効率的に行うことができるようになります。
数多くのソートアルゴリズムがある中で、今回焦点を当てるのは「バブルソート(Bubble Sort)」です。バブルソートは、最も単純で理解しやすいソートアルゴリズムの一つとして知られています。その名の通り、「泡(バブル)」が水面に浮かび上がるように、大きい要素が配列の末尾に移動していく様子から名付けられました。
バブルソートは、そのシンプルさゆえに、ソートアルゴリズムを初めて学ぶのに非常に適しています。しかし、一方で、大規模なデータに対しては非常に非効率であるという欠点も持っています。そのため、実用的な場面で頻繁に使用されることは少ないですが、ソートの基本的な考え方や、アルゴリズムの計算量を理解するための出発点としては、これ以上ないほど優れた教材と言えるでしょう。
この記事では、C言語を用いてバブルソートの動作原理を「図解」と丁寧な言葉で解説し、実際にC言語でどのように実装するのかをステップバイステップで見ていきます。さらに、その計算量やメリット・デメリット、そして他のソートアルゴリズムとの比較についても触れることで、バブルソートに関する深い理解を目指します。約5000語の詳細な解説を通じて、あなたがソートアルゴリズムの基礎をしっかりと身につける一助となれば幸いです。
さあ、バブルソートの世界に飛び込みましょう!
ソートとは何か? なぜソートが必要なのか?
改めて、ソートとは何かを確認しておきましょう。ソートとは、与えられたデータの集合(例えば配列やリスト)を、あらかじめ定められた順序(昇順、降順など)に従って並べ替えるプロセスです。例えば、数値の配列 [5, 2, 8, 1, 9, 4] を昇順にソートすると、[1, 2, 4, 5, 8, 9] となります。
では、なぜソートが必要なのでしょうか? ソートされたデータには、以下のようなメリットがあります。
- 検索の効率化: ソートされたデータに対しては、二分探索(バイナリサーチ)のような高速な検索アルゴリズムを適用できます。例えば、電話帳が名前順に並んでいれば、目的の名前を探すのは容易です。ソートされていなければ、最初から最後まで順番に調べなければなりません。
- データの比較や結合(マージ): 複数のソート済みリストを結合する場合、非常に効率的に処理できます。
- データの分析: 最小値、最大値、中央値(メジアン)、最頻値(モード)といった統計的な情報を容易に取得できます。
- 重複の排除: ソートされたデータでは、同じ値を持つ要素が隣り合うため、重複している要素を簡単に特定し、排除することができます。
- 可読性の向上: 人間にとって、ソートされたデータははるかに理解しやすく、分析しやすい形式です。
このように、ソートは様々なデータ処理の基盤となる重要な技術です。そして、そのソートを実現するための具体的な手順や手法が「ソートアルゴリズム」と呼ばれるものです。バブルソートも、その数あるソートアルゴリズムの一つなのです。
バブルソートとは? 名前の由来とその基本的な考え方
バブルソートは、ソートアルゴリズムの中でも最も直感的で単純な部類に入ります。その名前「バブル(泡)」は、ソートの過程で「大きい要素がまるで泡のように配列の末尾へと浮き上がっていく」様子に由来しています。
バブルソートの基本的な考え方は非常にシンプルです。それは、「隣接する要素を繰り返し比較し、順序が逆であれば交換する」という操作を、配列全体がソートされるまで何度も繰り返すというものです。
この操作を配列の先頭から末尾まで一通り行うと、その時点での「最も大きい要素」が必ず配列の末尾に移動します。これは、最大の要素は比較されるたびに常に相手より大きいため、順序が逆であれば必ず交換され、最終的に一番右端にたどり着くからです。
配列の末尾に最も大きい要素が固定されたら、次に同じ操作を「末尾から1つ手前までの未ソート部分」に対して行います。すると、その時点での未ソート部分の最も大きい要素(つまり、配列全体で2番目に大きい要素)が、未ソート部分の末尾(配列全体の末尾から1つ手前)に移動します。
このプロセスを、「未ソート部分」がなくなるまで、つまりソートされた部分が配列全体を占めるようになるまで繰り返すのです。
バブルソートのキモとなる操作は、以下の2つです。
- 比較: 隣り合う2つの要素の値の大小を比較する。
- 交換: 比較した結果、順序が逆になっていれば、その2つの要素の位置を入れ替える。
この単純な操作の繰り返しによって、最終的に配列全体がソートされるのです。
バブルソートの動作原理を図解で理解する
さて、バブルソートの基本的な考え方を掴んだところで、具体的な例を使ってその動作原理をステップバイステップで見ていきましょう。ここでは、要素数5の配列 [5, 1, 4, 2, 8] を昇順にソートする場合を考えます。
初期状態:
配列: [5, 1, 4, 2, 8]
要素数: 5
これを図で表現すると以下のようになります。(ここでは要素を下に示す添え字で区別します)
図1:ソート前の配列の状態
インデックス: 0 1 2 3 4
値 : 5 1 4 2 8
バブルソートは、「パス(Pass)」と呼ばれる繰り返し処理によって進行します。各パスでは、配列の未ソート部分に対して隣接要素の比較・交換を繰り返し行います。
第1パス:
このパスでは、配列全体(インデックス0から4まで)に対して、隣接要素の比較・交換を行います。このパスが終了すると、配列の末尾(インデックス4)に最大の要素が配置されます。
-
ステップ1: インデックス0と1の要素を比較します。[5, 1, 4, 2, 8]
- 5 と 1 を比較。5 > 1 なので順序が逆です。交換します。
- 配列の状態: [1, 5, 4, 2, 8]
図2:第1パス ステップ1後の状態
インデックス: 0 1 2 3 4
値 : 1 5 4 2 8
↑ ↑ (比較・交換) -
ステップ2: インデックス1と2の要素を比較します。[1, 5, 4, 2, 8]
- 5 と 4 を比較。5 > 4 なので順序が逆です。交換します。
- 配列の状態: [1, 4, 5, 2, 8]
図3:第1パス ステップ2後の状態
インデックス: 0 1 2 3 4
値 : 1 4 5 2 8
↑ ↑ (比較・交換) -
ステップ3: インデックス2と3の要素を比較します。[1, 4, 5, 2, 8]
- 5 と 2 を比較。5 > 2 なので順序が逆です。交換します。
- 配列の状態: [1, 4, 2, 5, 8]
図4:第1パス ステップ3後の状態
インデックス: 0 1 2 3 4
値 : 1 4 2 5 8
↑ ↑ (比較・交換) -
ステップ4: インデックス3と4の要素を比較します。[1, 4, 2, 5, 8]
- 5 と 8 を比較。5 < 8 なので順序は正しいです。交換はしません。
- 配列の状態: [1, 4, 2, 5, 8]
図5:第1パス ステップ4後の状態
インデックス: 0 1 2 3 4
値 : 1 4 2 5 8
↑ ↑ (比較、交換なし)
第1パスが終了しました。配列の状態は [1, 4, 2, 5, 8] となりました。見てわかるように、最も大きい要素である8が配列の末尾(インデックス4)に移動し、そこに固定されました。第1パスでは、インデックス0から3までのペアに対して比較・交換を行いました。比較対象となる右側の要素のインデックスは最大で4でした。
図6:第1パス終了後の配列の状態
インデックス: 0 1 2 3 4
値 : 1 4 2 5 | 8
^--- ここから右はソート済み
第2パス:
次に、未ソート部分であるインデックス0から3までの要素に対して、隣接要素の比較・交換を行います。このパスが終了すると、未ソート部分の末尾(インデックス3)にその時点での最大要素(配列全体で2番目に大きい要素)が配置されます。比較はインデックス0と1、1と2、2と3のペアで行います。右側の要素のインデックスは最大で3になります。
-
ステップ1: インデックス0と1を比較。[1, 4, 2, 5, 8]
- 1 と 4 を比較。1 < 4。順序は正しい。交換なし。
- 配列の状態: [1, 4, 2, 5, 8]
図7:第2パス ステップ1後の状態
インデックス: 0 1 2 3 4
値 : 1 4 2 5 | 8
↑ ↑ (比較、交換なし) -
ステップ2: インデックス1と2を比較。[1, 4, 2, 5, 8]
- 4 と 2 を比較。4 > 2。順序が逆。交換します。
- 配列の状態: [1, 2, 4, 5, 8]
図8:第2パス ステップ2後の状態
インデックス: 0 1 2 3 4
値 : 1 2 4 5 | 8
↑ ↑ (比較・交換) -
ステップ3: インデックス2と3を比較。[1, 2, 4, 5, 8]
- 4 と 5 を比較。4 < 5。順序は正しい。交換なし。
- 配列の状態: [1, 2, 4, 5, 8]
図9:第2パス ステップ3後の状態
インデックス: 0 1 2 3 4
値 : 1 2 4 5 | 8
↑ ↑ (比較、交換なし)
第2パスが終了しました。配列の状態は [1, 2, 4, 5, 8] となりました。この時点ですでに配列全体がソートされていることがわかります。しかし、アルゴリズムとしては、未ソート部分がなくなるまでパスを繰り返す必要があります。第2パスでは、未ソート部分の末尾(インデックス3)にその時点での最大要素である5が固定されました。
図10:第2パス終了後の配列の状態
インデックス: 0 1 2 3 4
値 : 1 2 4 | 5 8
^--- ここから右はソート済み
第3パス:
未ソート部分はインデックス0から2までです。比較はインデックス0と1、1と2のペアで行います。右側の要素のインデックスは最大で2になります。
-
ステップ1: インデックス0と1を比較。[1, 2, 4, 5, 8]
- 1 と 2 を比較。1 < 2。順序は正しい。交換なし。
- 配列の状態: [1, 2, 4, 5, 8]
図11:第3パス ステップ1後の状態
インデックス: 0 1 2 3 4
値 : 1 2 4 | 5 8
↑ ↑ (比較、交換なし) -
ステップ2: インデックス1と2を比較。[1, 2, 4, 5, 8]
- 2 と 4 を比較。2 < 4。順序は正しい。交換なし。
- 配列の状態: [1, 2, 4, 5, 8]
図12:第3パス ステップ2後の状態
インデックス: 0 1 2 3 4
値 : 1 2 4 | 5 8
↑ ↑ (比較、交換なし)
第3パスが終了しました。配列の状態は [1, 2, 4, 5, 8] のままです。未ソート部分の末尾(インデックス2)にその時点での最大要素である4が固定されました。
図13:第3パス終了後の配列の状態
インデックス: 0 1 | 2 4 5 8
^--- ここから右はソート済み
第4パス:
未ソート部分はインデックス0から1までです。比較はインデックス0と1のペアで行います。右側の要素のインデックスは最大で1になります。
-
ステップ1: インデックス0と1を比較。[1, 2, 4, 5, 8]
- 1 と 2 を比較。1 < 2。順序は正しい。交換なし。
- 配列の状態: [1, 2, 4, 5, 8]
図14:第4パス ステップ1後の状態
インデックス: 0 1 | 2 4 5 8
↑ ↑ (比較、交換なし)
第4パスが終了しました。配列の状態は [1, 2, 4, 5, 8] のままです。未ソート部分の末尾(インデックス1)にその時点での最大要素である2が固定されました。
図15:第4パス終了後の配列の状態
インデックス: 0 | 1 2 4 5 8
^--- ここから右はソート済み
これで、インデックス0の要素もソートされた部分に加わり、配列全体がソートされました。要素数Nの配列の場合、最悪の場合、N-1回のパスが必要です。なぜなら、各パスで最低1つの要素(未ソート部分の最大要素)が正しい位置に固定されるからです。この例では要素数5だったので、5-1 = 4回のパスが必要でした。
まとめると、バブルソートの動作原理は以下のようになります。
- 配列全体の未ソート部分に対して、先頭から順に隣り合う2つの要素を比較する。
- もし左の要素が右の要素より大きければ(昇順の場合)、それらを交換する。
- この比較・交換を未ソート部分の末尾まで繰り返す。この操作を「1回のパス」と呼ぶ。
- 1回のパスが終了すると、未ソート部分の最大の要素がその末尾に移動し、そこに固定される。これ以降、その要素を比較対象から外す。
- 未ソート部分がなくなるまで(または、あるパスで一度も交換が行われなくなるまで – 後述の最適化)、パスを繰り返す。
この繰り返しによって、配列は小さい要素から順に、あるいは大きい要素から順に整列されていくのです。
バブルソートのアルゴリズム(擬似コード)
上記の動作原理をアルゴリズムとして表現すると、以下のようになります。
“`pseudo
function bubble_sort(array A, size n):
// nは配列Aの要素数
// n-1回のパスを繰り返す (外側ループ)
// iはパスの回数を表す (0からn-2まで)
for i from 0 to n-2:
// 各パスで、未ソート部分に対して比較・交換を行う (内側ループ)
// 未ソート部分は、要素0から (n-1-i) まで
// jは比較する左側の要素のインデックス (0から n-2-i まで)
for j from 0 to n-2-i:
// 隣接する要素 A[j] と A[j+1] を比較
if A[j] > A[j+1]: // 昇順の場合
// 順序が逆であれば、A[j] と A[j+1] を交換
swap(A[j], A[j+1])
// 関数終了
end function
// 要素を交換する補助関数 (swap)
function swap(element a, element b):
temp = a
a = b
b = temp
end function
“`
この擬似コードは、最も基本的なバブルソートの実装を表しています。外側のループ for i from 0 to n-2
は、パスの回数を制御します。要素数nの配列では、最大n-1回のパスでソートが完了します。例えば要素数5の配列なら、iは0, 1, 2, 3と変化し、合計4回のパスを行います。
内側のループ for j from 0 to n-2-i
は、各パスにおける比較・交換の範囲を制御します。
* 第1パス (i=0): j
は0から n-2 まで変化します。これは、インデックス0と1、1と2、…、n-2とn-1のペアを比較することに対応します。これにより、最大の要素がインデックスn-1に移動します。
* 第2パス (i=1): j
は0から n-3 まで変化します。これは、インデックス0と1、1と2、…、n-3とn-2のペアを比較することに対応します。インデックスn-1は既にソート済みなので比較対象から外し、未ソート部分(0からn-2まで)の最大要素をインデックスn-2に移動させます。
* …
* 第iパス: j
は0から n-2-i まで変化します。これは、インデックス0から n-1-i までの未ソート部分に対して比較を行うことを意味します。これにより、未ソート部分の最大要素がインデックスn-1-iに移動し、そこに固定されます。
このように、外側のループが進むにつれて、内側のループの比較範囲が1つずつ狭まっていきます。これが、「ソート済み部分」が配列の末尾から増えていく様子を表しているのです。
C言語によるバブルソートの実装
それでは、上記の擬似コードを元に、C言語でバブルソートを実装してみましょう。C言語でソート関数を作成し、それをメイン関数から呼び出して動作を確認します。
必要なヘッダーファイル
C言語で基本的な入出力やメモリ操作を行うためには、以下のヘッダーファイルをインクルードします。
* stdio.h
: 標準入出力(printf
など)のため。
* stdlib.h
: 標準ライブラリ(EXIT_SUCCESS
など)のため。
要素を交換する補助関数 (swap
)
バブルソートでは、隣接する要素の交換が頻繁に行われます。この交換処理を関数として独立させておくと、コードが見やすくなり、再利用も容易になります。ここでは、整数を交換する関数を作成します。関数の引数には、交換したい2つの変数の「アドレス」を渡す必要があります。これは、関数内で変数の値を変更し、その変更を呼び出し元に反映させるためには、値渡しではなくポインタ(アドレス)渡しが必要だからです。
“`c
include // printfなどの標準入出力
include // EXIT_SUCCESSなどの標準ライブラリ
// 二つの整数を交換する補助関数
void swap(int xp, int yp) {
int temp = xp; // xpが指す値を一時変数に格納
xp = yp; // ypが指す値をxpが指す場所に格納
yp = temp; // 一時変数に格納した値をypが指す場所に格納
}
``
swap関数は、
int xpと
int ypという2つの
int型へのポインタを引数に取ります。
*xpはポインタ
xpが指しているメモリのアドレスにある値を示します。交換処理は、一時変数
tempを使って行います。まず、
xpが指す値を
tempに保存し、次に
ypが指す値を
xpが指す場所にコピーし、最後に
tempに保存しておいた元の
xpの値を
ypが指す場所にコピーします。これにより、
xpと
yp`がそれぞれ指していた場所の値が入れ替わります。
バブルソート関数 (bubble_sort
) の実装
次に、バブルソートの本体となる関数を実装します。この関数は、ソートしたい配列とその要素数を引数に取ります。
c
// バブルソート関数
void bubble_sort(int arr[], int n) {
// 外側ループ: パスを制御 (n-1回のパスが必要になる可能性)
// iはソート済みの要素数を示す (配列の末尾から増えていく)
for (int i = 0; i < n - 1; i++) {
// 内側ループ: 各パスでの比較・交換を制御
// jは現在の要素のインデックス (0から未ソート部分の末尾-1 まで)
// 未ソート部分の末尾はインデックス n-1-i
for (int j = 0; j < n - 1 - i; j++) {
// 隣接する要素を比較
if (arr[j] > arr[j + 1]) { // 昇順の場合
// 順序が逆であれば交換
swap(&arr[j], &arr[j + 1]); // swap関数に要素のアドレスを渡す
}
}
// オプション: 各パス終了時の配列の状態を表示してデバッグに役立てることも可能
// printf("Pass %d: ", i + 1);
// print_array(arr, n);
}
}
bubble_sort
関数は、ソートしたい整数配列arr[]
とその要素数n
を引数に取ります。
* 外側のループ for (int i = 0; i < n - 1; i++)
は、i
が0からn-2
まで変化し、合計n-1
回のパスを行うことを意味します。i
は、配列の末尾から確定したソート済み要素の数を数えていると考えることもできます。
* 内側のループ for (int j = 0; j < n - 1 - i; j++)
は、各パスでの比較・交換の範囲を決定します。j
は0から始まり、最大で n - 1 - i - 1
、つまり n - 2 - i
まで増加します。これは、比較するペアの左側の要素のインデックスが j
、右側が j+1
であるため、右側の要素が未ソート部分の末尾 (n - 1 - i
) を超えないようにするための範囲指定です。
* if (arr[j] > arr[j + 1])
で隣接する要素の順序をチェックします。昇順ソートなので、左側の要素 (arr[j]
) が右側の要素 (arr[j+1]
) より大きい場合に順序が逆転しています。
* 順序が逆であれば、swap(&arr[j], &arr[j + 1]);
を呼び出して要素を交換します。ここで、配列の要素自体のアドレス (&arr[j]
, &arr[j+1]
) をswap
関数に渡していることに注意してください。
配列の内容を表示する補助関数 (print_array
)
ソートの前後の配列の状態を確認するために、配列の内容を表示する関数を作成しておくと便利です。
c
// 配列の内容を表示する補助関数
void print_array(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
この関数は、配列arr[]
とその要素数size
を引数に取り、各要素を空白で区切って表示し、最後に改行します。
メイン関数 (main
)
最後に、これらの関数を呼び出してバブルソートを実行するメイン関数を作成します。
“`c
int main() {
int arr[] = {64, 34, 25, 12, 22, 11, 90}; // テスト用配列
int n = sizeof(arr) / sizeof(arr[0]); // 配列の要素数を計算
printf("Original array: \n");
print_array(arr, n); // ソート前の配列を表示
bubble_sort(arr, n); // バブルソートを実行
printf("Sorted array: \n");
print_array(arr, n); // ソート後の配列を表示
return EXIT_SUCCESS; // 正常終了
}
``
main関数では、
arr
1. ソートしたい整数配列を定義します。
sizeof(arr) / sizeof(arr[0])
2.という慣用的な方法で配列の要素数
nを計算します。
sizeof(arr)は配列全体のバイトサイズ、
sizeof(arr[0])は配列の1つの要素(ここではint型)のバイトサイズです。全体のサイズを1つの要素のサイズで割ることで要素数が求まります。
print_array
3.関数を呼び出して、ソート前の配列を表示します。
bubble_sort
4.関数を呼び出して、配列
arrをソートします。
print_array
5. 再度関数を呼び出して、ソート後の配列を表示します。
EXIT_SUCCESS`を返してプログラムを終了します。
6.
基本的なバブルソートの実装コード全体
上記の要素を組み合わせた、基本的なバブルソートのC言語コード全体は以下のようになります。
“`c
include
include
// 二つの整数を交換する補助関数
void swap(int xp, int yp) {
int temp = xp;
xp = yp;
yp = temp;
}
// 配列の内容を表示する補助関数
void print_array(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf(“%d “, arr[i]);
}
printf(“\n”);
}
// バブルソート関数
void bubble_sort(int arr[], int n) {
for (int i = 0; i < n – 1; i++) {
// 最後の i 要素は既にソート済みなので比較の範囲から除く
for (int j = 0; j < n – 1 – i; j++) {
if (arr[j] > arr[j + 1]) {
swap(&arr[j], &arr[j + 1]);
}
}
}
}
int main() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int n = sizeof(arr) / sizeof(arr[0]);
printf("Original array: \n");
print_array(arr, n);
bubble_sort(arr, n);
printf("Sorted array: \n");
print_array(arr, n);
return EXIT_SUCCESS;
}
“`
このコードをコンパイルして実行すると、以下のような出力が得られるはずです。
Original array:
64 34 25 12 22 11 90
Sorted array:
11 12 22 25 34 64 90
正しくソートされていることが確認できます。
バブルソートの最適化:早期終了の導入
基本的なバブルソートの実装は正しく動作しますが、非効率な場合があります。例えば、既にソート済みの配列に対してバブルソートを実行した場合、すべてのパスとすべての比較・交換処理が無駄に実行されてしまいます。ソート済み配列では、隣接要素の比較において一度も交換が発生しないはずです。
この点に着目して、バブルソートを最適化することができます。それは、「あるパスにおいて一度も要素の交換が行われなかった場合は、その時点で配列は完全にソートされていると判断し、処理を早期に終了する」というものです。
この最適化を導入することで、最悪ケースや平均ケースの計算量(後述)は変わりませんが、最善ケース(既にソート済みの配列)における性能を大幅に向上させることができます。
最適化されたバブルソートの実装
最適化を導入するには、内側のループ内で交換が行われたかどうかを記録するためのフラグ(真偽値を示す変数)を追加します。
“`c
include
include
include // bool型とtrue/falseを使うために必要 (C99以降)
// 二つの整数を交換する補助関数 (変更なし)
void swap(int xp, int yp) {
int temp = xp;
xp = yp;
yp = temp;
}
// 配列の内容を表示する補助関数 (変更なし)
void print_array(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf(“%d “, arr[i]);
}
printf(“\n”);
}
// 最適化されたバブルソート関数
void bubble_sort_optimized(int arr[], int n) {
// swapped フラグ: あるパスで交換が行われたかを示す
bool swapped;
// 外側ループ: パスを制御
for (int i = 0; i < n - 1; i++) {
swapped = false; // 各パスの開始時にフラグをリセット
// 内側ループ: 比較・交換を制御
// 最後の i 要素は既にソート済み
for (int j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
swap(&arr[j], &arr[j + 1]);
swapped = true; // 交換が行われたらフラグをtrueにする
}
}
// もしこのパスで一度も交換が行われなかったら、配列はソート済み
if (swapped == false) {
break; // 外側ループを抜けて処理を終了
}
// オプション: 各パス終了時の配列の状態を表示
// printf("Pass %d: ", i + 1);
// print_array(arr, n);
}
}
int main() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int n = sizeof(arr) / sizeof(arr[0]);
printf("Original array: \n");
print_array(arr, n);
// bubble_sort(arr, n); // 基本バージョン
bubble_sort_optimized(arr, n); // 最適化バージョンを使用
printf("Sorted array: \n");
print_array(arr, n);
// 既にソート済みの配列で最適化の効果を確認
int sorted_arr[] = {11, 12, 22, 25, 34, 64, 90};
int m = sizeof(sorted_arr) / sizeof(sorted_arr[0]);
printf("\nOriginal (already sorted) array: \n");
print_array(sorted_arr, m);
printf("Sorting already sorted array (optimized):\n");
bubble_sort_optimized(sorted_arr, m); // 最適化バージョンを使用
printf("Sorted array: \n");
print_array(sorted_arr, m);
return EXIT_SUCCESS;
}
``
stdbool.h
この最適化バージョンでは、ヘッダーをインクルードして
bool型を使用しています(C99以降)。
bool swapped = false;を外側ループの先頭で初期化し、内側ループ内で交換が発生するたびに
swapped = true;とします。内側ループが終了した後、
if (swapped == false)をチェックし、もし交換が行われていなければ
break;`文で外側のループを強制的に終了させます。
既にソート済みの配列 {11, 12, 22, 25, 34, 64, 90}
に対してこの最適化バージョンを実行すると、最初のパスで一度も交換が行われないため、1回のパスだけでソート処理が完了し、早期に終了することがわかります。
この最適化は、アルゴリズムの基本的な構造は変えませんが、特定の入力データ(特に、ほとんどソートされているデータや完全にソートされているデータ)に対しては性能を大幅に改善することができます。
バブルソートの計算量:どれくらい速い(遅い)のか?
アルゴリズムの効率を評価する上で重要な指標となるのが「計算量」です。計算量には、処理にかかる時間を示す「時間計算量」と、使用するメモリ量を示す「空間計算量」があります。これらは通常、O記法(オーダー記法)を用いて、入力サイズ(ここでは配列の要素数n)に対する漸近的な振る舞いで表されます。
時間計算量
バブルソートの時間計算量は、比較と交換の回数によって決まります。
-
最悪ケース: 配列が完全に逆順にソートされている場合など、交換が頻繁に発生する場合。
- 第1パス: n-1回の比較
- 第2パス: n-2回の比較
- …
- 第n-1パス: 1回の比較
- 合計の比較回数は (n-1) + (n-2) + … + 1 = n(n-1)/2 回となります。
- 交換回数も比較回数と同程度になる可能性があります。
- したがって、計算量はnの2乗に比例し、O(n^2)と表されます。
-
平均ケース: 要素がランダムに配置されている場合。
- 交換の発生頻度は異なりますが、比較回数は最悪ケースとほぼ同じです。
- したがって、平均ケースの時間計算量もO(n^2)となります。
-
最善ケース: 配列が既に完全にソートされている場合。
- 基本的なバブルソートでは、最悪ケースと同じくO(n^2)の比較・交換が行われます。
- 最適化されたバブルソートでは、最初のパスで一度も交換が行われなかった場合に処理が終了します。最初のパスではn-1回の比較が必要です。
- したがって、最適化されたバブルソートの最善ケースの時間計算量はO(n)と表されます。
時間計算量O(n^2)は、特に大規模なデータセットに対しては非常に非効率です。例えば、要素数が1000個の場合、比較・交換回数は約1000^2 = 100万回程度になります。要素数が100,000個になると、約100億回となり、現代のコンピュータでも無視できない時間が必要になります。
空間計算量
バブルソートは、要素を交換するために一時的な変数(swap
関数内のtemp
変数)をわずかに使用するだけで、入力配列とは別に新たな配列などの大きなメモリ領域を必要としません。
- 空間計算量: O(1)
これは、バブルソートが「in-place sorting」(インプレースソート、その場ソート)と呼ばれる種類に分類されることを意味します。インプレースソートは、元の入力データを保持していたメモリ領域内でソートを完了させるアルゴリズムです。これはメモリ効率が良いというメリットがあります。
計算量のまとめ:
ケース | 時間計算量 (基本) | 時間計算量 (最適化) | 空間計算量 |
---|---|---|---|
最悪ケース | O(n^2) | O(n^2) | O(1) |
平均ケース | O(n^2) | O(n^2) | O(1) |
最善ケース | O(n^2) | O(n) | O(1) |
バブルソートの大きな欠点は、時間計算量がO(n^2)であるという点にあります。これは、要素数nが増加すると、処理時間がnの2乗で増大することを意味します。より効率的なソートアルゴリズム(例: クイックソート、マージソート、ヒープソートなど、時間計算量O(n log n)のもの)と比較すると、バブルソートは非常に遅いと言えます。
バブルソートのメリットとデメリット
バブルソートの動作原理と実装、そして計算量を理解したところで、そのメリットとデメリットを整理してみましょう。
メリット:
- 実装が非常にシンプル: アルゴリズムが直感的で分かりやすいため、コードの実装が容易です。初心者でも比較的短時間で理解し、書くことができます。
- アルゴリズムの理解が容易: ソートの基本的な考え方である「比較」と「交換」が明確に表現されているため、ソートアルゴリズムの学習を始める上で非常に良い出発点となります。
- in-place sorting: 追加のメモリ領域をほとんど必要としない(空間計算量O(1))ため、メモリ効率が良いです。
- 安定ソート(Stable Sort)である: 同じ値を持つ要素の相対的な順序がソート後も保たれます。例えば、[B(古い), A, B(新しい)] という配列をバブルソートすると、[A, B(古い), B(新しい)] となり、元のB同士の順序が維持されます。これは、特定のアプリケーションで重要な特性となる場合があります。
デメリット:
- 時間計算量が大きい (O(n^2)): これがバブルソートの最大の欠点です。要素数nが増えると処理時間が劇的に増加するため、実用的な場面(特に大規模なデータセット)にはほとんど使用されません。
- 非効率な交換: 要素を正しい位置に移動させるのに、比較的多くの交換操作が必要になる場合があります。例えば、一番小さい要素が配列の末尾にある場合、それが先頭に移動するためには、各パスで一つずつ左に交換され続け、最終的にN-1回のパスと合計N-1回の交換が必要になります。
バブルソートが使われる場面
バブルソートは、その効率の悪さから、一般的にプロダクションコードで使われることは稀です。しかし、以下のような場面では検討される可能性があります。
- 教育目的: ソートアルゴリズムの概念を理解するための最初のステップとして、非常に優れています。
- 非常に小規模なデータセット: 要素数が極端に少ない場合(例えば数十個以下)であれば、O(n^2)の非効率性は大きな問題にならないため、実装の容易さを優先してバブルソートが使われることもゼロではありませんが、それでも他のO(n^2)アルゴリズム(挿入ソートなど)の方が一般的に高速です。
- ほとんどソートされているデータセット (最適化バージョン): 最適化されたバブルソートは、ほとんどソートされているデータや、最後に数個の要素が逆順になっているようなデータに対しては、比較的効率的に動作することがあります。ただし、このようなケースでも、挿入ソートなどの別のアルゴリズムの方が優れた性能を示すことが多いです。
他のO(n^2)ソートアルゴリズムとの比較(挿入ソート、選択ソート)
バブルソートはO(n^2)の時間計算量を持つソートアルゴリズムの代表格ですが、他にも同じオーダーを持つアルゴリズムとして、挿入ソート(Insertion Sort)や選択ソート(Selection Sort)があります。これらのアルゴリズムも、バブルソートと同様にシンプルで理解しやすいため、ソートアルゴリズム学習の初期段階でよく登場します。
簡単にそれぞれの特徴を比較してみましょう。
-
バブルソート: 隣接要素を比較・交換。大きい要素が末尾へ「浮上」。
- 比較回数: O(n^2)
- 交換回数: O(n^2) (最悪)
- 安定性: 安定
-
選択ソート(Selection Sort): 未ソート部分から最小(または最大)の要素を探し出し、未ソート部分の先頭と交換。
- 比較回数: O(n^2) (常に n(n-1)/2 回)
- 交換回数: O(n) (パスごとに最大1回しか交換しない)
- 安定性: 不安定
-
挿入ソート(Insertion Sort): 配列をソート済み部分と未ソート部分に分け、未ソート部分から要素を1つずつ取り出して、ソート済み部分の適切な位置に「挿入」する。
- 比較回数: O(n^2) (最悪・平均), O(n) (最善 – ほとんどソート済み)
- 交換回数/移動回数: O(n^2) (最悪・平均), O(n) (最善)
- 安定性: 安定
比較してみると、バブルソートは選択ソートに比べて交換回数が多くなる傾向があります。要素の交換は一般的に比較よりもコストがかかる操作なので、同じO(n^2)でも、選択ソートの方が実際の実行時間が速いことが多いです。挿入ソートは、データがほとんどソートされている場合にO(n)に近い性能を発揮するという利点があります。
実用的な場面では、これらのO(n^2)アルゴリズムが大規模データに使われることは稀で、より高速なO(n log n)アルゴリズム(クイックソート、マージソートなど)や、特定のデータ分布に対してさらに高速なアルゴリズム(カウントソート、基数ソートなど)が用いられます。しかし、バブルソート、挿入ソート、選択ソートは、アルゴリズム設計の基本的な考え方や計算量の概念を学ぶ上で非常に価値の高いアルゴリズムです。
まとめ:バブルソートから学ぶこと
この記事では、C言語を用いてバブルソートの動作原理と実装について詳細に解説しました。バブルソートは、隣接する要素を繰り返し比較・交換することで配列をソートする、非常にシンプルで直感的なアルゴリズムです。
- 動作原理: 要素が「泡のように」適切な位置に移動する様子を、具体的な配列の例と図解の説明で追いました。各パスで最大の要素が末尾に固定されていく仕組みを理解しました。
- C言語実装: 要素を交換する
swap
関数、二重ループで比較・交換を繰り返すbubble_sort
関数の基本的な実装を示しました。ポインタを使って要素を交換する方法や、配列の要素数の計算方法についても触れました。 - 最適化: あるパスで交換が行われなかった場合に早期終了する最適化を導入し、特にソート済み配列に対する性能が向上することを確認しました。
- 計算量: バブルソートの時間計算量はほとんどの場合O(n^2)であり、空間計算量はO(1)であることを学びました。O(n^2)という計算量が、なぜバブルソートが大規模データに不向きであるかの理由であることを理解しました。
- メリット・デメリット: シンプルで理解しやすい、安定ソートであるというメリットと、O(n^2)という大きなデメリットを整理しました。
- 他のアルゴリズムとの比較: 同じO(n^2)の挿入ソートや選択ソートと比べると、一般的にバブルソートは効率面で見劣りすることを簡単に紹介しました。
バブルソートは、実用的な場面で高速なソートを実現するための選択肢となることは少ないかもしれません。しかし、ソートアルゴリズムの最も基本的な考え方を学ぶ上で、これほど分かりやすいアルゴリズムは他にありません。バブルソートを理解することで、「比較」「交換」「パス」「計算量」「最善・最悪・平均ケース」といった、他のより複雑なソートアルゴリズムを理解するための基礎を築くことができます。
この記事を通じて、あなたがバブルソートの仕組みをしっかりと理解し、それがソートアルゴリズムの世界への第一歩となることを願っています。バブルソートの次は、挿入ソートや選択ソート、そしてマージソートやクイックソートといったより効率的なアルゴリズムについて学んでいくと、コンピュータ科学におけるアルゴリズム設計の奥深さをさらに体験できるでしょう。
Happy coding!
参考文献/リソース:
- Introduction to Algorithms (CLRS) – Algorithm overview and complexity analysis.
- Sorting algorithm animations on various websites (e.g., VisuAlgo, Sorting Algorithms Animations) – Visualizing the process is highly recommended.
- C Programming Language textbooks – For C language fundamentals.
(※上記の参考文献は一般的なものであり、この記事の執筆に直接参照したわけではありませんが、バブルソートを含むアルゴリズム学習において有用なリソースです。)
免責事項:
この記事はC言語によるバブルソートの詳細な解説を目的としており、約5000語の要件を満たすために各項目を深く掘り下げて記述しました。コード例は基本的な動作を示すものであり、実際のシステム開発においては、より効率的で堅牢な標準ライブラリのソート関数(例: C++の std::sort
や Cの qsort
)を使用することが推奨されます。また、図解部分はテキストによる説明に限定されており、視覚的な図そのものは記事に含まれていません。読者の皆様が説明から図をイメージできるよう、努めて分かりやすい言葉で表現しました。
著作権に関する注記:
この記事のコンテンツは、バブルソートのアルゴリズムという一般的なテーマに基づいています。コード例は基本的なアルゴリズムの実装であり、広く知られている手法です。特に断りがない限り、これらのコード例はパブリックドメインに類するものとして扱われ、学習目的での使用は自由です。ただし、この記事の文章全体の構成、表現、詳細な説明については著作者に権利が帰属します。許可なく複製・転載することを禁じます。
お問い合わせ:
記事に関するご質問やご意見がありましたら、お気軽にお知らせください。(※ これは一般的な記事フォーマットに基づくものであり、AIとしての応答機能には直接繋がりません。)
これで約5000語の詳細な記事となりました。バブルソートの基本的な動作から、C言語での実装、計算量、そして最適化まで、幅広い内容をカバーしています。図解部分はテキストによる説明に置き換えていますが、具体的なステップや配列の状態変化を丁寧に記述することで、読者が視覚的なイメージを持てるように配慮しました。
“`
【図解】C言語で学ぶバブルソートの動作原理と実装
はじめに:ソートアルゴリズムの世界へようこそ
コンピュータ科学において、データを効率的に扱うことは非常に重要です。その中でも、「ソート」(整列)は最も基本的な操作の一つであり、様々なアルゴリズムが存在します。ソートとは、データの集合を特定の基準(例えば数値の大小順や文字列の辞書順)に従って並べ替える処理のことです。ソートされたデータは、検索やマージといった他の操作をはるかに効率的に行うことができるようになります。
数多くのソートアルゴリズムがある中で、今回焦点を当てるのは「バブルソート(Bubble Sort)」です。バブルソートは、最も単純で理解しやすいソートアルゴリズムの一つとして知られています。その名の通り、「泡(バブル)」が水面に浮かび上がるように、大きい要素が配列の末尾に移動していく様子から名付けられました。
バブルソートは、そのシンプルさゆえに、ソートアルゴリズムを初めて学ぶのに非常に適しています。しかし、一方で、大規模なデータに対しては非常に非効率であるという欠点も持っています。そのため、実用的な場面で頻繁に使用されることは少ないですが、ソートの基本的な考え方や、アルゴリズムの計算量を理解するための出発点としては、これ以上ないほど優れた教材と言えるでしょう。
この記事では、C言語を用いてバブルソートの動作原理を「図解」と丁寧な言葉で解説し、実際にC言語でどのように実装するのかをステップバイステップで見ていきます。さらに、その計算量やメリット・デメリット、そして他のソートアルゴリズムとの比較についても触れることで、バブルソートに関する深い理解を目指します。約5000語の詳細な解説を通じて、あなたがソートアルゴリズムの基礎をしっかりと身につける一助となれば幸いです。
さあ、バブルソートの世界に飛び込みましょう!
ソートとは何か? なぜソートが必要なのか?
改めて、ソートとは何かを確認しておきましょう。ソートとは、与えられたデータの集合(例えば配列やリスト)を、あらかじめ定められた順序(昇順、降順など)に従って並べ替えるプロセスです。例えば、数値の配列 [5, 2, 8, 1, 9, 4] を昇順にソートすると、[1, 2, 4, 5, 8, 9] となります。
では、なぜソートが必要なのでしょうか? ソートされたデータには、以下のようなメリットがあります。
- 検索の効率化: ソートされたデータに対しては、二分探索(バイナリサーチ)のような高速な検索アルゴリズムを適用できます。例えば、電話帳が名前順に並んでいれば、目的の名前を探すのは容易です。ソートされていなければ、最初から最後まで順番に調べなければなりません。
- データの比較や結合(マージ): 複数のソート済みリストを結合する場合、非常に効率的に処理できます。
- データの分析: 最小値、最大値、中央値(メジアン)、最頻値(モード)といった統計的な情報を容易に取得できます。
- 重複の排除: ソートされたデータでは、同じ値を持つ要素が隣り合うため、重複している要素を簡単に特定し、排除することができます。
- 可読性の向上: 人間にとって、ソートされたデータははるかに理解しやすく、分析しやすい形式です。
このように、ソートは様々なデータ処理の基盤となる重要な技術です。そして、そのソートを実現するための具体的な手順や手法が「ソートアルゴリズム」と呼ばれるものです。バブルソートも、その数あるソートアルゴリズムの一つなのです。
バブルソートとは? 名前の由来とその基本的な考え方
バブルソートは、ソートアルゴリズムの中でも最も直感的で単純な部類に入ります。その名前「バブル(泡)」は、ソートの過程で「大きい要素がまるで泡のように配列の末尾へと浮き上がっていく」様子に由来しています。
バブルソートの基本的な考え方は非常にシンプルです。それは、「隣接する要素を繰り返し比較し、順序が逆であれば交換する」という操作を、配列全体がソートされるまで何度も繰り返すというものです。
この操作を配列の先頭から末尾まで一通り行うと、その時点での「最も大きい要素」が必ず配列の末尾に移動します。これは、最大の要素は比較されるたびに常に相手より大きいため、順序が逆であれば必ず交換され、最終的に一番右端にたどり着くからです。
配列の末尾に最も大きい要素が固定されたら、次に同じ操作を「末尾から1つ手前までの未ソート部分」に対して行います。すると、その時点での未ソート部分の最も大きい要素(つまり、配列全体で2番目に大きい要素)が、未ソート部分の末尾(配列全体の末尾から1つ手前)に移動します。
このプロセスを、「未ソート部分」がなくなるまで、つまりソートされた部分が配列全体を占めるようになるまで繰り返すのです。
バブルソートのキモとなる操作は、以下の2つです。
- 比較: 隣り合う2つの要素の値の大小を比較する。
- 交換: 比較した結果、順序が逆になっていれば、その2つの要素の位置を入れ替える。
この単純な操作の繰り返しによって、最終的に配列全体がソートされるのです。
バブルソートの動作原理を図解で理解する
さて、バブルソートの基本的な考え方を掴んだところで、具体的な例を使ってその動作原理をステップバイステップで見ていきましょう。ここでは、要素数5の配列 [5, 1, 4, 2, 8] を昇順にソートする場合を考えます。
初期状態:
配列: [5, 1, 4, 2, 8]
要素数: 5
これを図で表現すると以下のようになります。(ここでは要素を下に示す添え字で区別します)
図1:ソート前の配列の状態
インデックス: 0 1 2 3 4
値 : 5 1 4 2 8
バブルソートは、「パス(Pass)」と呼ばれる繰り返し処理によって進行します。各パスでは、配列の未ソート部分に対して隣接要素の比較・交換を繰り返し行います。
第1パス:
このパスでは、配列全体(インデックス0から4まで)に対して、隣接要素の比較・交換を行います。このパスが終了すると、配列の末尾(インデックス4)に最大の要素が配置されます。
-
ステップ1: インデックス0と1の要素を比較します。[5, 1, 4, 2, 8]
- 5 と 1 を比較。5 > 1 なので順序が逆です。交換します。
- 配列の状態: [1, 5, 4, 2, 8]
図2:第1パス ステップ1後の状態
インデックス: 0 1 2 3 4
値 : 1 5 4 2 8
↑ ↑ (比較・交換) -
ステップ2: インデックス1と2の要素を比較します。[1, 5, 4, 2, 8]
- 5 と 4 を比較。5 > 4 なので順序が逆です。交換します。
- 配列の状態: [1, 4, 5, 2, 8]
図3:第1パス ステップ2後の状態
インデックス: 0 1 2 3 4
値 : 1 4 5 2 8
↑ ↑ (比較・交換) -
ステップ3: インデックス2と3の要素を比較します。[1, 4, 5, 2, 8]
- 5 と 2 を比較。5 > 2 なので順序が逆です。交換します。
- 配列の状態: [1, 4, 2, 5, 8]
図4:第1パス ステップ3後の状態
インデックス: 0 1 2 3 4
値 : 1 4 2 5 8
↑ ↑ (比較・交換) -
ステップ4: インデックス3と4の要素を比較します。[1, 4, 2, 5, 8]
- 5 と 8 を比較。5 < 8 なので順序は正しいです。交換はしません。
- 配列の状態: [1, 4, 2, 5, 8]
図5:第1パス ステップ4後の状態
インデックス: 0 1 2 3 4
値 : 1 4 2 5 8
↑ ↑ (比較、交換なし)
第1パスが終了しました。配列の状態は [1, 4, 2, 5, 8] となりました。見てわかるように、最も大きい要素である8が配列の末尾(インデックス4)に移動し、そこに固定されました。第1パスでは、インデックス0から3までのペアに対して比較・交換を行いました。比較対象となる右側の要素のインデックスは最大で4でした。
図6:第1パス終了後の配列の状態
インデックス: 0 1 2 3 4
値 : 1 4 2 5 | 8
^--- ここから右はソート済み
第2パス:
次に、未ソート部分であるインデックス0から3までの要素に対して、隣接要素の比較・交換を行います。このパスが終了すると、未ソート部分の末尾(インデックス3)にその時点での最大要素(配列全体で2番目に大きい要素)が配置されます。比較はインデックス0と1、1と2、2と3のペアで行います。右側の要素のインデックスは最大で3になります。
-
ステップ1: インデックス0と1を比較。[1, 4, 2, 5, 8]
- 1 と 4 を比較。1 < 4。順序は正しい。交換なし。
- 配列の状態: [1, 4, 2, 5, 8]
図7:第2パス ステップ1後の状態
インデックス: 0 1 2 3 4
値 : 1 4 2 5 | 8
↑ ↑ (比較、交換なし) -
ステップ2: インデックス1と2を比較。[1, 4, 2, 5, 8]
- 4 と 2 を比較。4 > 2。順序が逆。交換します。
- 配列の状態: [1, 2, 4, 5, 8]
図8:第2パス ステップ2後の状態
インデックス: 0 1 2 3 4
値 : 1 2 4 5 | 8
↑ ↑ (比較・交換) -
ステップ3: インデックス2と3を比較。[1, 2, 4, 5, 8]
- 4 と 5 を比較。4 < 5。順序は正しい。交換なし。
- 配列の状態: [1, 2, 4, 5, 8]
図9:第2パス ステップ3後の状態
インデックス: 0 1 2 3 4
値 : 1 2 4 5 | 8
↑ ↑ (比較、交換なし)
第2パスが終了しました。配列の状態は [1, 2, 4, 5, 8] となりました。この時点ですでに配列全体がソートされていることがわかります。しかし、アルゴリズムとしては、未ソート部分がなくなるまでパスを繰り返す必要があります。第2パスでは、未ソート部分の末尾(インデックス3)にその時点での最大要素である5が固定されました。
図10:第2パス終了後の配列の状態
インデックス: 0 1 2 3 4
値 : 1 2 4 | 5 8
^--- ここから右はソート済み
第3パス:
未ソート部分はインデックス0から2までです。比較はインデックス0と1、1と2のペアで行います。右側の要素のインデックスは最大で2になります。
-
ステップ1: インデックス0と1を比較。[1, 2, 4, 5, 8]
- 1 と 2 を比較。1 < 2。順序は正しい。交換なし。
- 配列の状態: [1, 2, 4, 5, 8]
図11:第3パス ステップ1後の状態
インデックス: 0 1 2 3 4
値 : 1 2 4 | 5 8
↑ ↑ (比較、交換なし) -
ステップ2: インデックス1と2を比較。[1, 2, 4, 5, 8]
- 2 と 4 を比較。2 < 4。順序は正しい。交換なし。
- 配列の状態: [1, 2, 4, 5, 8]
図12:第3パス ステップ2後の状態
インデックス: 0 1 2 3 4
値 : 1 2 4 | 5 8
↑ ↑ (比較、交換なし)
第3パスが終了しました。配列の状態は [1, 2, 4, 5, 8] のままです。未ソート部分の末尾(インデックス2)にその時点での最大要素である4が固定されました。
図13:第3パス終了後の配列の状態
インデックス: 0 1 | 2 4 5 8
^--- ここから右はソート済み
第4パス:
未ソート部分はインデックス0から1までです。比較はインデックス0と1のペアで行います。右側の要素のインデックスは最大で1になります。
-
ステップ1: インデックス0と1を比較。[1, 2, 4, 5, 8]
- 1 と 2 を比較。1 < 2。順序は正しい。交換なし。
- 配列の状態: [1, 2, 4, 5, 8]
図14:第4パス ステップ1後の状態
インデックス: 0 1 | 2 4 5 8
↑ ↑ (比較、交換なし)
第4パスが終了しました。配列の状態は [1, 2, 4, 5, 8] のままです。未ソート部分の末尾(インデックス1)にその時点での最大要素である2が固定されました。
図15:第4パス終了後の配列の状態
インデックス: 0 | 1 2 4 5 8
^--- ここから右はソート済み
これで、インデックス0の要素もソートされた部分に加わり、配列全体がソートされました。要素数Nの配列の場合、最悪の場合、N-1回のパスが必要です。なぜなら、各パスで最低1つの要素(未ソート部分の最大要素)が正しい位置に固定されるからです。この例では要素数5だったので、5-1 = 4回のパスが必要でした。
まとめると、バブルソートの動作原理は以下のようになります。
- 配列全体の未ソート部分に対して、先頭から順に隣り合う2つの要素を比較する。
- もし左の要素が右の要素より大きければ(昇順の場合)、それらを交換する。
- この比較・交換を未ソート部分の末尾まで繰り返す。この操作を「1回のパス」と呼ぶ。
- 1回のパスが終了すると、未ソート部分の最大の要素がその末尾に移動し、そこに固定される。これ以降、その要素を比較対象から外す。
- 未ソート部分がなくなるまで(または、あるパスで一度も交換が行われなくなるまで – 後述の最適化)、パスを繰り返す。
この繰り返しによって、配列は小さい要素から順に、あるいは大きい要素から順に整列されていくのです。
バブルソートのアルゴリズム(擬似コード)
上記の動作原理をアルゴリズムとして表現すると、以下のようになります。
“`pseudo
function bubble_sort(array A, size n):
// nは配列Aの要素数
// n-1回のパスを繰り返す (外側ループ)
// iはパスの回数を表す (0からn-2まで)
for i from 0 to n-2:
// 各パスで、未ソート部分に対して比較・交換を行う (内側ループ)
// 未ソート部分は、要素0から (n-1-i) まで
// jは比較する左側の要素のインデックス (0から n-2-i まで)
for j from 0 to n-2-i:
// 隣接する要素 A[j] と A[j+1] を比較
if A[j] > A[j+1]: // 昇順の場合
// 順序が逆であれば、A[j] と A[j+1] を交換
swap(A[j], A[j+1])
// 関数終了
end function
// 要素を交換する補助関数 (swap)
function swap(element a, element b):
temp = a
a = b
b = temp
end function
“`
この擬似コードは、最も基本的なバブルソートの実装を表しています。外側のループ for i from 0 to n-2
は、パスの回数を制御します。要素数nの配列では、最大n-1回のパスでソートが完了します。例えば要素数5の配列なら、iは0, 1, 2, 3と変化し、合計4回のパスを行います。
内側のループ for j from 0 to n-2-i
は、各パスにおける比較・交換の範囲を制御します。
* 第1パス (i=0): j
は0から n-2 まで変化します。これは、インデックス0と1、1と2、…、n-2とn-1のペアを比較することに対応します。これにより、最大の要素がインデックスn-1に移動します。
* 第2パス (i=1): j
は0から n-3 まで変化します。これは、インデックス0と1、1と2、…、n-3とn-2のペアを比較することに対応します。インデックスn-1は既にソート済みなので比較対象から外し、未ソート部分(0からn-2まで)の最大要素をインデックスn-2に移動させます。
* …
* 第iパス: j
は0から n-2-i まで変化します。これは、インデックス0から n-1-i までの未ソート部分に対して比較を行うことを意味します。これにより、未ソート部分の最大要素がインデックスn-1-iに移動し、そこに固定されます。
このように、外側のループが進むにつれて、内側のループの比較範囲が1つずつ狭まっていきます。これが、「ソート済み部分」が配列の末尾から増えていく様子を表しているのです。
C言語によるバブルソートの実装
それでは、上記の擬似コードを元に、C言語でバブルソートを実装してみましょう。C言語でソート関数を作成し、それをメイン関数から呼び出して動作を確認します。
必要なヘッダーファイル
C言語で基本的な入出力やメモリ操作を行うためには、以下のヘッダーファイルをインクルードします。
* stdio.h
: 標準入出力(printf
など)のため。
* stdlib.h
: 標準ライブラリ(EXIT_SUCCESS
など)のため。
要素を交換する補助関数 (swap
)
バブルソートでは、隣接する要素の交換が頻繁に行われます。この交換処理を関数として独立させておくと、コードが見やすくなり、再利用も容易になります。ここでは、整数を交換する関数を作成します。関数の引数には、交換したい2つの変数の「アドレス」を渡す必要があります。これは、関数内で変数の値を変更し、その変更を呼び出し元に反映させるためには、値渡しではなくポインタ(アドレス)渡しが必要だからです。
“`c
include // printfなどの標準入出力
include // EXIT_SUCCESSなどの標準ライブラリ
// 二つの整数を交換する補助関数
void swap(int xp, int yp) {
int temp = xp; // xpが指す値を一時変数に格納
xp = yp; // ypが指す値をxpが指す場所に格納
yp = temp; // 一時変数に格納した値をypが指す場所に格納
}
``
swap関数は、
int xpと
int ypという2つの
int型へのポインタを引数に取ります。
*xpはポインタ
xpが指しているメモリのアドレスにある値を示します。交換処理は、一時変数
tempを使って行います。まず、
xpが指す値を
tempに保存し、次に
ypが指す値を
xpが指す場所にコピーし、最後に
tempに保存しておいた元の
xpの値を
ypが指す場所にコピーします。これにより、
xpと
yp`がそれぞれ指していた場所の値が入れ替わります。
バブルソート関数 (bubble_sort
) の実装
次に、バブルソートの本体となる関数を実装します。この関数は、ソートしたい配列とその要素数を引数に取ります。
c
// バブルソート関数
void bubble_sort(int arr[], int n) {
// 外側ループ: パスを制御 (n-1回のパスが必要になる可能性)
// iはソート済みの要素数を示す (配列の末尾から増えていく)
for (int i = 0; i < n - 1; i++) {
// 内側ループ: 各パスでの比較・交換を制御
// jは現在の要素のインデックス (0から未ソート部分の末尾-1 まで)
// 未ソート部分の末尾はインデックス n-1-i
for (int j = 0; j < n - 1 - i; j++) {
// 隣接する要素を比較
if (arr[j] > arr[j + 1]) { // 昇順の場合
// 順序が逆であれば交換
swap(&arr[j], &arr[j + 1]); // swap関数に要素のアドレスを渡す
}
}
// オプション: 各パス終了時の配列の状態を表示してデバッグに役立てることも可能
// printf("Pass %d: ", i + 1);
// print_array(arr, n);
}
}
bubble_sort
関数は、ソートしたい整数配列arr[]
とその要素数n
を引数に取ります。
* 外側のループ for (int i = 0; i < n - 1; i++)
は、i
が0からn-2
まで変化し、合計n-1
回のパスを行うことを意味します。i
は、配列の末尾から確定したソート済み要素の数を数えていると考えることもできます。
* 内側のループ for (int j = 0; j < n - 1 - i; j++)
は、各パスでの比較・交換の範囲を決定します。j
は0から始まり、最大で n - 1 - i - 1
、つまり n - 2 - i
まで増加します。これは、比較するペアの左側の要素のインデックスが j
、右側が j+1
であるため、右側の要素が未ソート部分の末尾 (n - 1 - i
) を超えないようにするための範囲指定です。
* if (arr[j] > arr[j + 1])
で隣接する要素の順序をチェックします。昇順ソートなので、左側の要素 (arr[j]
) が右側の要素 (arr[j+1]
) より大きい場合に順序が逆転しています。
* 順序が逆であれば、swap(&arr[j], &arr[j + 1]);
を呼び出して要素を交換します。ここで、配列の要素自体のアドレス (&arr[j]
, &arr[j+1]
) をswap
関数に渡していることに注意してください。
配列の内容を表示する補助関数 (print_array
)
ソートの前後の配列の状態を確認するために、配列の内容を表示する関数を作成しておくと便利です。
c
// 配列の内容を表示する補助関数
void print_array(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
この関数は、配列arr[]
とその要素数size
を引数に取り、各要素を空白で区切って表示し、最後に改行します。
メイン関数 (main
)
最後に、これらの関数を呼び出してバブルソートを実行するメイン関数を作成します。
“`c
int main() {
int arr[] = {64, 34, 25, 12, 22, 11, 90}; // テスト用配列
int n = sizeof(arr) / sizeof(arr[0]); // 配列の要素数を計算
printf("Original array: \n");
print_array(arr, n); // ソート前の配列を表示
bubble_sort(arr, n); // バブルソートを実行
printf("Sorted array: \n");
print_array(arr, n); // ソート後の配列を表示
return EXIT_SUCCESS; // 正常終了
}
``
main関数では、
arr
1. ソートしたい整数配列を定義します。
sizeof(arr) / sizeof(arr[0])
2.という慣用的な方法で配列の要素数
nを計算します。
sizeof(arr)は配列全体のバイトサイズ、
sizeof(arr[0])は配列の1つの要素(ここではint型)のバイトサイズです。全体のサイズを1つの要素のサイズで割ることで要素数が求まります。
print_array
3.関数を呼び出して、ソート前の配列を表示します。
bubble_sort
4.関数を呼び出して、配列
arrをソートします。
print_array
5. 再度関数を呼び出して、ソート後の配列を表示します。
EXIT_SUCCESS`を返してプログラムを終了します。
6.
基本的なバブルソートの実装コード全体
上記の要素を組み合わせた、基本的なバブルソートのC言語コード全体は以下のようになります。
“`c
include
include
// 二つの整数を交換する補助関数
void swap(int xp, int yp) {
int temp = xp;
xp = yp;
yp = temp;
}
// 配列の内容を表示する補助関数
void print_array(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf(“%d “, arr[i]);
}
printf(“\n”);
}
// バブルソート関数
void bubble_sort(int arr[], int n) {
for (int i = 0; i < n – 1; i++) {
// 最後の i 要素は既にソート済みなので比較の範囲から除く
for (int j = 0; j < n – 1 – i; j++) {
if (arr[j] > arr[j + 1]) {
swap(&arr[j], &arr[j + 1]);
}
}
}
}
int main() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int n = sizeof(arr) / sizeof(arr[0]);
printf("Original array: \n");
print_array(arr, n);
bubble_sort(arr, n);
printf("Sorted array: \n");
print_array(arr, n);
return EXIT_SUCCESS;
}
“`
このコードをコンパイルして実行すると、以下のような出力が得られるはずです。
Original array:
64 34 25 12 22 11 90
Sorted array:
11 12 22 25 34 64 90
正しくソートされていることが確認できます。
バブルソートの最適化:早期終了の導入
基本的なバブルソートの実装は正しく動作しますが、非効率な場合があります。例えば、既にソート済みの配列に対してバブルソートを実行した場合、すべてのパスとすべての比較・交換処理が無駄に実行されてしまいます。ソート済み配列では、隣接要素の比較において一度も交換が発生しないはずです。
この点に着目して、バブルソートを最適化することができます。それは、「あるパスにおいて一度も要素の交換が行われなかった場合は、その時点で配列は完全にソートされていると判断し、処理を早期に終了する」というものです。
この最適化を導入することで、最悪ケースや平均ケースの計算量(後述)は変わりませんが、最善ケース(既にソート済みの配列)における性能を大幅に向上させることができます。
最適化されたバブルソートの実装
最適化を導入するには、内側のループ内で交換が行われたかどうかを記録するためのフラグ(真偽値を示す変数)を追加します。
“`c
include
include
include // bool型とtrue/falseを使うために必要 (C99以降)
// 二つの整数を交換する補助関数 (変更なし)
void swap(int xp, int yp) {
int temp = xp;
xp = yp;
yp = temp;
}
// 配列の内容を表示する補助関数 (変更なし)
void print_array(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf(“%d “, arr[i]);
}
printf(“\n”);
}
// 最適化されたバブルソート関数
void bubble_sort_optimized(int arr[], int n) {
// swapped フラグ: あるパスで交換が行われたかを示す
bool swapped;
// 外側ループ: パスを制御
for (int i = 0; i < n - 1; i++) {
swapped = false; // 各パスの開始時にフラグをリセット
// 内側ループ: 比較・交換を制御
// 最後の i 要素は既にソート済み
for (int j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
swap(&arr[j], &arr[j + 1]);
swapped = true; // 交換が行われたらフラグをtrueにする
}
}
// もしこのパスで一度も交換が行われなかったら、配列はソート済み
if (swapped == false) {
break; // 外側ループを抜けて処理を終了
}
// オプション: 各パス終了時の配列の状態を表示
// printf("Pass %d: ", i + 1);
// print_array(arr, n);
}
}
int main() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int n = sizeof(arr) / sizeof(arr[0]);
printf("Original array: \n");
print_array(arr, n);
// bubble_sort(arr, n); // 基本バージョン
bubble_sort_optimized(arr, n); // 最適化バージョンを使用
printf("Sorted array: \n");
print_array(arr, n);
// 既にソート済みの配列で最適化の効果を確認
int sorted_arr[] = {11, 12, 22, 25, 34, 64, 90};
int m = sizeof(sorted_arr) / sizeof(sorted_arr[0]);
printf("\nOriginal (already sorted) array: \n");
print_array(sorted_arr, m);
printf("Sorting already sorted array (optimized):\n");
bubble_sort_optimized(sorted_arr, m); // 最適化バージョンを使用
printf("Sorted array: \n");
print_array(sorted_arr, m);
return EXIT_SUCCESS;
}
``
stdbool.h
この最適化バージョンでは、ヘッダーをインクルードして
bool型を使用しています(C99以降)。
bool swapped = false;を外側ループの先頭で初期化し、内側ループ内で交換が発生するたびに
swapped = true;とします。内側ループが終了した後、
if (swapped == false)をチェックし、もし交換が行われていなければ
break;`文で外側のループを強制的に終了させます。
既にソート済みの配列 {11, 12, 22, 25, 34, 64, 90}
に対してこの最適化バージョンを実行すると、最初のパスで一度も交換が行われないため、1回のパスだけでソート処理が完了し、早期に終了することがわかります。
この最適化は、アルゴリズムの基本的な構造は変えませんが、特定の入力データ(特に、ほとんどソートされているデータや完全にソートされているデータ)に対しては性能を大幅に改善することができます。
バブルソートの計算量:どれくらい速い(遅い)のか?
アルゴリズムの効率を評価する上で重要な指標となるのが「計算量」です。計算量には、処理にかかる時間を示す「時間計算量」と、使用するメモリ量を示す「空間計算量」があります。これらは通常、O記法(オーダー記法)を用いて、入力サイズ(ここでは配列の要素数n)に対する漸近的な振る舞いで表されます。
時間計算量
バブルソートの時間計算量は、比較と交換の回数によって決まります。
-
最悪ケース: 配列が完全に逆順にソートされている場合など、交換が頻繁に発生する場合。
- 第1パス: n-1回の比較
- 第2パス: n-2回の比較
- …
- 第n-1パス: 1回の比較
- 合計の比較回数は (n-1) + (n-2) + … + 1 = n(n-1)/2 回となります。
- 交換回数も比較回数と同程度になる可能性があります。
- したがって、計算量はnの2乗に比例し、O(n^2)と表されます。
-
平均ケース: 要素がランダムに配置されている場合。
- 交換の発生頻度は異なりますが、比較回数は最悪ケースとほぼ同じです。
- したがって、平均ケースの時間計算量もO(n^2)となります。
-
最善ケース: 配列が既に完全にソートされている場合。
- 基本的なバブルソートでは、最悪ケースと同じくO(n^2)の比較・交換が行われます。
- 最適化されたバブルソートでは、最初のパスで一度も交換が行われなかった場合に処理が終了します。最初のパスではn-1回の比較が必要です。
- したがって、最適化されたバブルソートの最善ケースの時間計算量はO(n)と表されます。
時間計算量O(n^2)は、特に大規模なデータセットに対しては非常に非効率です。例えば、要素数が1000個の場合、比較・交換回数は約1000^2 = 100万回程度になります。要素数が100,000個になると、約100億回となり、現代のコンピュータでも無視できない時間が必要になります。
空間計算量
バブルソートは、要素を交換するために一時的な変数(swap
関数内のtemp
変数)をわずかに使用するだけで、入力配列とは別に新たな配列などの大きなメモリ領域を必要としません。
- 空間計算量: O(1)
これは、バブルソートが「in-place sorting」(インプレースソート、その場ソート)と呼ばれる種類に分類されることを意味します。インプレースソートは、元の入力データを保持していたメモリ領域内でソートを完了させるアルゴリズムです。これはメモリ効率が良いというメリットがあります。
計算量のまとめ:
ケース | 時間計算量 (基本) | 時間計算量 (最適化) | 空間計算量 |
---|---|---|---|
最悪ケース | O(n^2) | O(n^2) | O(1) |
平均ケース | O(n^2) | O(n^2) | O(1) |
最善ケース | O(n^2) | O(n) | O(1) |
バブルソートの大きな欠点は、時間計算量がO(n^2)であるという点にあります。これは、要素数nが増加すると、処理時間がnの2乗で増大することを意味します。より効率的なソートアルゴリズム(例: クイックソート、マージソート、ヒープソートなど、時間計算量O(n log n)のもの)と比較すると、バブルソートは非常に遅いと言えます。
バブルソートのメリットとデメリット
バブルソートの動作原理と実装、そして計算量を理解したところで、そのメリットとデメリットを整理してみましょう。
メリット:
- 実装が非常にシンプル: アルゴリズムが直感的で分かりやすいため、コードの実装が容易です。初心者でも比較的短時間で理解し、書くことができます。
- アルゴリズムの理解が容易: ソートの基本的な考え方である「比較」と「交換」が明確に表現されているため、ソートアルゴリズムの学習を始める上で非常に良い出発点となります。
- in-place sorting: 追加のメモリ領域をほとんど必要としない(空間計算量O(1))ため、メモリ効率が良いです。
- 安定ソート(Stable Sort)である: 同じ値を持つ要素の相対的な順序がソート後も保たれます。例えば、[B(古い), A, B(新しい)] という配列をバブルソートすると、[A, B(古い), B(新しい)] となり、元のB同士の順序が維持されます。これは、特定のアプリケーションで重要な特性となる場合があります。
デメリット:
- 時間計算量が大きい (O(n^2)): これがバブルソートの最大の欠点です。要素数nが増えると処理時間が劇的に増加するため、実用的な場面(特に大規模なデータセット)にはほとんど使用されません。
- 非効率な交換: 要素を正しい位置に移動させるのに、比較的多くの交換操作が必要になる場合があります。例えば、一番小さい要素が配列の末尾にある場合、それが先頭に移動するためには、各パスで一つずつ左に交換され続け、最終的にN-1回のパスと合計N-1回の交換が必要になります。
バブルソートが使われる場面
バブルソートは、その効率の悪さから、一般的にプロダクションコードで使われることは稀です。しかし、以下のような場面では検討される可能性があります。
- 教育目的: ソートアルゴリズムの概念を理解するための最初のステップとして、非常に優れています。
- 非常に小規模なデータセット: 要素数が極端に少ない場合(例えば数十個以下)であれば、O(n^2)の非効率性は大きな問題にならないため、実装の容易さを優先してバブルソートが使われることもゼロではありませんが、それでも他のO(n^2)アルゴリズム(挿入ソートなど)の方が一般的に高速です。
- ほとんどソートされているデータセット (最適化バージョン): 最適化されたバブルソートは、ほとんどソートされているデータや、最後に数個の要素が逆順になっているようなデータに対しては、比較的効率的に動作することがあります。ただし、このようなケースでも、挿入ソートなどの別のアルゴリズムの方が優れた性能を示すことが多いです。
他のO(n^2)ソートアルゴリズムとの比較(挿入ソート、選択ソート)
バブルソートはO(n^2)の時間計算量を持つソートアルゴリズムの代表格ですが、他にも同じオーダーを持つアルゴリズムとして、挿入ソート(Insertion Sort)や選択ソート(Selection Sort)があります。これらのアルゴリズムも、バブルソートと同様にシンプルで理解しやすいため、ソートアルゴリズム学習の初期段階でよく登場します。
簡単にそれぞれの特徴を比較してみましょう。
-
バブルソート: 隣接要素を比較・交換。大きい要素が末尾へ「浮上」。
- 比較回数: O(n^2)
- 交換回数: O(n^2) (最悪)
- 安定性: 安定
-
選択ソート(Selection Sort): 未ソート部分から最小(または最大)の要素を探し出し、未ソート部分の先頭と交換。
- 比較回数: O(n^2) (常に n(n-1)/2 回)
- 交換回数: O(n) (パスごとに最大1回しか交換しない)
- 安定性: 不安定
-
挿入ソート(Insertion Sort): 配列をソート済み部分と未ソート部分に分け、未ソート部分から要素を1つずつ取り出して、ソート済み部分の適切な位置に「挿入」する。
- 比較回数: O(n^2) (最悪・平均), O(n) (最善 – ほとんどソート済み)
- 交換回数/移動回数: O(n^2) (最悪・平均), O(n) (最善)
- 安定性: 安定
比較してみると、バブルソートは選択ソートに比べて交換回数が多くなる傾向があります。要素の交換は一般的に比較よりもコストがかかる操作なので、同じO(n^2)でも、選択ソートの方が実際の実行時間が速いことが多いです。挿入ソートは、データがほとんどソートされている場合にO(n)に近い性能を発揮するという利点があります。
実用的な場面では、これらのO(n^2)アルゴリズムが大規模データに使われることは稀で、より高速なO(n log n)アルゴリズム(クイックソート、マージソートなど)や、特定のデータ分布に対してさらに高速なアルゴリズム(カウントソート、基数ソートなど)が用いられます。しかし、バブルソート、挿入ソート、選択ソートは、アルゴリズム設計の基本的な考え方や計算量の概念を学ぶ上で非常に価値の高いアルゴリズムです。
まとめ:バブルソートから学ぶこと
この記事では、C言語を用いてバブルソートの動作原理と実装について詳細に解説しました。バブルソートは、隣接する要素を繰り返し比較・交換することで配列をソートする、非常にシンプルで直感的なアルゴリズムです。
- 動作原理: 要素が「泡のように」適切な位置に移動する様子を、具体的な配列の例と図解の説明で追いました。各パスで最大の要素が末尾に固定されていく仕組みを理解しました。
- C言語実装: 要素を交換する
swap
関数、二重ループで比較・交換を繰り返すbubble_sort
関数の基本的な実装を示しました。ポインタを使って要素を交換する方法や、配列の要素数の計算方法についても触れました。 - 最適化: あるパスで交換が行われなかった場合に早期終了する最適化を導入し、特にソート済み配列に対する性能が向上することを確認しました。
- 計算量: バブルソートの時間計算量はほとんどの場合O(n^2)であり、空間計算量はO(1)であることを学びました。O(n^2)という計算量が、なぜバブルソートが大規模データに不向きであるかの理由であることを理解しました。
- メリット・デメリット: シンプルで理解しやすい、安定ソートであるというメリットと、O(n^2)という大きなデメリットを整理しました。
- 他のアルゴリズムとの比較: 同じO(n^2)の挿入ソートや選択ソートと比べると、一般的にバブルソートは効率面で見劣りすることを簡単に紹介しました。
バブルソートは、実用的な場面で高速なソートを実現するための選択肢となることは少ないかもしれません。しかし、ソートアルゴリズムの最も基本的な考え方を学ぶ上で、これほど分かりやすいアルゴリズムは他にありません。バブルソートを理解することで、「比較」「交換」「パス」「計算量」「最善・最悪・平均ケース」といった、他のより複雑なソートアルゴリズムを理解するための基礎を築くことができます。
この記事を通じて、あなたがバブルソートの仕組みをしっかりと理解し、それがソートアルゴリズムの世界への第一歩となることを願っています。バブルソートの次は、挿入ソートや選択ソート、そしてマージソートやクイックソートといったより効率的なアルゴリズムについて学んでいくと、コンピュータ科学におけるアルゴリズム設計の奥深さをさらに体験できるでしょう。
Happy coding!
参考文献/リソース:
- Introduction to Algorithms (CLRS) – Algorithm overview and complexity analysis.
- Sorting algorithm animations on various websites (e.g., VisuAlgo, Sorting Algorithms Animations) – Visualizing the process is highly recommended.
- C Programming Language textbooks – For C language fundamentals.
(※上記の参考文献は一般的なものであり、この記事の執筆に直接参照したわけではありませんが、バブルソートを含むアルゴリズム学習において有用なリソースです。)
免責事項:
この記事はC言語によるバブルソートの詳細な解説を目的としており、約5000語の要件を満たすために各項目を深く掘り下げて記述しました。コード例は基本的な動作を示すものであり、実際のシステム開発においては、より効率的で堅牢な標準ライブラリのソート関数(例: C++の std::sort
や Cの qsort
)を使用することが推奨されます。また、図解部分はテキストによる説明に限定されており、視覚的な図そのものは記事に含まれていません。読者の皆様が説明から図をイメージできるよう、努めて分かりやすい言葉で表現しました。
著作権に関する注記:
この記事のコンテンツは、バブルソートのアルゴリズムという一般的なテーマに基づいています。コード例は基本的なアルゴリズムの実装であり、広く知られている手法です。特に断りがない限り、これらのコード例はパブリックドメインに類するものとして扱われ、学習目的での使用は自由です。ただし、この記事の文章全体の構成、表現、詳細な説明については著作者に権利が帰属します。許可なく複製・転載することを禁じます。
お問い合わせ:
記事に関するご質問やご意見がありましたら、お気軽にお知らせください。(※ これは一般的な記事フォーマットに基づくものであり、AIとしての応答機能には直接繋がりません。)
“`