2023 年度「計算数学a」 2023-05-19

§6.1 乱数(続き)

(6.1.1) 今後、1000000 ぐらいの大きさの配列を乱数で初期化することがあり、 値が 0 から 32767 (= 215 - 1) まででは、 同じ値を持つものが多く現れて、都合が悪い場合がある。 そこで、rand() を二回呼ぶことを考える。 二度めに得た値は、32768 (= 215) 倍して一度めの値に加え、 さらに 1 を加える。 これで、1 から 1073741824 (= 230) までに値をとる乱数が得られる。

(一度めでなく二度めを 32768 倍するのは、 (4.8.5) で述べたように第 1 項が種に強く依存するからである(が、 しろうとの浅知恵かもしれない)。)

関数 int myrand(void) の void については (6.3.6) で説明する。

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int myrand(void);   /* 自家製の乱数発生関数 */

int main() {
  int i;

  {   /* この中カッコの中(乱数の種の設定)はわからなくてもよい */
    unsigned seed = (unsigned)time(NULL);   /* 現在時刻を取得して */
    printf("乱数の種は %u です.\n", seed);  /* その値を出力 */
    srand(seed);                            /* それを乱数の種に */
  }

  for (i = 0; i < 20; i++) {
    printf("%10d\n", myrand());             /* 乱数を出力 */
  }
}


int myrand(void) {      /* 1 から 1073741824 までに値をとる乱数を返す */
  int r;

  r = rand();
  return rand() * 32768 + r + 1;
}

§6.2 整列(ソート)とは

(6.2.1) 以上で準備を終わり、この授業のテーマである「整列」にはいる。

 88 98 20 82 31 53 22 10 12 91 92 79 95 50 19 58

整列とは、上のような有限(実)数列が与えられたとき、 それらを小さい順に並べかえることである。 整列は「ソート (sort)」とも言う。 ただし、「整列」「sort」の日常用語としての意味は少し違う。

(6.2.2) ※ 実際の仕事でコンピュータを使う際には、 数だけを並べかえるのでは意味がないのが普通である。 たとえば、 受験者一人一人に対応するデータ 「受験番号、国語の得点、数学の得点、英語の得点、合計点」 の組が受験者の数だけあり、それらを合計点の順に並べかえる、 というような操作をすることが多いであろう。 これから考えるのは、(いわば)合計点だけを並べかえるもので、 実地にはあまり役立たない。 また、Excel などの表計算ソフトはソート機能を持っているので、 応用上はそれらを使えばよい。

(6.2.3) ※ 同じ値があった場合にどちらを先にもってくるかは、 「どちらでもよい」としておく。

実用上は、問題になる場合がある。 昔、試験のあと、 成績優秀者の得点と氏名を掲示したことがあった。 名簿の順に並んでいるデータを得点順に並べかえて上から何人かを選ぶのだが、 もしも同点の学生がいた場合は左のように名簿の順にしておくのが普通である。

    100 点 伊藤*、田中**、藤村**        100 点 藤村**、伊藤*、田中**

     95 点 ...                                95 点 ...

そうではなく、右のように書いたとすると 「同じ 100 点だけど藤村君が先頭にきているのは何か理由があるのかな」 と思われるのでうまくないかもしれない。

§6.3 ソートプログラムの準備

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define N 8

void init(void);
int myrand(void);
void print(void);

int a[N+1];     /* これで a[1] ... a[N] が使える。a[0] は使わない */

int main() {
  printf("配列の大きさは %d です.  ", N);     /* 配列の大きさを出力 */
  printf("…………田中美佐子\n");             /* 自分の氏名を出力 */
  init();                                     /* 初期化 */
  print();                                    /* 画面表示 */
  print();                                    /* 画面表示(もう一度)*/
}


void init(void) {
  int i;

  {   /* この中カッコの中(乱数の種の設定)はわからなくてもよい */
    unsigned seed = (unsigned)time(NULL);   /* 現在時刻を取得して */
    printf("乱数の種は %u です.\n", seed);
    srand(seed);                            /* それを乱数の種に */
  }

  a[0] = 0;                       /* 念のためこうしておく */

  if (N < 80) {                   /* N が小さいとき */
    for (i = 1; i <= N; i++) {
      a[i] = rand() % 99 + 1;
    }
  } else {                        /* N を大きくして実験するとき */
    for (i = 1; i <= N; i++) {
      a[i] = myrand();
    }
  }
  return;                         /* この return は普通は書かない */
}


int myrand(void) {      /* 1 から 1073741824 までに値をとる乱数を返す */
  int r;

  r = rand();
  return rand() * 32768 + r + 1;
}


void print(void) {
  /* (6.3.9) を読んで、ここを書く */
}

(6.3.1) 上のプログラム例を見ながら、以下の説明を読むこと。 このプログラムは完全ではないので、 指示に従って自分で書き加える必要がある。 長いので、コピペすることを強くすすめる。

(6.3.2) 数列は、int 型の配列 a[ ] に入れる。 その大きさ N は、 容易に変えられるよう、 #define を使って書いた。

(6.3.3) C言語の配列は添え字が 0 から始まるので、普通は a[0] から a[N-1] までを使うことになるが、 わかりやすくするため a[1] から a[N] までを使いたい。 それには、 「int a[N+1];」と宣言することで a[0] から a[N] までを用意し、 最初の一つ、a[0] は使わなければよい。

(6.3.4) 「自分の氏名を出力」の部分は、 「田中美佐子」を自分の氏名(として大学に届けてあるもの)に変えること。 (これは、採点時の便宜のために入れてもらうものである。)

(6.3.5) 初めに、init() という自作関数でこの配列を「初期化」する。 初期化とは、一般には初めのデータを代入することである。 ここでは乱数で初期化する。

(6.3.6) 「void init(void) {」で始まっているが、 最初の void はこの関数が値を返さないことを示す。 値は返さないがある働きをする、という関数である。 小カッコの中の void は、引数がないことを示す。 使うときは「init();」のように使う。 小カッコの中には何も書かない。 すでに出てきた rand() がそうだった。

(6.3.7) N の値によって初期化を変える理由を説明する。 考えながらプログラムを書いている間は、N の値を小さくとり、 ソートの過程で配列の値を全て出力して、 ソートが進んでいることを確認する。 この段階では、 配列を初期化する際に代入する値は 100 未満で十分だし、 そのほうが画面上での大小の比較が容易である。 また、あとで説明するが、 これらの値は 1 以上としておくとプログラムの間違いを見つけやすい。 以上から、値の範囲は 1 以上 99 以下とした。

(6.3.8) プログラムが完成したら、 N の値を 1000000 ぐらいまで大きくしての実験もおこなう。 その際には、 同じ値で初期化された項がたくさんあるとうまくない場合があるので、 値の範囲は広ければ広いほどよい。 よって、配列には myrand() の返した値をそのまま代入し、 値の範囲を 1 以上 1073741824 以下とする。

(6.3.9) プログラムの末尾にある関数 print() の本体を書け。 この関数は、配列に格納されている値を (6.2.1) に示したようなスタイルで、 すなわち、スペースで区切って一行に出力し、最後に改行するものとする。 数が一桁のときも二桁分の幅に出力されるようにせよ。 そのとき、十の位を 0 とするかスペースにするかは各自の趣味にまかせる。 (printf() の中で、 %d の代わりに %02d または %2d と書けばよい。)

(6.3.10) main() では init() に続いて print() を二度呼んでいるので、 起動するたびに異なる乱数列が表示されるプログラムができたはずである。 (同じ乱数列が二行、出力される。)

§6.4 互換をおこなう関数 swap()

(6.4.1) ソートプログラムの多くは、 互換、 すなわち配列の二つの要素の交換からできている。 そこで、まず、互換をおこなう関数を書こう。

(6.4.2) §6.3 で完成させたプログラムに、 関数 void swap(int i, int j) を書き加えよ。 この関数は、 要素 a[i] と a[j] の値を入れかえるものとする。 i と j が正しい範囲にあるかどうかはチェックしなくてよろしい。 プロトタイプ宣言もつけ加えること。 (最初は i と j とは等しくないと仮定して書いてみて、 書けたものが i と j が等しいときも正しく動くかどうか考えよ。)

(6.4.3) 次はうまくゆかない例である。

  a[i] = a[j];
  a[j] = a[i];

一つのコップにオレンジジュースが、 もう一つのコップにグレープフルーツジュースがはいっているとき、 これらの中身を入れ替えるにはどうしたらよいか? と考えてみよ。

(6.4.4) この関数を呼び出してみなければ、正しく書けたかどうかはわからない。 それには、main() の最後に以下の 2 行をつけ加えればよいだろう。

  swap(3, 8);
  print();

これで a[3] と a[8] が入れかわっていれば OK である。 swap(8, 3) や swap(3, 3) も試すこと。

(6.4.5) ※ swap(-1, 8) や swap(3, N+1) を実行するプログラムは、 配列の添え字が範囲外になるので誤ったプログラムである。 そのことを理解したうえで、そういうプログラムを書いて実行してみよ。 ここのコンピュータでは、エラーメッセージなどは出ない。 前者では a[8] に、後者では a[3] に、 配列の“外”にあった値がはいってくることになるが、 それは 0 であることが多い。確かめよ。

(6.4.6) ※ プログラムを書く際、間違って swap(-1, 8) や swap(3, N+1) を実行するようなプログラムを書いてしまうことはよくある。 配列を初期化する際、値の範囲を 1 から 99 までとしておいたのは、 print() が 0 を出力したら間違いだとすぐにわかるためであった。 a[0] に 0 を代入しておいたのも同様の理由からである。

§6.5 比較をおこなう関数 comp()

(6.5.1) 配列の要素 a[i] と a[j] を比較するには if (a[i] > a[j]) のように書けばよいわけだが、 あとの都合で、次のような関数にしておこう。

int comp(int i, int j) {
  return a[i] - a[j];
}

こう定義したうえで if (comp(i, j) > 0) と書けば、 スピードの点で若干劣るが、同じことである。 プロトタイプ宣言も付け足しておくこと。

(6.5.2) main() の最後に次のように書き足して実験してみよう。

  if (comp(1, 2) > 0) {
    swap(1, 2);
  }
  print();

どうなれば実験成功かは各自で考えよ。

§6.6 素朴なソート

(6.6.1) ソートの「アルゴリズム」について考えてみよう。 「アルゴリズム」とは、 問題を解くための手順をあいまいさのないように書いたもののことである。

(6.6.2) まずは、「素朴なソート」を考える。 これは特定のアルゴリズムの名前ではなく、 プログラミングの経験を少し積んだ人がちょっと考えれば思いつくものを呼ぶ、 一般的な名前である。

§6.7 ソートのアルゴリズムの良し悪し

(6.7.1) ソートのアルゴリズムの良し悪しは、 比較回数(=配列の中の二つの要素を比較する回数)、 交換回数(=配列の中の二つの要素の互換をおこなう回数) を配列の大きさ N の関数とみなして判定する。 少ない回数で済むほうがよいアルゴリズムである。

(6.7.2) オーダーを表すオミクロン記法を復習しよう。 ここでは N→∞ のときだけを考える。 N の関数 f(N) と g(N) があって、 「N→∞ のとき |f(N)/g(N)| は有界」なら、 f(N) = Ο(g(N)) と書き、 「f(N) のオーダーは g(N) のオーダー以下である」などと言う。 たとえば N(N-1)/2 = Ο(N2) である。

(6.7.3) アルゴリズムの良し悪しを論ずる際には、 必要な回数を配列の大きさ N の関数とみなしたときのオーダーがまず第一に問題となる。 オーダーが同じ場合は係数が次の問題となる。 すなわち、同じ Ο(N2) であった場合には、 N2/4 のほうが N2/2 よりも優れたアルゴリズムとなる。

(6.7.4) Ο(N log(N)) のアルゴリズムは Ο(N2) のアルゴリズムでもあるわけだが、 わざわざ悪く言うこともないのでそれを 「Ο(N2) のアルゴリズム」と呼ぶことは滅多にない。

(6.7.5) 比較回数と交換回数とでオーダーが違うこともあるが、 その場合は オーダーの大きいほうが全体のオーダーを決める。 たとえば、 Ο(N2) である関数 N2/2 と Ο(N) である関数 N とを足した N2/2 + N が Ο(N2) ではあるが Ο(N) ではないように。

§6.8 素朴なソートの例(その1)―― 挿入ソート

(6.8.1) 挿入ソートは、次の原理で配列をソートする。

  1. ソート開始時点で、a[1] から a[1] まではソートされている。 (これは事実を確認しただけであり、 何らかの操作をおこなうわけではない。 「--」はまだ調べていないという印である。)
    48------------
  2. a[1] から a[i-1] まではソートされているとき、 a[i] をしかるべき位置に挿入して、 a[1] から a[i] までがソートされているようにする。
    0305489213----
    ↓
    0305134892----
    (ここでは i は 5 である。 元の a[3], a[4] はそれぞれ一つずつ右にずらした。)

(6.8.2) 図書カード ―― 数学コース閲覧室にもあるような、図書室の蔵書を検索する際に使うカード ―― が(たとえば)著者名順に並んでいるところへ、 新しく到着した本のカードを追加するときは、 まさにこの二段目をおこなうだろう。 すなわち、すでに並んでいるカードを新しいカードがはいるべきところで二組に分け、 そこに新しいカードを挿入する。

(6.8.3) 図書カードのときは、 新しく挿入したカードよりも「大」なるカードはカード一枚の厚みの分だけ奥にずれる。 しかし、コンピュータではそう簡単にはいかないので、次のようにおこなう。

(6.8.4) つまり、二段目の各ステップ(= a[i] を挿入するステップ)は

とする。 ただし、逆転していなかったらそこで打ち切る。 そこが挿入すべき位置だから。

(6.8.5) 上の例ではこうなる。

0305489213----
↓
0305481392----
↓
0305134892----

次に 05 と 13 とを比較するが、 ここは逆転していないので、これで操作は終わりである。 その先の比較はする必要がない。

(6.8.6) 極めて重要なヒント: for ループを使い、継続条件を 「j > 1 かつ a[j-1] が a[j] より大きい限り」 とする。「かつ」は C 言語では && と書くのだった。 j の値は 1 ずつ減ってゆく。

(6.8.7) そして、これを、i が 2 から N まで、 i の値を 1 ずつ大きくしながらおこなってゆく。 だから、for ループは二重になる。

(6.8.8) 初めからソートされていた場合が最も lucky な場合であり、 比較回数は N-1, 交換回数は 0 である。 逆順にソートされていた場合が最も unlucky な場合であり、 a[i] を挿入する際の比較回数・交換回数はどちらも i-1 となって、全体ではどちらもおよそ N2/2 となる。 平均的な場合には、 a[i] を挿入するときの比較・交換回数(の期待値) はどちらもおよそ i/2 であろう。 よって、全体では、 比較・交換回数ともに最も unlucky な場合の半分、 すなわち N2/4 とみてよいだろう。 どちらも Ο(N2) である。 §6.13, §6.14 で述べる二つのアルゴリズムとは異なり、 もしも配列がほとんどソートされていれば比較回数も交換回数も小さいことに注意しておこう。

§6.9 課題1

(6.9.1) 前からの準備を元にして、以下のヒントを読みつつ、 乱数列で初期化された配列を挿入ソートでソートするプログラムを書け。 そして、メールでレポートせよ。 ファイル名は自由だが、insert.c がよいかも知れない。 (英語では insertion sort と呼ばれるらしい。)

(6.9.2) 元にするのは、上で書いた、print() の本体を書き、 swap() と comp() を付け加えたソースファイルである。 これらの関数のテスト用として main() に書き加えた部分などは削除する。 すなわち、main() は次のようにする。

int main() {
  printf("配列の大きさは %d です.  ", N);     /* 配列の大きさを出力 */
  printf("…………岡田奈々\n");               /* 自分の氏名を出力 */
  init();                                     /* 初期化 */
  print();                                    /* 画面表示 */
}

(6.9.3) これの、最後の「print()」と「}」との間に、(6.8.7) で述べた二重ループがはいる。詳しく言うと、次のようにする。

初期化したのちは、配列 a[ ] に収められた値は、 関数 swap() によってのみ変更する。 すなわち、a[i] = a[i+1] などとは書かない。 プログラムを完成させるまでは、 関数 swap() で値が入れ替わるごとに配列の値すべてを表示させたいので、 swap() の中で print() を呼び出す。 このときは、配列の大きさは 8 でよい。 途中で「いまから a[5] (= 48) を挿入します.」) のようなメッセージを出すのは、 出力結果に区切りを入れて見やすくするためである。 (次を参照。)

#include ...

int main() {
  int i, j;

   ...
  print();
  for (i = ?; i <= ?; i++) {
    printf("いまから a[%d] (= %02d) を挿入します.\n", i, a[i]);
    for (j = ?; j ? ? && ???; j??) {
      swap(?, ?);
    }
  }
}

...

void swap(int i, int j) {
  ...

  print();    /* 画面表示 */
}

(6.9.4) 正しくソートされることがわかったら、 次のように書き足して、比較回数、交換回数を調べる。

#include ...

int ccount = 0;     /* 比較(comp())のカウンタ。0 で初期化 */
int scount = 0;     /* 交換(swap())のカウンタ。0 で初期化 */

int main() {
  int i, j;

  printf("配列の大きさは %d です.  ", N);     /* 配列の大きさを出力 */
  printf("…………岡田奈々\n");               /* 自分の氏名を出力 */
  init();
  print();

  /* ここに、上で書いた二重ループによるソートの部分がくる */

  printf("%d 回比較しました.\n", ccount);     /* 比較回数を出力 */
  printf("%d 回交換しました.\n", scount);     /* 交換回数を出力 */
}


void swap(int i, int j) {
  ...

  print();    /* 画面表示 */
  scount++;   /* ここを通るたびにカウンタが増える */
}


int comp(int i, int j) {
  ccount++;   /* ここを通るたびにカウンタが増える */

  return a[i] - a[j];
}

関数 comp() が呼び出されるたびに変数 ccount の値が増されるので、 これで comp() が呼び出された回数がわかる。 変数 scount についても同様。

(6.9.5) 毎回「28 回比較しました」になる人は私を呼べ。 次に、配列の大きさを 78 とし、コマンドプロンプトのウィンドウを最大化して試す。 782/4 = 1521 である。 比較回数・交換回数が (6.8.8) に書かれた値に近ければよい。 もしも、 正しくソートされるが理論上の二倍近い比較回数になって直し方がわからない場合も私を呼べ。

(6.9.6) ここまでできたら、print() の *呼び出し* をコメントにし、 配列の出力はいっさいおこなわないようにする。 「いまから ... を挿入します.」の出力も同様にコメントとする。 なお、この授業で使っているコンパイラは実は C++ コンパイラなので、 C++ でのコメントの記号、すなわち「//」を使ってもよい。 これを書くと、そこから行の終わりまでがコメントになる、という約束である。 つまり、main() は

int main() {
   ...

  printf("配列の大きさは %d です.  ", N);     /* 配列の大きさを出力 */
  printf("…………岡田奈々\n");               /* 自分の氏名を出力 */
  init();                                     /* 初期化 */
//  print();                                    /* 画面表示 */

  for (i = ?; i <= ?; i++) {
//    printf("いまから a[%d] (= %02d) を挿入します.\n", i, a[i]);
    for (j = ?; j ? ? && ???; j??) {
      ...
      swap(?, ?);
    }
  }
   ...
}

のようになり、swap() は

void swap(int i, int j) {
  ...

//  print();    /* 画面表示 */
  scount++;   /* ここを通るたびにカウンタが増える */
}

のようになる。 (関数 print() は一度も呼び出されないが、 そのまま残しておいてよい。)

このときの出力は次のようになる。 配列の大きさ N をいくつに選ぶかは、下を参照。

配列の大きさは .... です.  …………岡田奈々
乱数の種は .......... です.
..... 回比較しました.
..... 回交換しました.

(6.9.7) メールは、@stu.kanazawa-u.ac.jp のアドレスから送れ。 メールの本文冒頭に、 学籍番号、名列番号、氏名(として大学に届けてあるもの)を忘れずに書け。 次に、レポートの内容を、「挿入ソートのプログラムと実験結果」程度に、 簡潔に書け。

(6.9.8) その次に、プログラムを載せよ。 その際、ファイルを *添付するのではなく* 本文の中にソースファイルを貼りつけよ。 プログラムは、下に指示するように、 N の値を三通りに変えてコンパイル・実行するが、 載せるプログラムは N が指定のうちで最大のものとせよ。 また、実験をおこなったプログラムそのものでなければならない。

(6.9.9) 比較回数・交換回数を調べる際には、 配列の大きさ N の値を 100, 1000, 10000 の三通りに変えること。 それぞれの N について最低でも5回ずつくり返して実験せよ。 その際、乱数の種が毎回異なるようにするため、 一秒以内に二度以上プログラムを動かすな。

(6.9.10) 実験結果すべて(最低でも 15 回分)を、まとめてコピペして送れ。 すなわち、プロンプトや「a」、 「gcc insert.c」を含んだままコピペせよ。 編集すると間違うおそれがあるからである。 ここのコマンドプロンプトからのコピーは、 「範囲指定して Enter キー」でできる。

(6.9.11) 感想などは書かなくてよい。書きたければ書いてもよい。

(6.9.12) 宛て先は私(岩瀬)のアドレス(§1.9 参照)である。 件名は「??? kadai1」(←全て半角文字、 ??? は自分の履修者番号、その後ろに半角スペース一つ、 kadai は小文字、 kadai と 1 との間にはスペースを入れない)とせよ。

(6.9.13) 出されたレポートは、なるべく早く採点し、必ず返事をメールで送る。

そこに「OK」と書かれていて初めて、その課題の点数を得たことになる。 「やり直し!」の場合、「OK」になるまで、訂正・加筆して、再提出せよ。 何回再提出しても、最後に「OK」になれば、 一度で「OK」になった人と同じ得点を与える。 なお、再提出の場合も、「あたかもそれが最初の提出であるかのように」書け。 (訂正箇所だけを送るのは不可。最初のメールを「転送」するのも不可。)

§6.10 ここからあとはオプション項目

(6.10.1) ここからあと(§6.10-6.14)は、 時間などに余裕がある人のためのオプション項目である。

§6.11 偏った初期化をしてみる

(6.11.1) 配列 a[ ] を初期化する際、わざと偏ったやり方でおこなったら、 比較回数、交換回数はどうなるだろうか?  「a[i] = i;」と書けば元々データはソートされていたことになる。 「a[i] = N + 1 - i;」なら逆順。 「a[i] = i + myrand() % 5;」とすれば 「だいたいそろっているが完全にはソートされていないデータ」になる。 (5 という数に特別な意味はない。)

§6.12 ソートが完了したことをチェックするルーチン

(6.12.1) 本当にソートできたかどうかチェックするルーチンをつけてみよう。 配列 a[ ] に 0 が代入されていないこと、および、 a[1] が a[2] 以下であること、 a[2] が a[3] 以下であること、 ………、 a[N-1] が a[N] 以下であること、 を順にチェックする。 違っていたらメッセージを画面に出力させる。 独立した関数にしてもよいし、 main() の最後に置いてもよいだろう。

§6.13 素朴なソートの例(その2)―― バブルソート

(6.13.1) 「a[i] > a[i+1] を満たす i があったら a[i] と a[i+1] とを交換する。これをくり返してゆく」 というのがこのソートのアイディアである。 幼稚園の先生が園児を背の順に並ばせるとき、 この方法を採用することもあるだろう。 そのような i のさがし方はいろいろ考えられる。 (そのうちのどれを採用するかをきちんと述べるまでは、 これをアルゴリズムとは呼べない。)

(6.13.2) 次に示すのはその一つのやり方である。

これで、最大の値をもつもの(のうちの一つ)が a[N] にはいることを(頭の中で)確かめよ。 (幼稚園の先生が園児を背の順に並ばせる過程でいえば、 どんな手順をとったことになるか?)

次に

これで、a[1] から a[N-1] までのうちで最大のもの (=配列全体で言えば二番目に大きいもの)が a[N-1] にはいった。 以下はこれのくり返し。

(6.13.3) このアルゴリズムでの比較回数は (N-1) + (N-2) + ... + 2 + 1 = N(N-1)/2, すなわち、 およそ N2/2 である。 交換回数は最も unlucky な場合(=完全に逆順に並んでいた場合)が比較回数と同じ、 最も lucky な場合(=元々ソートされていた場合)が 0 で、 平均するとおよそ N2/4 とみてよいだろう。 よって、全体では Ο(N2) のアルゴリズムである。

(6.13.4) 興味のある人はこのアルゴリズムでソートをおこなうプログラムを書いてみよ。

§6.14 素朴なソートの例(その3)―― 単純選択法

(6.14.1) a[1] から a[i] までのうちで最大のもの (の一つ)の添え字を求める操作を i-1 回の比較でおこなう方法が存在する。 このやり方は比較的容易だから説明を省略する。

(6.14.2)

これでソートが完了する。

(6.14.3) 全体の比較回数は (N-1) + (N-2) + ... + 1 = N(N-1)/2 回、 交換回数は N-1 回である。 (最大のものが最後にきていても、すなわち、交換の必要がなくても swap() を呼び出すほうが全体としては速い。 よって、 交換回数は「たかだか N-1 回」ではなくちょうど N-1 回である。)

(6.14.4) 以上から、 このソートの比較回数は Ο(N2) であり、 交換回数は Ο(N) であることがわかる。 全体では Ο(N2) のアルゴリズムである。

(6.14.5) 興味のある人はこのアルゴリズムでソートをおこなうプログラムを書いてみよ。

(6.14.6) ※ a[1] から a[i] までのうちで最大のもの(の一つ)の添え字を求める際には、 j < i なる j に対し a[1] から a[j] までで最大の値を持つもの(の一つ)の添え字を保持しておくわけだが、 より速いプログラムを書くなら、そこまでの最大値そのものも保持しておくほうがよい。 ただし、こうすると、前に書いた関数 comp() は使わないことになる。

付:scanf() の使いにくさについて

#include <stdio.h>

int main(){
  int a, b;

  scanf("%d %d", &a, &b);
  printf("a = %d, b = %d.\n", a, b);
}

上のように書くと、「3 2」のように、 スペースで区切って二つの整数を一度に入力できる。 しかし、間違えて「3, 2」のようにカンマで区切ると、 まったく違う数値になってしまう。

#include <stdio.h>

int main(){
  int a, b;

  scanf("%d, %d", &a, &b);
  printf("a = %d, b = %d.\n", a, b);
}

こんどは「3, 2」のようにカンマで区切って入力する。 カンマを抜かすとまずいことになる。

また、数を一つだけ打ち込んだ場合、 いきなり Enter キーを押した場合も、予期せぬ動きをする。

付:K&R2 の pdf ファイル

Kernighan, Brian W.; Ritchie, Dennis M. (1988). The C Programming Language (2nd ed.) の pdf ファイルがネット上で見つかります。 「kernighan ritchie language c pdf」を検索してみてください。

この本は純然たる入門書ではありませんが、 この授業でここまで学んだみなさんなら、読み始めることができます。 この本一冊を読み終えれば、「C 言語の全貌を学んだ」と胸をはって言えるでしょう。

この本の内容についての質問はいつでも歓迎です。


岩瀬順一