Data & Algorithm 今回のお題:
『ソート (1) - 選択法、バブルソート、挿入ソート


●概要

アルゴリズムの勉強と言えば、まず最初に出てくるのが「ソーティングアルゴリズム」でしょう。
葬儀では『可哀想なジョージ。心臓病になるまではソートルーチンもちゃんと動いていたのに』 と言ってるのが本物のプログラマだ という言葉もありますしね〜。
# from "Real Programmers Don't Use PASCAL"

とりあえず、今回は基本となる (らしい) 3 種類です。

1. 選択法

浜工情報処理部で最初に習ったソート法です。 データの先頭から末尾に向かって、各要素に次のような処理を行います。

  1. 処理を行う要素を ai、最後の要素を an とする。 ai 〜 an の中で、最小または最大の要素を探す。 この要素を am とする。
  2. ai と am を入れ替える。
  3. 1. と 2. を i = 1, 2, ... n-2, n-1 として繰り返す。

簡単に言えば、「データの中から一番大きい / 小さいモノを次々に取っていく」 ということです。次々に選択して取っていくから、選択法。

ただ、我が浜工情報処理部では別の方法で習ったため、バブルソートとの区別が付きにくくなってしまいました。 処理部で教えてたコードはサンプルコードの中にあります。こっちの方がソースは短くなるんですが、交換回数は増えます。

2. バブルソート

日本語では「(隣接) 交換法」と言います。なんか訳分かんない名前ですが、方法は簡単です。 浜工情報処理部では 2 番目に習いました。その次がクイックソート。 同様に、データの先頭から末尾に向かって処理を行います。

  1. i = 1 (最初の要素)、m = n (最後の要素) とする。
  2. ai と ai+1 を比較して、ai の方がが大きかった (小さかった) ら、 ai と ai+1 を入れ替える。
    (大きかったら入れ替え: 昇順、小さかったら入れ替え: 降順)
  3. 2. を、i = 2, 3, ... m-2, m-1 として繰り返す。
  4. m を m = n-1, n-2, ... 3, 2 として、1.〜3. を繰り返す。

やっぱり文章だと解りづらいですね。基本的には「すぐ右と比較して交換」です。 これを繰り返すと、右端に最も大きい or 小さいデータが来ますから、次は残りのデータで同じ事をやって…… ってのを繰り替えしていけば、最終的には全てのデータが順番通りに並ぶことになります。 選択法とは違い、右端からだんだんソートされていきます。

「ソート済みの部分に、次々に値がくっついていって大きくなる」様子が、 水の泡が大きくなっていくのに似ているので「バブルソート」と言うのですが、 最初に聞いたときは「何じゃそりゃ?」と思いました。(苦笑

3. 挿入ソート

英語のまま「insertion sort」と言うこともあります。 でも、選択法を「selection sort」とはあまり呼びません。何故だ。手順は以下の通り。

  1. i = 2 から開始。
  2. ai-1 と ai を比較して、ai の方が大きかった (小さかった) ら入れ替える。
  3. i = 2 となるか、 交換が起こらなくなるまで、i を 1 ずつ減らしながら 2.を繰り返す。
  4. i = 3, 4, ... n-1, n で 2. と 3. を繰り返す。

既にソート済みの部分に、新しい要素を次々に挿入していくので、 挿入ソートといいます。挿入の様子はバブルソートに似ています。

このソートの特徴として、「部分的にソートされているデータに関して高速」というのがあります。 そのため、大まかなソートを他の方法でやってしまって、仕上げに挿入ソートを掛ける、ということが行われる……らしいです。

●実装

ソートの勉強で出てくる自作関数のプロトタイプって、たいてい

void hogesort( int array[], int num );

みたいな汎用性のかけらもないモノだったりするわけですけど、こんなモノを組んだって後々役に立つワケが無い。 というわけで、今回の関数のプロトタイプは

void bubblesort( void *base, size_t nel, size_t width, int( *compar )( const void *x, const void *y ) );

のような、qsort() と同じ引数を持つモノにしてみました。 これなら、後々何かに利用することがあっても問題なく使えます。

●サンプル

Cで書いてみたサンプル (download)
今回はCで書いてみました。ポインタでごりごり弄ってるので、一寸判り辛いかも知れません。

2000/07/24 追記: このサンプルの「挿入ソート」の方法が説明と違っています。 動作に支障はありませんが、「説明通りの」挿入ソートは ソート (2) に載っています。
2001/02/17 追記: 浜工 OB の銀の星さんにメールを頂きました。 メールの内容はこんな感じ。 C++ でえらいスマートに書いてあります。私も精進せねば……。
なお、selectsort1() が削除してあるのは、「あまりにも効率の悪いアルゴリズムなので掲載しない方が良い」 というご指摘を受けたためです。確かに、参照しても毒にしかなりませんな、アレは(苦笑

●備考

関数 swap() 中で作業領域用に動的メモリ確保をしていて、これが動作速度の 足枷になっていること間違いなし って感じなんですが、 やっぱり呼び出し側 (ソート本体か main() )で、作業領域を用意してやるのが良いんですかねぇ……。

shi3z大先生が実装を試していたらしい「最初にたっぷり動的確保して、あとはそれを自前で切り分けて配給」 っていうコードも組んでみたいですな。C++ で new をオーバーロードしてやれば出来るかな ?

こういうプログラムを組んでいていつも気になるのがエラー処理。

fprintf( stderr, "Memory arrocation error.\n" );

って感じのモノを、本なんかでよく見かけるのですが、 MS-DOS の場合の stdout と stderr の違いって何なんでしょう。(爆
ちなみに、今回は puts() でエラーメッセージ吐かせてます。ひぃ(汗

本当なら、C なら setjmp() 〜 longjmp()、C++ なら try 〜 catch 文で エラー処理をするとかが望ましいんでしょうけど、今回は特にそういうことはやっていません。

2001/02/17 追記: stdout はリダイレクトやらパイプで出力先を変更できるけど、 stderr は常にコンソールに表示される、ということのようです。 成る程、考えてみれば、stdout が「標準出力」で、リダイレクトやら何やらって 「標準入出力を他のストリームに変更する」機能でしたっけね。

戻る