2016 年度「計算数学」 2016-11-11

§5.1 配列

(5.1.1) 変数名には,a, b, x, y のような一文字はもちろん,sum, total のように二文字以上からなる文字列も使える。 英語の単語でなくてももちろん可。 x11 のように数字がはいっても構わないが, 数字で始まる 1x などは不可。 (使おうとしたらどうなるか?)

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

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

#include <stdio.h>
 
int a[10];      /* 配列の宣言。これで a[0], ..., a[9] が使える */
 
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) 同じ定数が何度も現れる場合,#define を利用すると便利である。 #define の次に書かれた文字列は,その右に書かれたもので置き換えられる。 次の例は (5.1.3) のプログラムとまったく同一である。 配列の大きさを変えたくなった場合, #define N の右の数を変えるだけで, 複数回あらわれる N の値を一気に変えることができる。

#include <stdio.h>
#define N 10
 
int a[N];       /* 配列の宣言。これで a[0], ..., a[N-1] が使える */
 
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.2 乱数

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

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

#include <stdio.h>
#include <stdlib.h> /* rand() */

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 の付録に書いてある。 「/* rand() */」は, 「rand() を使うためにこの行を書いた」という覚え書きである。

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

#include <stdio.h>
#include <stdlib.h> /* RAND_MAX */

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

興味のある人は値をメモしておこう。

(5.2.5) (5.2.1) のプログラムは,毎回同じ乱数列を出力する。 それでは困るので,現在時刻を乱数の種(たね)にすることを考える。

#include <stdio.h>
#include <stdlib.h> /* rand(), srand() */
#include <time.h>   /* time() */

main() {
    int i;

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

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

センターの Linux でこのプログラムを動かすと, 秒未満は無視されるようである。(二人で“同時に”実行してみよ。)

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

(5.2.7) 練習問題その1:(5.2.5) のプログラムを,(5.2.6) のように改造し, 0 以上 100 未満の整数に値をとる乱数が得られること, および,実行するごとにその乱数列が異なることを確認せよ。

(5.2.8) 練習問題その2:(5.2.5) のプログラムを, 1 以上 100 未満の整数に値をとる乱数が発生されるよう改変せよ。

§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) 次のプログラム例を見ながら, 以下の説明を読むこと。 このプログラムは完全ではないので, 指示に従って自分で書き加える必要がある。 長いので,コピペすることを強くすすめる。

#include <stdio.h>
#include <stdlib.h> /* rand(), srand() */
#include <time.h>   /* time() */

#define N 10

void init(void);
void print(void);

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

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


void init(void) {
    int i;

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

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

    if (N < 70) {                   /* N が小さいとき */
 
        /* ここを各自で埋めよ */

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


void print(void) {
    /* このコメントは消して,ここに関数 print() の本体を書く */
}

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

(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) 初めに,init() という自作関数でこの配列を「初期化」する。 初期化とは,一般には初めのデータを代入することである。 ここでは乱数で初期化する。

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

(5.4.6) N の値によって初期化を変える理由を, 以下で説明する。

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

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

(5.4.9) 上の init() の, 「N が小さいとき」の部分を各自で書け。

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

(5.4.11) 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() の最後に(swap() のテスト用に書いた部分は消してから) 次のように書き足して実験してみよう。

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

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


岩瀬順一