2021 年度「計算数学a」 2021-11-05

§5.1 配列

(5.1.1) 変数名には、x11 のように数字がはいっても構わないが、 数字で始まる 1x などは不可。

(5.1.2) x1, x2, x3 とあったら人間は関連のある変数だろうと想像するが、 コンパイラにとっては全く無関係な、ただの三つの変数である。

(5.1.3) 配列を使った自明なプログラム。

#include <stdio.h>

int a[10];      /* 配列の宣言。これで a[0], ..., a[9] が使える */

int main() {
  int i;

  for (i = 0; i < 10; i++) {      /* 配列の要素に代入 */
    a[i] = i * i;
  }
  for (i = 0; i < 10; i++) {      /* 配列の要素を印字(=出力) */
    printf("a[%d] は %d です.\n", i, a[i]);
  }
}

(5.1.4) 「配列」とは、 0 からある自然数までの整数で添え字づけられた有限個の変数 x[0], x[1], x[2], ..., x[N-1] を一斉に定義し、一括して扱うためのものである。 K&R2 では §1.6 を参照。

(5.1.5) 「配列の宣言。...」の行。こう書くと、 a[0] から始まる 10 個の int 型変数が使える。 すなわち、a[0], a[1], a[2], ..., a[9] が使える。 a[10] は使えない。うっかり使わないよう、注意。

(5.1.6) 「配列の宣言は main() の中カッコの外で行ない、 ほかの変数の宣言は main() の中カッコの中で行なう」 という規則があるわけではないが、 ある事情により、この授業ではそうする。 正確なところは K&R2 §1.10, §4.3 参照。

(5.1.7) 同じ定数が何度も現れる場合、§3.4 で説明した #define を利用すると便利なのであった。 次の例は (5.1.3) のプログラムとまったく同一である。

#include <stdio.h>
#define N 10

int a[N];       /* 配列の宣言。これで a[0], ..., a[N-1] が使える */

int main() {
  int i;

  for (i = 0; i < N; i++) {       /* 配列の要素に代入 */
    a[i] = i * i;
  }
  for (i = 0; i < N; i++) {       /* 配列の要素を印字(=出力) */
    printf("a[%d] は %d です.\n", i, a[i]);
  }
}

(5.1.8) 練習:上のプログラムを改変し、 a[i]i*i を代入したあと、 隣りあった項どうしの差 (a[1] - a[0], a[2] - a[1], ..., a[N-1] - a[N-2]) を出力するプログラムを書け。 出力時に、添え字が範囲外の値にならないよう注意せよ。 次に、わざと間違えて、添え字が範囲外の値になるようにせよ。 それをコンパイル・実行してみよ。

(5.1.9) 練習:a0 = a1 = 1; an+2 = an + an+1 で決まる フィボナッチ数列を(適当な項数だけ)計算し、出力するプログラムを書け。 (配列を使わなくても書けるが、使うほうが楽であろう。)

(5.1.10) 練習:上のプログラムでは int 型の配列を使ったであろう。 それを double 型に変え、an+1/an の値をも出力するようにしてみよ。

(5.1.11) int a[3][4]; のように宣言すれば下のような二重配列が使える。

    a[0][0]    a[0][1]    a[0][2]    a[0][3]
    a[1][0]    a[1][1]    a[1][2]    a[1][3]
    a[2][0]    a[2][1]    a[2][2]    a[2][3]

(5.1.12) 練習:次は、パスカルの三角形を出力するプログラムの書きかけである。 完成してみよ。 完成後、N の値を大きくして、どこまで計算できるか確かめよ。

#include <stdio.h>

#define N 8

int c[N][N];

int main() {
  int n, r;

  for (n = 0; n < N; n++) {
    c[n][0] = c[n][n] = 1;
  }

  /* 漸化式を使って配列を埋める(二重ループ)*/

  /* パスカルの三角形状に出力(二重ループ)*/
}

§5.2 乱数

(5.2.1) 「乱数」が数学でどう定義されるかは、ここでは考えない。 計算機でいう乱数とは、 種(たね)と呼ばれる整数を元に数式を用いて発生させる、 乱数のように見える数列のことである。

(5.2.2) 関数 rand() は、小カッコの中には何も書かずに呼び出す。 呼び出されるたびに、 0 以上 RAND_MAX 以下の整数(int 型)に値をとる乱数の項を、 一つずつ返す。次のプログラムでは、一行に一つずつ、全部で 10 個の整数が出力される。 その並びが乱数、というわけである。

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

int main() {
  int i;

  for (i = 0; i < 10; i++) {      /* 10 項からなる乱数列を発生 */
    printf("%d\n", rand());
  }
}

(5.2.3) 2行目。関数 rand() を使うには stdlib.h#include することが必要である。 どの関数を使う際にどの .h ファイルが必要になるのかは、 この授業ではそのつど教える。また、K&R2 の付録に書いてある。

(5.2.4) RAND_MAX はコンパイラごとに違う値を取り得る定数である。 次のプログラムで調べることができる。

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

int main() {
  printf("%d\n", RAND_MAX);
}

この演習室の gcc では、その値は 32767 (= 215 - 1) である。 意外と小さいことに注意。

(5.2.5) (5.2.2) のプログラムは、毎回同じ乱数列を出力する。 それでは困るので、現在時刻を乱数の種にすることを考える。 (#include <time.h>time() 関数を使うために必要である。)

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

int main() {
  int i;

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

  for (i = 0; i < 10; i++) {
    printf("%d\n", rand());
  }
}

センターの PC でこのプログラムを動かすと、 秒未満は無視される。 また、1 秒に一回ぐらいのテンポで動かすと、乱数の種が 1 だけ変わっても、 乱数の第 1 項はあまり変化しないことに気づくだろう。

(5.2.6) rand() % 100 とすると、 0 以上 100 未満の整数に値をとる乱数が得られる。 厳密に言えば、RAND_MAX の値によっては、 0 の出現率と 99 の出現率はわずかながら異なるが、 この授業ではそこまでは気にしないことにしよう。

(5.2.7) 練習:(5.2.5) のプログラムを、(5.2.6) を参考にして改造し、 さいころのように、1 から 6 までの整数に値をとる乱数が発生されるよう改変せよ。 また、実行するごとにその乱数列が異なることを確認せよ。

(5.2.8) 今後、10000000 ぐらいの大きさの配列を乱数で初期化することがあるので、 0 から 32767 (= 215 - 1) まででは、 同じ値を持つものが多く現れ、都合が悪い。 そこで、rand() を二回呼ぶことを考える。 二度めに得た値は、32768 (= 215) 倍して一度めの値に加え、 さらに 1 を加える。 これで、1 から 1073741824 (= 230) までに値をとる乱数が得られる。 (一度めでなく二度めを 32768 倍するのは、 (5.2.5) で述べたように第 1 項が種に強く依存するからである(が、 しろうとの浅知恵かもしれない)。)

#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;
}

§5.3 整列(ソート)とは

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

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

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

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

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

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

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

     95 点 ...                                95 点 ...

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

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

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

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

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

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

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

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

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

#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] = myrand() % 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) {
  /* (5.4.9) を読んで、ここを書く */
}

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

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

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

§5.5 互換を行なう関数 swap()

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

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

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

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

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

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

  swap(3, 8);
  print();

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

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

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

§5.6 比較を行なう関数 comp()

(5.6.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) と書けば、 スピードの点で若干劣るが、同じことである。 プロトタイプ宣言も付け足しておくこと。

(5.6.2) main() の最後に次のように書き足して実験してみよう。 (§5.5 で付け加えた swap() のテストの部分は残しておけ。 すなわち、

  swap(3, 8);
  print();

の下に、次のように書け。)

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

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

§5.7 課題1

(5.7.1) 前の節で完成させたプログラムを送れ。 main() は次のようになる。

int main() {
  printf("配列の大きさは %d です.  ", N);     /* 配列の大きさを出力 */
  printf("…………大場久美子\n");             /* 自分の氏名を出力 */
  init();                                     /* 初期化 */
  print();                                    /* 画面表示 */
  print();                                    /* 画面表示(もう一度)*/
  swap(3, 8);
  print();
  if (comp(1, 2) > 0) {
    swap(1, 2);
  }
  print();
}

(5.7.2) メールは,必ず,この授業で配布したアカウントから送れ。 メールの本文冒頭に, 学籍番号,名列番号,氏名(として大学に届けてあるもの)を忘れずに書け。

(5.7.3) その次に,プログラムを載せよ。 その際,KAINS Webmail のメール作成画面の「ファイルを添付」 *ではなく* 本文の中にソースファイルを貼りつけよ。

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

(5.7.5) 宛て先は私(岩瀬)の実習用アカウント((1.8.8) 参照)である。 件名は「?? kadai1」(←全て半角文字, ?? は自分の id の下二けた,その後ろに半角スペース一つ, kadai は小文字, kadai1 の間にはスペースを入れない)とせよ。

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

(5.7.7) ※ この課題をこなしておかないと、事実上、次の課題に進むことができない。


岩瀬順一