CONTENTS / BACK-PAGE / NEXT-PAGE

 前回に引続き、今回も演習です。

 今回の解説の前に前回の話しの中でいくつか疑問を投げかけておきましたので、それについて解説しましょう。

 最初は、配列の最後を指定するのに何故大きさから一つ少ない数を指定するかということでした。これはその前の例を見ればすぐにわかります。例えば大きさが5だとすると、

data[0], data[1], data[2], data[3], data[4]
の5つになります。データの個数(つまり大きさ)は5ですが、添字は0から始まるために最後は4になっています。よって大きさより一つ小さい数が最後の添え字となるわけです。

 次はgetchar()で文字を読むときのアルゴリズムですが、do文で書くと1ステップ余分に付けなければならないことに気が付きます。for文はどちらかというと配列の添え字の操作のように初期値と終値が決まっていて、一定の変化率で変わる場合に使います。今回のように入力終了記号が来るまでという様な場合には使いません。

 大体おわかり頂けたでしょうか。

 前回で大体のプログラミングの流れはつかめたかと思います。(まだの方は今回と合わせて覚えるようにしましょう)要するに

・実世界に存在するものをデータで表す。
・大まかな流れを決める。
・大まかな流れに沿って、アルゴリズムを決定する。
・データとアルゴリズムが合うように細かいところを埋める。
ということです。よくわからない部分があれば、そこの部分は保留にしておいて先に進んで後で埋め直しても構いません。この考え方は重要なので、ぜひ身に付けるようにしましょう。


 では今回の本題に入りましょう。今回は前回の解答の別解をやってみます。 前回の問題は以下のようなものでした。

問題1 : 文字列を受取り、その逆順を返すプログラムを書け。───────0)

 前回はこの問題に対し、とりあえず大きな文字配列を用意し、その大きさを得て、ループを使って逆順に出力するプログラムを作成しました。  しかし、これでは予定した大きさの文字列以上の文字列を扱うことができません。例えば、前回のプログラムでは

char data[65535];
という文字配列を使用しましたが、これでは大きさが65535以上の文字列は扱えません。

 そこで、今回は4.5.3でやった再帰を用いて無限の大きさの文字列を扱うプログラムを組んでみましょう。(無限の大きさと言っても各マシンのメモリー数を越えることはできませんが)今回も前回と同じ様な段階をふんで行きます。


1)問題1の分析─────────────────────────────1)

(1)入力元は何で、出力先は何か。
(2)文字列の長さはどれくらいか。

  (1)は前回と同じです。解決策も前回と同じ、標準入力、標準出力にしましょう。
  (2)については今回は関係ありませんね。


2)データとデータ構造の決定───────────────────────2)

 今回はこれが問題です。入力される文字列の大きさは予めわかっていないのに、データ構造と呼べるほどのものを用意できるのでしょうか。今のところ、答えはNOです。(もう少しCについて勉強するとできます)では、どうすればいいのでしょうか。

 とりあえずわかっているものとしては、前回最後に出てきた、入力文字を判定する一文字分のデータが必要なことはわかります。それ以外は用意できそうにありませんのでこのままにして先に進みましょう。

必要データと型 : 入力終了文字取得のための文字、文字型。

3)全体の大まかな流れを考える。─────────────────────3)

 再帰的に処理するという基本方針は決まっているのですが、一体これはどういうことなのでしょうか。もう少し具体的に考えてみましょう。4.5.3 で少しやりましたが、再帰構造とは、”その処理が再帰的に行われるもの”ということです。n階乗の関数などはこのいい例でしょう。(後で出てきます)では、今回の問題では、何が再帰的に処理されているのでしょうか。今回考える再帰構造は少し変わっているかもしれません。それは、”一文字入力し、その文字を出力する”という構造を持つからです。”??”なんて考え込まず、気を楽にして先に進んでください。

 その前に少しおさらいです。4.5.3で再帰構造について学びました。その中では階乗を求める関数の説明をしていますが、これはf(n)=n!とすると、

として定義していました。通常数学で式を定義する場合は右側の式で左側を定義するのですが、この式は右側に同じ式が含まれていて、一見おかしいような気がしますが、これは正しいのです。例えば、

	 5! = 5 * 4!
	    = 5 * 4 * 3!
	    = 5 * 4 * 3 * 2!
	    = 5 * 4 * 3 * 2 * 1!
	    = 5 * 4 * 3 * 2 * 1 * 0!
	    = 5 * 4 * 3 * 2 * 1 * 1
となって正しい答えが得られることがわかります。ここまではNo.8でやったので、おさらいはここまでですが、この後の説明のためもう少し、詳しく見てみましょう。

 上の例で5!を求めるのに5と4!を掛け合わせていますが、これは4!がその値を持っているということですね。えっ、何を当たり前のことをいっているのかって? それとも良くわかりません?? では先の関数f(n)=n!を用いて説明しましょう。

 上の例は

	f(5) = 5 * f(4)
	     = 5 * 4 * f(3)
	     ...................
	     = 5 * 4 * 3 * 2 * 1 * f(0)
	     = 5 * 4 * 3 * 2 * 1 * 1
となるのですが、一行目でf(5)を計算するのに5とf(4)を掛け合わせていますが、このときf(4)は4!の値を知っていることになります。同様に次の行ではf(3)が3!の値を知っていることになります。同様に最後までいくと、f(0)が出てくるわけですがこれは定義にはっきりと1と書いてあるので、ここで計算のための展開は終了します。(計算はしていないことに注意。展開が終ってから始めて計算ができるようになります。)ここでは各f(n−1)がその値を知っていることに注意してください。この各関数が何か情報を持っているということと、各f(n)がf(n−1)を呼び出していることがこれから重要になってきます。

 このことを上記の例を参考にしながら、今回の問題に当てはめて考えてみましょう。説明のため以下の関数を定義します。

 r(x) : 関数rは情報として文字xを持っている。

 また、表記上の約束として以下のことを約束することにしましょう。

	r()      : 関数rは何も情報を持たない。
	r()  ←  x  : 関数rに情報 xを入力する。
	            xは文字列でも良く、この場合は一文字受けとって
	           もう一度自分自身を呼び次の文字を入力させる。
	r(x) r(y) : 関数r(y)は関数r(x)に呼び出されている。
 簡単な例でabcdeを入力させたいとします。この時、関数rがこれらの情報を順番に持っていればどうなるでしょうか。

	  r()  ←  abcde

	  r(a) r()  ←  bcde

	  r(a) r(b) r()  ←  cde

	  r(a) r(b) r(c) r()  ←  de

	  r(a) r(b) r(c) r(d)  ←  e

	  r(a) r(b) r(c) r(d) r(e)
1行目:関数rに文字列abcdeを渡す。
2行目:関数rはaを文字列として持ち、さらに自分自身を呼び出し、残りの文字列bcdeを渡す。
3〜5行目:同様
6行目:入力文字がなくなったら、終り。
 大体これで感じがつかめるでしょうか。最終的には

	r(a)はr(b)を呼びだし、
	r(b)はr(c)を呼びだし、
	r(c)はr(d)を呼びだし、
	r(d)はr(e)を呼び出すことによって、
文字列abcdeは関数達が持っていることになります。これを利用すれば、変数を用意しなくても、文字列は保持することができます。

 では、逆順に出力するにはどうしたら良いでしょうか。上の説明の文字列の保持は、先の階乗関数の例でいくと展開したことに相当します。つまりまだ計算段階に到っていません。実際、階乗関数の再帰版では、展開した右端から計算していくようになっています。計算が終るとその関数は計算結果をreturn文で呼び出した関数に返し、終了してしまいます。(Progrraming Lecture No8参照)つまり

	f(0)は1   をf(1)に返すと終了し、
	f(1)は1*1 をf(2)に返すと終了し、
	f(2)は2*1 をf(3)に返すと終了し、
	f(3)は3*2 をf(4)に返すと終了し、
	f(4)は4*6 をf(5)に返すと終了し、
	f(5)は5*24を計算して終了。
となるわけです。これを関数rに当てはめて考えてみましょう。

	r(e)はeを印字すると終了し、
	r(d)はdを印字すると終了し、
	r(c)はcを印字すると終了し、
	r(b)はbを印字すると終了し、
	r(a)はaを印字して終了。
という風にすればいいでしょう。

 話しがややこしくてわかりにくそうですが、もう一度見直してみるなどして、少し考えてみてください。プログラムに少し自身のある人は読み飛ばして、プログラムを見るとよくわかることと思います。

 さてこれで大まかな流れはおわかり頂けたかと思います。ところで再帰構造で最も重要なことは、

	・停止条件は何か
	・必ず止まるか。(停止条件に引っかかるか)
ということです。(本質的にはプログラム自体にこのことが当てはまります。つまり、どんな入力に対しても必ず停止するかということです。この問に対する厳密な解を与えることは現在では不可能で、多くの研究者が研究中です。ただここでは”自分自身を呼ぶのを止めて、戻り始める条件は何か”ということです。例えば、n階乗のプログラムでは関数fact内の if(n == 0) がこれに当ります。

int fact(int n)          
{
                              
    int dum;                  
                              
    if (n == 0)               
        dum = 1;              
    else                      
        dum = n * fact(n - 1);
                              
    return dum;               
                              
/*  End of fact  */           

n階乗のプログラム(部分)

つまり、nが0でなければ、nを1引いて自分自身を呼出し、nが0になったら1を返して呼び出しを止めています。(Programming Lecture No.8参)これは渡された引数が正の整数であれば必ず停止条件に引っかかり、停止します。

 では、今回の問題での関数の停止条件はなんでしょうか。そうです、入力終了文字(CRかEOF)が入力されたら停止すればいいのですね。

 ではこれに肉付けをする形で細かい部分を考えることにしましょう。


4)データ構造にあったアルゴリズムを考える。───────────────4)

 今までのことを整理してまとめてみましょう。

	 ・再帰的に入力すること
	 ・再帰的に出力すること
	 ・そのためにはもう一つ別の関数を(メインの他に)作成する必要があること
	 ・再帰的なアルゴリズムには、終了条件が必要なこと
	 ・終了条件は入力終了記号が入力されたときであること
 これらをもとにアルゴリズムを考えてみましょう。

 まず、メインルーチンですがこれは単に、入力と出力を行なう関数を呼ぶだけですね。

 では、入力と出力を行なう関数はどうすればいいのでしょうか。考える前にもう一つ考えることがあります。関数には返り値の型と引数というものがあったことを思い出してください。例えば先の階乗関数では返り値の型はint型、引数は一つで型はintでした。この場合はどうなるでしょうか。何か値を返す必要があるでしょうか。何か引数として渡す必要があるでしょうか。このことも交えて少し考えてみてください。









 では解答です。まず、この関数の返り値の型と引数ですが、これはどちらも必要ありません。解説で用いた関数rは引数として入力された文字というものを持っていましたが、これは実際にはプログラム中に埋め込むため必要なくなるのです。これは下の解答を見てもわかります。以下の部分はサブルーチンである関数のアルゴリズムであることに注意してください。

	一文字入力する。
	if(その文字が入力終了記号でないなら)
	{
	  自分自身を呼び出す。
	  一文字出力する。
	}
	関数終了。
 これで終りです。以外とあっさりしているので、落胆したかも知れませんが、これで充分なのです。これは実際にプログラムを見るとよくわかります。では、実際にプログラムを組んでみましょう。


5)プログラミング────────────────────────────5)

 いきなりですが、今までの解説をもとに少し考えてみてください。わからなければ悩まずすぐ先に進んでください。









 では解答です。このプログラムでは特別な関数は一切使っていないので、#include文で取り込まれるものは<stdio.h>だけです。

#include <stdio.h>

 次に使用する関数の宣言を行ないます。標準関数ではなく、ユーザ定義の関数を使う場合、予め宣言しておく必要があります。ここではサブルーチンとなる関数を作成しますので、宣言しておきます。宣言は

返り値の型 関数名(引数、引数、、、、);   ← セミコロンがあることに注意。

でした。関数宣言の場合は最後にセミコロンが付くことを忘れないで下さい。前に書いた階乗関数のプログラムを見ればわかりますが、実際の定義のところでは最後にセミコロンは付けません。

 ここでは関数名getandwriteとして宣言します。

void getandwrite(void);

 返り値のところのvoidは”この関数は返り値がない”ことを表しており、引数のところのvoidは”この関数は引数がない”ことを表しています。

 次にメインですが、これは前にも書いたとおり、サブルーチンを呼び出すだけです。

	main()
	{
	    getandwrite();
	}
 呼ばれる側では引数がない場合にはvoidと宣言しましたが、呼ぶ側では何も書かず()だけ書いておきます。

 さて、では問題のサブルーチンです。4)の解説をもとに書いて見ましょう。

	void getandwrite(void)
	{
	    char c;
	
	    c = getchar();
	    if(( c!= EOF) && (c != '\n'))
	    {
	        getandwrite();
	        write("%c", c);
	    }
	
	    return;
	}
 となります。

 では、解説していきましょう。

 まずこの関数では一文字入力を行なうので、そのためのデータが必要となります。型は当然char型です。

 次にこの変数に一文字読ませるのですが、これはとりあえずgetchar()を使いましょう。scanf()でも今回は問題ないのですが、こちらの方が関数らしくていい感じです。

 で、次のif文ですが()内は”入力終了記号でないなら”を表しています。(詳しくはNo.9参照)すると自分自身を呼び出し、返ってきたら入力された一文字を出力して終っています。プログラム全体をまとめて書くと以下のようになります。

/*  Program  */
/*  Short Program 1 */
/*  無限に文字列を受取り、その逆順を返す  */

#include <stdio.h>

void getandwrite(void);

main()
{

    getandwrite();
    printf("\n");
}

void getandwrite(void)
{
    char c;

    c = getchar();
    if((c != '\n') && (c != EOF))
    {
        getandwrite();
        printf("%c",c);
    }

    return;
}
 論より証拠。実際にコンパイルして動かしてみましょう。


6)コンパイル──────────────────────────────6)

 コンパイル中でエラーがでたら、エディタで編集し直してエラー箇所を取り除きます。大体エラーは、{}の対応付けが出来ていない、;がない等が多いようなのでよく見てみましょう。


7)テスト────────────────────────────────7)

 コンパイルできたら、実際に動かしてみましょう。適当に文字列をいれてみて、ちゃんと動くかどうか確認してみます。スペース等が入ってもきちんと動きますし、ファイルからリダイレクションで渡しても、大丈夫です。


8)改良─────────────────────────────────8)

 今回は改良するところはないでしょう。手を加える余地があるとすれば、

  • getchar()ではなく、scanf()を使う。
  • 後に保存できるように、配列などのデータに入力した文字列を代入していく。
くらいでしょう。

 scanf()を使う場合には、 scanf("%c",c); として使います。

 配列などへの代入は、後でポインタを勉強したときに公開することにしましょう。


9)完成─────────────────────────────────9)

 さて、悩んだ割にはあっというまに完成です。今回はどちらかというと、演習というよりは講義という感じでしたが、再帰の考え方は慣れるまではけっこう面倒なので今回はこういう形式になってしまいました。再帰に関する要点(3、4参照)は述べてあるので、もう一度読み返してしっかり理解してください。

 練習問題として、前々回練習問題として出しておいたユークリッドの互除法について再帰的なプログラムを組んでみてください。互除法の定義に関しては以下に転載しておきます。


演習問題

 ユークリッドの互除法を用いて、2つの数の最大公約数を求めるプログラムを再帰的に定義せよ。ユークリッドの互除法については、以下の例で説明しよう。

	例 128と36の最大公約数を求める。
	 (128,36) → (36,128を36で割った余り)
	          → (36,20)
	          → (20,36を20で割った余り)
	          → (20,16)
	          → (16,20を16で割った余り)
	          → (16,4)
	          → (4,16を4で割った余り)
	          → (4,0)
	             ↑
	            この4が128と36の最大公約数
	プログラミングのヒント
	 xとyの最大公約数を求める関数をgcd(x,y)とする。
	  一般にr=x % yとすると
	
のような式が定義できる。

 とにかく再帰で考えることは、

	・何が再帰か
	・停止条件は何か
の二点です。これをしっかり考えてアルゴリズム等をたててください。

 この問題は多くの本で例題として挙げているので参考に見てみてもいいでしょう。本によっては再帰的なプログラムと非再帰的なプログラムと両方載っているので見比べてみてください。ここでの解答は次回行うことにします。(比較のため、3種類のプログラムを公開します)

 今回はここまでです。


CONTENTS / BACK-PAGE / NEXT-PAGE