2009 年度「計算数学1」 2009-04-13

§50 準備

(50.1) 「計算機基礎論3B」の最初のページを参照のこと。

(50.2) 今回のコンテンツ ID は z09eb0??(「?」は数字一文字)である。

(50.3) この授業のページは http://wwwedu.ipe.kanazawa-u.ac.jp/%7Ez09ebt01/ である。見て、ブックマークに追加しておくこと。

(50.4) emacs の設定ファイルを私のところからコピーするため、 「cp -p ~z09ebt01/.em* .」を一度だけ実行せよ。 (このコマンドは、この授業のページからコピーすれば簡単だし間違いがない。 「cp: `/home/z09eb/z09ebt01/.emacs.d': ディレクトリは対象外です」 というエラーメッセージが出るかもしれないが構わない。)

(50.5) Active! mail も設定しておくように。諸君のメールアドレスは z09eb0??@mailedu.ipe.kanazawa-u.ac.jp であり、 担当教員のメールアドレスは z09ebt01@mailedu.ipe.kanazawa-u.ac.jp である。

(50.6) 「計算機基礎論3B」の最後に、別のところに保管しておくよう伝えたファイルは、 来週までに、今回のアカウントのホームに復元しておくこと。






































§51 再帰

(51.1) ある関数の中でその関数自身を呼び出すことを「再帰」という。 ある程度のプログラミング体験を積んで 「ここは再帰を用いてもよい」 「用いるべきだ」 という判断ができるまでは、自分の判断で再帰を使うことは避けたほうがよい。 クイックソートでは再帰を用いる。 きょうはその練習である。

(51.2) 最大公約数を返す関数を再帰を用いて書いてみたもの。

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

int gcd(int x, int y);
int gcd0(int x, int y);

main() {
    int i, x, y;

    srand((unsigned)time(NULL));    /* 現在時刻で乱数の種を初期化 */
    for (i = 0; i < 30; i++) {      /* 乱数を引数にして gcd() と gcd0() を試す */
        x = rand() % 100; y = rand() % 100;
        printf("gcd(%d, %d) = %d, ", x, y, gcd(x,y));
        printf("gcd(%d, %d) = %d\n", x, y, gcd0(x,y));
    }
}


/* 非負整数 x, y の最大公約数を返す。再帰バージョン。どちらが大きくてもよい */
int gcd(int x, int y) {
/*  printf("%d と %d の最大公約数を求めます.\n", x, y); */
    if (y == 0) {
        return x;
    } else {
        return gcd(y, x % y);
    }
}


/* 同上。再帰を使わないバージョン */
int gcd0(int x, int y) {
    int r;

    for (   ; y != 0;    ) {
        r = x % y;
        x = y;
        y = r;
    }
    return x;
}

(51.3) どちらの関数も、エウクレイデス(ユークリッド)の互除法を使っている。 再帰を使わないバージョンの説明は省略しよう。 再帰バージョンを、gcd(60, 24) として呼び出したとする。 するとこの関数が gcd(24, 12) を呼び出し、 それがさらに gcd(12, 0) を呼び出し、これは 12 を返す。 するとそれが順に返されて、全体として 12 が返ることになる。

(51.4) その場合、gcd(60, 24) の実行が終わる前に gcd(24, 12) が呼び出される。 その実行が終わる前に gcd(12, 0) が呼び出される。 つまり、ある時点では三つの gcd() が同時に動いている。 これがあまりに多すぎると、メモリを使い果たしてしまい、プログラムは正しく動かない。

(51.5) 次は、配列の中から最大のものの添え字を見つける関数を、 クイックソートの練習のため、わざと再帰を使って書いたものである。 再帰の「深さ」とは、その関数が何重に呼び出されているか、である。 すなわち、最初に呼び出されたときは深さ 0 であり、 深さ i で呼び出された関数から呼び出されたときは深さ i+1 である、と定義する。

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

#define N 20

int a[N+1];

int mymax(int d, int from, int to);

main() {
    int i;

    srand((unsigned)time(NULL));
    for (i = 1; i <= N; i++) {          /* 初期化 */
        a[i] = rand() % 100;
    }
    for (i = 1; i <= N; i++) {          /* 印字 */
        printf("%2d ", a[i]);
    }
    printf("\n");

    printf("a[%d] が最大(のものの一つ)です.\n", mymax(0, 1, N));
}


/* a[from] から a[to] までの中から最大のものの添字を返す。*/
/* わざと再帰を使ってある。d は再帰の深さ。               */
int mymax(int d, int from, int to) {
    int i, j, k, n;

    for (n = 0; n < d; n++) {           /* d の値の二倍だけスペースを */
        printf("  ");                   /* 印字することで深さを示す */
    }
    printf("mymax(%d, %d) として呼び出されました.\n", from, to);

    if (from == to) {
        return from;
    } else {
        k = rand() % (to - from) + from;    /* 切れ目 k を乱数で決める */
                                            /* (from 以上 to 未満の範囲で) */

        i = mymax(d + 1, from, k);      /* 再帰呼び出し */
        j = mymax(d + 1, k + 1, to);    /*      同      */

        if (a[i] >= a[j]) {     /* 大きいほうの添字を返す */
            return i;
        } else {
            return j;
        }
    }
}

(51.6) 初めは main() から mymax(0, 1, 20) として呼び出される。 この関数は a[from] から a[to] までのうちで最大のもの(の一つ)の添え字を返すのだが、 乱数で切れ目 k の値を決め、もしもそれが 6 だったとすると mymax(1, 1, 6) として自分自身を呼び出し、 次に mymax(1, 7, 20) として自分自身を呼び出し、 それらが返した添え字のうちで値が大きいほうの添え字を返す。 呼び出された mymax(1, 1, 6)mymax(1, 7, 20) はまた自分自身を呼び出す。 同時に呼び出されている関数の数は、配列がうまく半分ずつに分かれてゆけば log2(N) 程度だが、 運が悪いと N に近くなる。

(51.7) また、上の二つの例から、 「再帰を用いた関数は中で場合分けを行ない、 自明な場合にはすみやかに return し、 非自明な場合には“より自明に近い”引数で自分自身を呼び出す」という形になることがわかるであろう。

(51.8) 次は積を返す関数だが、再帰が深くなりすぎるのでよくない。 (そもそも x*y と書けばよいだけのこと。)

int mul(int x, int y) {
    if (y == 0) {
        return 0;
    } else {
        return mul(x, y-1) + x;
    }
}

(51.9) 次はまちがったプログラムである。 次々と自分自身を呼び出し、メモリを使い果たしてしまう。

int mul(int x, int y) {
    return mul(x, y);
}

§52 練習問題

(52.1) 階乗を求める関数 int factorial(int n) を再帰を用いて書け。 返り値の型は double 型にしてもよい。

(52.2) n が偶数 2k のとき xn = (xk)2, n が奇数 2k+1 のとき xn = x(xk)2 を利用して xn 乗を求める関数 double power(double x, int n) を書け。

(52.3) nCr = n-1Cr + n-1Cr-1 を利用して nCr を求める関数 int comb(int n, int r) を書け。

(52.4) 「ハノイの塔」で、棒の番号を 0, 1, 2 とする。 小さいほうから数えて i 枚の円盤を棒 from から棒 to へ移動する方法を

円盤 1 を棒 0 から棒 1 へ移動します.
円盤 2 を棒 0 から棒 2 へ移動します.
円盤 1 を棒 1 から棒 2 へ移動します.
円盤 3 を棒 0 から棒 1 へ移動します.
円盤 1 を棒 2 から棒 0 へ移動します.
円盤 2 を棒 2 から棒 1 へ移動します.
円盤 1 を棒 0 から棒 1 へ移動します.
のように表示する関数 void hanoi(int i, int from, int to) を書け。 (この例では i は 3 である。)


岩瀬順一