C言語のバブルソートを配列と交換で理解する
白本47Pの補足として、隣同士を比べて交換する処理を分解する
Copyright © 2026 LWP 山中 一弘
本資料は、出典を明記いただければ、商用・非商用を問わず、ご自由に複製・改変・再配布していただけます。なお、著作権表示は改変せず、そのまま記載してご利用くださいますようお願いいたします。
記事要約
バブルソートは、C言語の配列、添字、for文、if文、変数による値の交換をまとめて練習できる題材です。考え方は単純で、隣り合う2つの値を比べ、順番が逆なら入れ替えます。この処理を何度も繰り返すことで、大きい値が泡のように配列の後ろへ移動していきます。
しかし、コードとして読むと急に難しく見えることがあります。理由は、外側のfor文と内側のfor文があり、さらに a[j] と a[j + 1] のような添字が出てくるためです。どのループが何を担当しているのか、なぜ n - 1 - i で止めるのか、交換用の一時変数がなぜ必要なのかを分けて理解すると、バブルソートはかなり読みやすくなります。
この記事では、白本47Pの補足として、バブルソートを「並べ替えの暗記」ではなく「配列を安全に扱う練習」として読み直します。
本記事の対象とゴール
想定読者
C言語の配列とfor文を学び始めた人
バブルソートのコードを見て、添字の範囲で迷いやすい人
a[j] と a[j + 1] の比較が何をしているのか確認したい人
値の交換で一時変数が必要になる理由を整理したい人
本記事で得られること
バブルソートを、隣同士の比較と交換の繰り返しとして読めるようになります。
外側のfor文と内側のfor文の役割を分けて説明できるようになります。
n - 1 - i の境界条件を、暗記ではなく理由で理解できます。
C言語の配列添字、条件分岐、交換処理をまとめて復習できます。
本記事で扱わないこと
高速なソートアルゴリズムの比較
クイックソート、マージソート、ヒープソートの詳細
ポインタを使った汎用ソート関数の実装
標準ライブラリ qsort の使い方
先に結論
バブルソートで一番大事なのは、ソート名を覚えることではありません。配列の隣同士を比べ、必要なら交換するという小さな処理を、範囲を変えながら繰り返していると理解することです。
基本形は次のようになります。
void bubble_sort(int a[], int n){ int i; int j; for (i = 0; i < n - 1; i++) { for (j = 0; j < n - 1 - i; j++) { if (a[j] > a[j + 1]) { int temp = a[j]; a[j] = a[j + 1]; a[j + 1] = temp; } } }}内側のfor文は、隣同士を比べて交換する担当です。外側のfor文は、その作業を何回繰り返すかを決める担当です。
1回の内側ループが終わると、その時点で一番大きい値が右端へ移動します。右端はもう確定した場所なので、次の回ではそこまで見に行く必要がありません。そのため、内側ループの条件は j < n - 1 - i になります。
1. バブルソートは何をしているのか
バブルソートは、配列全体を一気に並べ替える処理ではありません。小さな処理を何度も繰り返して、結果として整列させる方法です。
小さな処理とは、次の2つです。
隣同士を比べる。
左の値が右の値より大きければ入れ替える。
たとえば、次の配列があるとします。
5 3 4 1左から順に、隣同士を比べます。
最初は 5 と 3 です。左の 5 の方が大きいので、入れ替えます。
3 5 4 1次は 5 と 4 です。左の 5 の方が大きいので、入れ替えます。
3 4 5 1次は 5 と 1 です。左の 5 の方が大きいので、入れ替えます。
3 4 1 5ここまでで、最大値の 5 が右端に移動しました。まだ全体は整列していませんが、右端だけは確定しています。
この「最大値が右端に移動する」性質を何回も使うのがバブルソートです。
2. 配列の添字を先に確認する
C言語の配列は、先頭を0番目として数えます。
int a[4] = {5, 3, 4, 1};このとき、各要素は次のように参照します。
a[0] は 5a[1] は 3a[2] は 4a[3] は 14個の要素がある場合、使える添字は 0 から 3 までです。a[4] は存在しません。
バブルソートでは、隣同士を比べるために a[j] と a[j + 1] を使います。このとき、j + 1 が配列の最後を超えてはいけません。
4個の配列なら、最後に比較してよいのは a[2] と a[3] です。つまり、j は最大でも 2 までです。
これを一般化すると、要素数が n のとき、最後に比較してよいのは a[n - 2] と a[n - 1] です。だから、内側のループでは j < n - 1 という形が基本になります。
3. 値を交換するときは一時変数が必要になる
隣同士の値を交換するコードは、次の形になります。
int temp = a[j];a[j] = a[j + 1];a[j + 1] = temp;ここで一時変数 temp を使う理由は、最初の値を失わないためです。
たとえば、a[j] が 5、a[j + 1] が 3 だとします。もし次のように書くと、うまく交換できません。
a[j] = a[j + 1];a[j + 1] = a[j];1行目で a[j] は 3 になります。この時点で、もともと a[j] に入っていた 5 は失われます。2行目で a[j + 1] に代入されるのも 3 なので、結果は両方とも 3 になってしまいます。
そのため、先に a[j] の値を temp に退避します。
temp = 5a[j] = 3a[j + 1] = 5値の交換は、バブルソートだけでなくC言語全体でよく出てくる基本動作です。ここを丁寧に理解しておくと、配列操作の読みやすさが上がります。
4. 内側のfor文は隣同士を比べる
内側のfor文は、配列の左から右へ進みながら、隣同士を比べる処理です。
for (j = 0; j < n - 1 - i; j++) { if (a[j] > a[j + 1]) { int temp = a[j]; a[j] = a[j + 1]; a[j + 1] = temp; }}if (a[j] > a[j + 1]) は、左の値が右の値より大きいかを調べています。昇順に並べたいので、左が大きければ順番が逆です。そこで交換します。
逆に、左の値が右の値以下であれば、すでにその2つは昇順になっています。その場合は何もしません。
この内側ループだけを見ると、やっていることは単純です。
左と右を見る逆なら交換する1つ右へ進むこの3つを、比較してよい範囲の最後まで繰り返します。
5. 外側のfor文は確定範囲を広げる
外側のfor文は、内側の比較と交換を何回繰り返すかを決めています。
for (i = 0; i < n - 1; i++) { ...}1回目の内側ループが終わると、最大値が右端に移動します。2回目が終わると、残りの中で最大の値が右から2番目に移動します。
つまり、右側から順に、整列済みの場所が増えていきます。
1回目の後: 右端が確定2回目の後: 右から2番目まで確定3回目の後: 右から3番目まで確定この確定済みの場所を、もう一度比較する必要はありません。そこで内側のループ条件に - i が入ります。
j < n - 1 - ii が0のときは、まだ確定済みの場所がありません。だから、最後の隣同士まで比較します。
i が1のときは、右端が確定済みです。だから、右端の1つ手前まで比較します。
i が2のときは、右側2つが確定済みです。だから、さらに1つ手前までで止めます。
このように考えると、n - 1 - i は暗記する式ではなく、「右側の確定済み部分を見に行かないための境界」です。
6. 4個の配列で追ってみる
次の配列を昇順に並べ替えます。
5 3 4 11回目
比較範囲は全体です。
5 と 3 を比較 -> 交換する3 5 4 15 と 4 を比較 -> 交換する3 4 5 15 と 1 を比較 -> 交換する3 4 1 5右端の 5 が確定しました。
2回目
右端は確定済みなので、そこまでは見ません。
3 と 4 を比較 -> 交換しない3 4 1 54 と 1 を比較 -> 交換する3 1 4 5右から2番目の 4 が確定しました。
3回目
右側2つは確定済みなので、左側だけを見ます。
3 と 1 を比較 -> 交換する1 3 4 5これで昇順になりました。
7. よくあるつまずき
`j <= n - 1` と書いてしまう
a[j] と a[j + 1] を比較するため、j + 1 が最後の添字を超えてはいけません。
要素数が n の配列で最後の添字は n - 1 です。したがって、j + 1 は最大でも n - 1 でなければなりません。
つまり、j は最大でも n - 2 です。for文では次のように書きます。
j < n - 1j <= n - 1 と書くと、最後に a[n - 1] と a[n] を比べようとしてしまいます。a[n] は配列の外側です。
交換処理で値を消してしまう
交換では、一時変数が必要です。
int temp = a[j];a[j] = a[j + 1];a[j + 1] = temp;一時変数を使わずに、先に a[j] を上書きしてしまうと、もとの値を失います。
外側と内側のループを同じものとして読んでしまう
外側の i は、何回目の通過かを表します。右側にどれだけ確定済みの値があるかにも対応します。
内側の j は、今どの隣同士を比べているかを表します。
この2つを分けると読みやすくなります。
i は回数j は比較位置バブルソートを効率のよい方法だと思ってしまう
バブルソートは、考え方を学ぶにはよい題材ですが、大量データの実用的なソートとしては効率がよいとは言えません。
ただし、学習題材としての価値は大きいです。配列、添字、比較、交換、二重ループ、境界条件が一つの短いコードにまとまっているためです。
8. 学習では何を見るべきか
バブルソートを学ぶときは、完成コードを丸暗記するより、次の観点で読む方が身につきます。
比較している2つの要素はどれか。
交換が必要な条件は何か。
交換するとき、元の値はどこに退避しているか。
内側ループはどこまで進むか。
外側ループが1回進むと、どの場所が確定するか。
この5つを説明できれば、バブルソートの理解はかなり安定します。
特にC言語では、添字の範囲を外すと未定義動作につながります。ソートそのものよりも、a[j + 1] を読んでよい範囲を常に意識することが大切です。
9. 改良版として早期終了を入れる
基本形では、すでに整列済みになっていても、外側ループを最後まで回します。学習が進んだら、交換が一度も発生しなかった回で終了する形も確認するとよいです。
void bubble_sort(int a[], int n){ int i; int j; for (i = 0; i < n - 1; i++) { int swapped = 0; for (j = 0; j < n - 1 - i; j++) { if (a[j] > a[j + 1]) { int temp = a[j]; a[j] = a[j + 1]; a[j + 1] = temp; swapped = 1; } } if (!swapped) { break; } }}swapped は、その回で交換が起きたかを記録する変数です。交換が一度も起きなかったなら、配列はすでに整列済みです。そこで break で外側ループを抜けます。
この改良版を見ると、ソート処理の中でも状態を記録しながら制御する考え方を学べます。
まとめ
バブルソートは、単純なソートアルゴリズムですが、C言語の基本を確認するにはとてもよい題材です。
大事なのは、二重ループを見て怖がることではありません。内側のループは隣同士の比較と交換、外側のループは確定済みの範囲を広げる役割だと分けて読むことです。
境界条件の n - 1 - i も、式だけを見ると覚えにくく感じます。しかし、a[j + 1] が配列の外に出ないこと、右側の確定済み部分をもう見ないことを考えれば、自然に出てくる形です。
白本47Pのバブルソートは、並べ替えの手順を覚えるページとしてだけでなく、配列を安全に扱う練習、値の交換を理解する練習、二重ループの役割分担を読む練習として使うと理解が深まります。
出典メモ
元リンクファイル: C:\Users\hoehoe\マイドライブ\LWP記事\03過去記号\エンジョイC\エンジョイC012回目(補足)白本47Pバブルソート – Dropbox Paper.URL
元URL: https://paper.dropbox.com/doc/C47P--BbGeOxHLLybrsU3yoP~k1jKEAg-0GrqW93WdZ7JA8kxQGMmV
Dropbox検索で確認した関連Paper: エンジョイC 47回目アジェンダ2021-03-14 13_00~.paper
取得日: 2026-05-24
備考: この環境ではDropbox Paper本文の自動抽出に失敗したため、リンクファイル名とDropbox検索で確認できた題名を手がかりに、白本47Pのバブルソート補足記事として再構成しました。
