CONTENTS / BACK-PAGE / NEXT-PAGE

 今回で演習最後です。
 最後の演習問題は、ユークリッドの互除法を用いて、最大公約数を求めるというものでした。

演習問題

 ユークリッドの互除法を用いて、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とすると

gcd(x,y)  / y
\ gcd(y,r)
  :r=0のとき
  :r>0のとき
のような式が定義できる。

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

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

 問題では、再帰版のみを考えるようにしてありますが、今回は非再帰版と両方を考えてみます。

非再帰版

 まず、非再帰版を考えてみましょう。前々回に提示した方法で順に組んで行きます。


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

 この問題で考えることはそんなにありませんね。とりあえず、入出力は標準入出力としておけば十分でしょう。あと必要なものは与えられた2数と、それらの商、余りです。

 学生にこの手の問題を出すと、すぐメインルーチンで解こうとする人が多いのですが、あまりよくありません。こういう関数は後で使えるので、なるべく独立性を高くした方がいいのです。よって、2数を引数とし、その最大公約数を返す関数を作成することにします。(これは再帰版でも同じです)。

 余談ですが、入出力については今回は(前回と違って)問題になりません。なぜなら、入出力に関係なく、与えられた2数の最大公約数を返す関数を作成すればいいからです。

詳しくは、後述します。


2)データとデータ構造──────────────────────────2)

 データとしては、2数が与えられるので、これを受ける変数が必要です。それらの型としては普通の整数型で十分でしょう。あと、商、余りもあります。これらも整数型にしておきましょう。


3)大まかな流れ─────────────────────────────3)

 大筋は簡単なので考えなくてもいいでしょう。

	1)2数を入力する。
	2)最大公約数を計算する。
	3)結果を出力する。
 こんな感じになります。えっ、何を当り前のことをって。いやこれで終りではなく、肝腎の最大公約数を計算する部分も考えましょう。二つの数が与えられたとき、その最大公約数を計算するにはどうすればいいでしょうか。(もちろんユークリッドの互除法を用いてです)これは少し考えてもらいましょう。(問題の例をよく見てください)









 解答は次章で。


4)細かいアルゴリズムの決定───────────────────────4)

 まず、メインルーチンの方からいきましょう。簡単ですが、前章を参考に少し考えてみてください。









 では解答です。

 最初はメインルーチンです。

 まず、数の入力方法ですが、簡単にscanfを使いましょう。確かに前回、(またはご質問の解答で)scanf()は扱いが厄介なので使用は控えましょう、と書きましたが、この場合、逆にgetchar()で整数を入力する方が面倒なので、あえてscanf()を用いることにします。もし、ほかのものを使用する場合は、そこだけ書き換えても差し支えないでしょう。

 次に最大公約数を計算する関数を呼び出し、その答えを変数にいれておきます。

 最後に求めた答えを出力します。

 次に、最大公約数を計算する関数ですが、どうなるでしょうか。(先の問題の例を見てください)

 まず、とりあえず2数の余りを求めます。その余りが0なら割った答え(つまり商)が求める最大公約数です。もし0でないなら、割る数を余りで割り、その答えが0かどうか調べます。そして、余りが0になるまで繰り返します。この繰り返す部分をうまく作ればいいのです。手順としては

	(1)aをbで割り、その余りをcとする。
	(2)もしcが0なら(7)へ行く。
	(3)a ← bとする。
	(4)b ← cとする。
	(5)a÷bを計算し、余りをcとする。
	(6)(2)へ行く。
	(7)bが答えである。
	     (ただし、a、bは与えられた整数でa>bとする。以降同じ)
 こんな感じでしょうか。   (記号” ← ”は代入をあらわします。)

 しかし、このアルゴリズムには、1つミスがあり、かつ冗長です。

 ミスは”もしbが0ならどうするか”ということであり、冗長な部分は”手順の中で2回割算がでてくる”ということです。bの問題はif文などを用いて解決できますが、冗長な部分はどうでしょうか。

 アルゴリズムによっては、このような冗長性はなくせませんが、今回はなくすことが出来ます。実はその方法を使うと先のミス”bが0の時”も同時に解決できます。さぁ、一体どうすればいいのでしょうか。少し考えてみてください。









 では、解答です。

 まず、bが0の時を考えてみましょう。この時、求める最大公約数はaになります。
 よって、

(0)もしbが0ならaが答えである。
ということになります。

 さて次に冗長性ですが、先の例の図とアルゴリズムをよく見てください。aにbを、bにcを代入していますね。ここがポイントになります。bにcを代入しているのですから、”cが0であるかないか”という問題はそのままbについていえることになります。するとaわるbを計算し、その余りをcとして、a ← b、b ← cとして先の(0)で比較すればいいことになります。よく分からない方は、もう一度よく考えてみてください。

 結局、アルゴリズムは以下のようになります。

	(0)もしbが0なら(5)へいく。
	(1)aをbで割り、その余りをcとする。
	(2)a ← bとする。
	(3)b ← cとする。
	(4)(0)へいく。
	(5)aが求める答えである。
	         (ただし、a、bは与えられた整数でa>bとする。)
 よろしいですか??

 これを実際にプログラムに直すわけですが、繰り返しに関してはCでは3種類ありましたね。今回はどれを用いればよいでしょうか。そうです。while文ですね。他のものを使って書くことは出来ますが、あまり自然ではありません。

 では前回同様、抽象言語を使ってアルゴリズムを書き直してみましょう。

	  while(bが0でないならば)
	  {
	      a÷bの余りをcとする。
	      a=b;
	      b=c;
	  }
 こうなります。最後になりましたが、関数はint型を返し、同じく、int型の引数を二つとります。

 では、これらを基にプログラムを書いてみましょう。


5)プログラムを書く───────────────────────────5)

 早速、プログラム全体を書いてみてください。インクルード文、変数、関数の宣言等を忘れないようにして下さい。









 では解答です。

演習プログラム3−1

/*  Program  */
/*  Short Program 5  */
/*  最大公約数を求める 1  */

#include <stdio.h>

int gcd(int a, int b);

main()
{
    int x, y, z;

    scanf("%d", &x);
    scanf("%d", &y);

    z = gcd(x, y);

    printf("%d\n", z);
}

int gcd(int a, int b)
{
    int c;

    while (b <> 0)
    {
        c = a % b;
        a = b;
        b = c;
    }
    return a;
}
 どうでしょうか。こんな感じになりましたか??
 では、解説しましょう。

 まず、今回も特別な標準関数は用いていないので、インクルードするのはstdio.hだけでいいでしょう。

 次に、最大公約数を計算する関数を宣言しています。返り値の型はint型、関数名はgcd、引数は二つでともint型です。

 メインルーチンは、変数を3つ宣言しており、それぞれ、2つの数を入力する変数と、その最大公約数を入れる変数になります。

 それから二つの値を入力し、その後それを関数に引数として渡し、最大公約数を求め、その結果を出力して終りです。

 問題の関数の方ですが、a、bは引数として渡されるので、余りcを宣言するだけです。後は、先のアルゴリズムに従って、書くだけです。

 最後に答えのaをリターン文で返します。


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

 いつものお約束のコンパイルです。くどいようですが、間違いが多いのは、

	・”;”がない。
	・”{}”の対応があってない。
など結構、初歩的なミスです。

 最近のコンパイラはかなり親切にエラーメッセージを出してくれるので、それほど苦労することもないでしょうが、たまに分からなくなることがあります。そういう時は大抵これだと思って間違いないでしょう。エラーがでたらもう一度エディタで書き直します。

 リンクが必要なコンパイラは、リンクもしておきましょう。


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

 完成したらテストしてみてください。適当に2つの整数を入れてみて、プログラムがきちんと動くかどうか見てください。動いたら完成です。


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

 今回の改良点は以下の通りです。

	・入力をscanf()ではないものにする。
	・入力がx<yでもきちんと動くようにする。
	・最大公約数を再帰的に求めるようにする。
	・大きな整数も扱えるようにする。(100,000とか10,000,000,000位)
 最初の点については、ここではやりません。いずれ機会があるでしょうから、その時にgetchar()を用いた方法について述べます。

 2点目は、scanf()で二つの数を読み込んだ後、大小を比較して、大きい方がx小さい方がyになるようにすればいいでしょう。

 3点目は、後半で解説します。

 4点目は、根本的にプログラムを改造する必要があります。

 コンピュータでは通常大きな数(または小さな数)を扱う時は、配列を用いて計算します。例えば大きさ100の整数型の配列を用意し、各要素として各桁の数値(0〜9)をいれるとするとこの配列で100桁の数値が表現できることになります。これに対して、四則演算を行うサブルーチンを書き、比較するサブルーチンを書かなければならないわけです。出来ないことはありませんが、今はまだ難しいでしょう。(別にやってみてもかまいませんが)


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

 と言うわけで、非再帰版はこれで完成です。
 一休みしたら、再帰版に進みましょう。

中休み

UNIX、C誕生秘話 〜ゲームで生まれたUNIX〜

 これはかなり有名な話なので、知っている人には面白くないかもしれません。

 C言語は元々UNIXというオペレーティングシステムを記述するために生まれました。UNIXの開発にはAT&T(アメリカの電信電話会社、日本のNTTにあたる)にいたKen ThompsonとDenis M Richieという人達が携わりました。当時(1960年代後半)のコンピュータは今のパソコン以下の性能しかなく、しかも高価でした。AT&Tも多分にもれず、コンピュータ不足でした。大きなプロジェクト(仕事)に参加していた彼らはそんな辛い状況にあったのです。そこで彼らは、隅の方に眠っていた少々古いマシンを再利用することにしたのです。再利用すると言ってもなにせOSというものが世に出てまもない頃の話ですから、まずそのコンピュータで走るOSを作らなければなりませんでした。そうして完成したのがUNIXです。

 と、ここまでは、表向きの話。(と言っても嘘は書いていませんが)

 実はKen ThompsonがこのOSの開発に燃えたのは別の理由があったのです。それは自分のためのゲーム専用マシンの確保でした。当時Kenはあるゲームに熱中しており、そのゲームで遊ぶためのマシンが欲しかったのです。しかし、前述の通り、コンピュータは余っていません。そこで、古いマシンを引っ張り出して、何とか使えるマシンに仕立てあげるべく、UNIXを作ったのです。このことは、当時彼らが請け負っていたプロジェクトが取り止めになったにも関わらず、古いマシンでのUNIXの開発を辞めなかったことが立証してくれます。

 そして、無事、UNIXは完成し、Kenはゲーム専用マシンを手に入れたのでした。ところがこのOS、使い勝手がよいと評判になり、他のマシンへ移植されるようになりました。最初はFortranやアセンブラで書かれていたこのOSも、新しい言語で書き直すことになりました。そこで誕生したのがC言語です。今日ではUNIXもCコンパイラ自体も(UNIX上のプログラムは全て)C言語で書いてあります。

 ”C”という名前は、BCPLという言語の後釜のB言語の次にあたる言語ということで、命名されました。

 その後、CはUNIXと共に世界中に行き渡り、今日の地位を獲得したのです。途中一度だけ仕様変更(統一規格への変更)がありましたが、それはC言語をより強力なものにしました。

 ゲームがなければ、UNIXもCも生まれなかったのかも知れませんね。

再帰版

さて、では再帰版です。

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

 ここは、前の非再帰版と変りませんね。


2)データとデータ構造──────────────────────────2)

 ここも変りません。


3)大まかな流れ─────────────────────────────3)

 メインの流れは変りませんが、関数の方が変ります。今回は再帰版ですから、

	 ・何が再帰か
	 ・停止条件は何か
ということを考えなければなりません。前回のn階乗の解説を思い出してください。(プリントアウトされているなら、それを見てもかまいません) 関数はその引数の階乗を知っていると言うことだったと思います。今回も同様で、関数は2引数の最大公約数を知っていると考えるのです。

 少し考えてみてください。









 答えは次章で。


4)細かいアルゴリズムの決定───────────────────────4)

 メインルーチンは変らないので、非再帰版のところに出てきたものと同様でいいでしょう。問題は関数のところです。これは以下のようにすればいいでしょう。

 もし(aをbで割った余りが0)なら

    最大公約数はb;

 そうでなければ     最大公約数は(bと、aをbで割った余り)の最大公約数に等しい;

 これは、問題のところに書いてある式をそのまま書き直したものです。このままでもプログラムに書き直せますが、非再帰版同様bが0の時に困ります。そこでこのアルゴリズムを書き直すわけですが、どうすればいいでしょうか。今回はノーヒントです。考えてみてください。









 この解決策も非再帰版のところでやったのと同じ方法が使えます。つまり、a ← b、b ← (aをbで割った余り)として計算させているので、(アルゴリズムの最下行)同じ様に解決します。つまり、

 もし(bが0なら)

    最大公約数はa;

 そうでなければ

    最大公約数はbとaをbで割った余りに等しい;

という風になります。

 ではこれらを元にプログラムを書いてみましょう。


5)プログラムを書く───────────────────────────5)

 では、早速考えてみてください。前回のn階乗の関数や解説などを見直してみるのもいいでしょう。インクルード文、関数、変数の宣言、リターン文等に気をつけて下さい。









 では解答です。

演習プログラム3−2

/*  Program  */
/*  Short Program 6  */
/*  最大公約数を求める 2  */

#include <stdio.h>

int gcd(int a, int b);

main()
{
    int x, y, z;

    scanf("%d", &x);
    scanf("%d", &y);

    z = gcd(x, y);

    printf("%d\n", z);
}

int gcd(int a, int b)
{
    int d;

    if (b == 0)
      d = a;
    else
      d = gcd(b, a % b);

    return d;
}

 最初の方と、メインルーチンは先のものと同じなので説明は割愛します。

 肝心の関数ですが、これは先のアルゴリズムをプログラム化したものです。

 非再帰版との違いは(余りではなく)最大公約数そのものを計算して(変数d)返しているところです。それ以外は、関数の呼び方まで同じですね(当り前ですが)


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

 コンパイルして、必要ならリンクもしましょう。エラーがでたらエディタで書き直して、もう一度コンパイルします。


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

 無事終わったら、さっきと同様にいろんな値を入れて、試してみましょう。間違えていませんか??もし、答えが違うようなら、もう一度プログラムを見直してみてください。コンパイラなどでは発見できないエラーがあることがあります。


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

 このプログラムの改良点も、先の非再帰版と同じですが、更にもう一つあります。

 もう一度プログラムを見てください。変数dは計算した値を保持して、それを(リターン文で)返す以外に使われていません。この様な場合は、変数をわざわざ用いなくても、ダイレクトに返してしまうという手があります。以下に示します。

演習プログラム3−2’

/*  Program  */
/*  Short Program 7  */
/*  最大公約数を求める 3  */

int gcd(int a, int b)
{
    if (b == 0)
      return a;
    else
      return gcd(b, a % b);

}

 メインルーチン等は同じなので省略しました。この様にリターン文をあちこちに入れることが出来ます。しかし、これは出口が沢山あるのと同じで、ちょっとでも長いプログラムになると見にくく、また分かりにくくなります。出来れば、この様な形ではなく、一つの関数に、リターン文は一つという風にした方がいいでしょう。

 以上で、演習はおしまいです。どうです、できましたか。分からなければ、最初から読み直してみるのもいいでしょう。後、プログラム組み方がほんの少しでも分かっていただけたら幸いです。

 最後になりましたが、n階乗の数学上の式とプログラム、最大公約数の数学上の式とプログラムを見比べてみてください。実は、再帰的に定義された数式は、すぐ再帰的関数に書き直すことが出来ます。再帰的に定義された数列などもすぐ出来ます。ただ、実世界には再帰的でありながら数式で書けないもの(あるいは書きにくいもの)などが多いので、そういう場合は、やはりきちんと考えなければなりません。そういう時は今まで同じ様な段階を踏んで考えて行きます。


CONTENTS / BACK-PAGE / NEXT-PAGE