2009 年度「計算数学」 2010-01-22

§49 マージソート

(49.1) マージ (merge) ソートでは、 ソートすべきデータがはいっている配列 a[ ] のほかに、 それと同じ大きさの配列 b[ ] が必要となる。 そこが欠点であるが、アルゴリズムは比較的わかりやすい。

(49.2) 配列 a[ ]

を満たしているとする。 それを(ここでは)「4 ずつ並んでいる」と呼ぼう。 (最後に半端が出たら、それらも小さい順に並んでいるとする。)

(49.3) このとき、この配列全体をもう一つの配列 b[ ] にコピーし、

を満たしているようにする。 それには、次のようにすればよい。

(49.4) a[1] から a[8] までの部分で説明しよう。 まず a[1]a[5] を比べ、小さいほうを b[1] にコピーする。 もしも a[1] のほうがコピーされたなら、 次には a[2]a[5] を比較する。 いわば“勝ち抜き戦”である。 片方のグループの元をすべてコピーしてしまったら、 あとはもう一つのグループの残りを(比較することなしに)順にコピーする。

(49.5) 配列 a[ ] 全体に対し上の操作を行なって配列 b[ ] にコピーし終えたら、 それから、b[ ] 全体を a[ ] に、順序を変えずにコピーする。

(49.6) これで、配列 a[ ] を、 「4 ずつ並んでいる」から 「8 ずつ並んでいる」に変えることができた。

(49.7) 最初に配列 a[ ] を乱数で初期化した直後には、 この配列は「1 ずつ並んでいる」と言える。 それを上のような操作で「2 ずつ並んでいる」ように変え、 「4 ずつ並んでいる」ように変え、 ……とくりかえしてゆけば、やがて配列全体がソートされる。 このアルゴリズムを「マージソート」という。

(49.8) m ずつ並んでいる m 個のもの二組を小さい順に並べて別の場所にコピーする際、 次にコピーすべき元(げん)を決めるには、たかだか一回の比較で済む。 最後の一つは、残ったものだから、比較しなくても決まる。 よって、比較回数はたかだか 2m-1 回である。 コピーは 2m 回である。 これらは Ο(m) である。 よって、 「m ずつ並んでいる」から「2m ずつ並んでいる」に変えることは Ο(N) の計算量で可能である。 これを全部で約 log2(N) 回くりかえすから、 全体では Ο(N log(N)) である。

§50 課題ではない

(50.1) 課題ではないが、興味のある人は、マージソートのプログラムを書いてみよ。

(50.2) 挿入ソートのソースファイルをコピーしたものに、 下のプログラムをはりつけ、ヒントに従って埋めてゆく。 ファイル名は merge.c がよいだろう。

(50.3) 変数は、下のように、五つ用意する。

main() {
    int m, start, i, j, k;

    /* ここで、いつものように、配列の大きさの表示、配列 */
    /* の初期化、初期化直後の配列の画面表示を行なう     */

    for (m = 1; m ???; m = ???) {
        printf("「%d ずつ並んでいる」のを", m);
        printf("「%d ずつ並んでいる」ように変えます.\n", 2*m);
        for (start = 1; start ???; start = ???) {
            i = 0; j = 0; k = 0;
            for (    ; ????????????????????????????????;   ) {
                if (comp(start+i, start+m+j) < 0) {
                    b[???] = a[???];
                } else {
                    b[???] = a[???];
                }
            }
            for (    ; ??????????????????????;   ) {
                b[???] = a[???];
            }
            for (    ; ??????????????????????;   ) {
                b[???] = a[???];
            }
        }
        /* ここで、for ループを用いて、配列 b[ ] を a[ ] に */
        /* そっくりそのままコピーする */
        print();
    }
    /* ここで、いつものように比較回数の表示などを行なう */
}

(50.4) 「int a[N+1];」の次の行に「int b[N+1];」を入れるのを忘れずに。

(50.5) いままでのアルゴリズムとは異なり、要素の交換のくり返しではないから、 関数 swap() は使わない。よって、交換回数は調べない。 (関数 swap() や、最後の交換回数の出力の printf() 文は残しておいて構わない。)

(50.6) ※ 実は、b[ ] をそのまま a[ ] にコピーして戻すのは時間の無駄である。 その際、ついでに「2m ずつ並んでいる」のを「4m ずつ並んでいる」ように変えれば、 この無駄が省ける。 しかし、プログラムが長く煩雑になるか、まだ習っていない書き方が必要になる(と思う)。

§51 まとめ

(51.1) 以上で、ソートの学習は終わりとする。 初めのうちは非常に自明なプログラムを学び、 突然、初心者にはかなり高度といえる、 非自明なアルゴリズムに基づくプログラミングを学んだことになるが、 諸君の多くが、粘り強く課題に取り組んでくれたと思う。 その粘り強さを、今後、数学や物理学の勉強に生かしてほしい。

(51.2) 興味のある人は、取り上げたうちの、 計算量が Ο(N log(N)) のアルゴリズム --- ヒープソート、クイックソート、マージソート --- について、配列の大きさ N をさらにいろいろな値に変えて実験し、 N を大きくするとき N log(N) の定数倍に近づいてゆくかどうかを調べ、 もしそうなっていたら、その定数の値はいくつになるか、 求めてみてほしい。 それが小さいほど、速いアルゴリズムということになる。 そのほか、この三つのアルゴリズムにはそれぞれ長所・短所がある。 まとめてみるとよいだろう。

(51.3) ソートのような、問題の意味を理解するだけなら簡単な問題にも、 いろいろな解法があり、 工夫次第でスピードを上げることができる。 先人たちの研究結果を学ぶことの大切さを知ってほしい。

(51.4) また、いくら努力してもこえられない理論上の限界 (§40) がある、 ということを理解するのも大切である。

(51.5) 数学では、可能な操作は一瞬で終わったとして先へ進む。 たとえば、 「この行列の固有値を小さい順に並べて λ1 < λ2 < ... < λn とします」 のように。 このあたりの、ものの考え方の違いについても考えてみてほしい。

付録:センターの Linux の“裏”端末

センターの Linux の、あまり知られていない、“裏”端末を紹介する。

Ctrl+Alt+F1 で“裏”端末に表示が切り替わる。 ログインプロンプトが出ているので、 この実習用アカウントのアカウント名・パスワードでログインする。 プロンプトの出ているところで Ctrl+D を押せばログアウトできる。 ログインしたままで次に述べるように“表”に戻ることもできるが、 シャットダウンする前には、“裏”端末のほうはログアウトしておくほうがよい。

“表”に表示を戻すのは、Ctrl+Alt+F7 である。

Linux を使っていて画面全体がまったく動かなくなってしまった場合、 この“裏”端末からログインして、 ある方法で正常に戻すことができる場合がある。

具体的には、 悪さをしているプロセスを kill することになる。 そのやり方は、簡単には説明できないので、 センターの人を呼んできてお願いすることになるだろう。


岩瀬順一