• 検索結果がありません。

を区別するものではありません。

N/A
N/A
Protected

Academic year: 2021

シェア "を区別するものではありません。"

Copied!
10
0
0

読み込み中.... (全文を見る)

全文

(1)

今回は,前回に引き続き,自分で関数を定義する方法を学びますが,今回の特徴は再帰的な 関数定義を扱うことです。

再帰的な関数定義とは,関数(f としましょう)の定義の中でその関数(f)自身が使われるような 定義の仕方のことです。あくまでも「定義の仕方」を区別する概念であり,「関数そのものの性質」

を区別するものではありません。

たとえば,

n

の階乗

n!

を求める関数

f(n)

は,

n! = n*(n-1)!

という関係式を利用して,

if

文の中 で,

n=0

のときは

f(n)=1

n>0

のときは

f(n)=n*f(n-1)

と書けば,これは再帰的定義です。

一方,for ループの中で,p=1 を初期値とする変数

p に,p=p*i; という式を用いてi=1,2,…,n を

つぎつぎと掛け算するコードを書けば,これは非再帰的定義です。

このように同じ関数でも再帰的定義と非再帰的定義があり得るわけですが,関数を再帰的に定 義したときに,それを「再帰関数」と呼びます。

再帰関数の多くは,上記の

n! = n*(n-1)! のように,数学的に自然な関係式をそのままC言語で

記述したもので,決して難しいものではないのですが,コンピュータがこれをどのように実行する

のかを考え出すと,初心者にはかなり難しい面があります。今日の講義では,このような再帰関

数のコンピュータによる実行メカニズム(ふるまい)にも少し触れますが,あまり深入りしないことに

して,再帰関数の設計方法をノウハウとして学びます。また,「数学的に自然な関係式」とはいえ

ないような関係式を私たちユーザーが自ら考案する必要がある例として,累積テクニックと末尾

再帰という技術も紹介します。

(2)

このスライドの関数

sum(n) は,1

から

n までの和を計算するものです。これはs=0 を初期値と

する変数

s を用意し,for 文の中でs=s+i; を反復実行して,i=1,2,…,n をつぎつぎとs に足して

いくコードを書いても定義できます。しかし,このスライドの

sum(n) の中には反復実行のための

ループ(for 文や

while 文)がなく,if 文が1つあるだけです。その中味をよく見ると,いま定義中

sum を利用して(つまりまだsum の定義が完成していないのに)sum(n-1)を計算する部分が

含まれています。このように,関数(この場合は

sum)の定義の中でその関数(sum)自身が使われ

るような定義の仕方のことを再帰的な定義といいます。また,再帰的に定義された関数を再帰関 数といいます。

この

sum(n) の定義を見ながら,再帰関数についての基本概念を説明します。

まず最初の行に書かれている関数定義の見出し

int sum(int n)

は,関数名が

sum

,引数が

int n

,および戻り値が

int

型であることを示しています。この見出しに 加えて,プログラムのドキュメント(説明文書)あるいはプログラムのコメントの中に,この関数が「

何を」

(what)

計算するものなのかを簡潔に書きます。

sum

の場合は,「

1

から

n

までの整数の和」

と書けます。これらの情報全体が関数の仕様を表します。

2行目からは,関数

sum を「どのように」(how)計算するか,その計算手順を書きます。書き方は

基本的に前回学んだとおりですが,今回は再帰的に記述している点が異なります。つまり,if 文

の中で 「もし

n=1 ならば1 を返す」 は自明で問題ないとして,「そうでなければsum(n-1)+n を返

す」 というところが再帰的になっています。再帰関数を呼び出している部分

sum(n-1) のことを再

帰呼び出しといいます。

(3)

再帰関数

sum の定義は,数学的には sum(n) = 1 (if n=1)

sum(n) = sum(n-1)+1 (if n>1)

と表すことができ,数学的に考察すると,この式は自然なものとして受け入れられるように思いま す。

しかし,この定義がコンピュータの内部でどう実行されるのか(ふるまい)は,初心者にはかなり 理解しにくい面があります。ここでは深入りせずに,概略だけを説明します。

このスライドを見てください。上記の数学的な定義に基づいて,sum(3) が次々と展開され,最

終的には

(1+2)+3 という式となり,これが計算されて6 が得られる様子を示しています。コンピュ

ータはちょうどこれに相当する計算を巧妙な仕組みで行うのです。

(4)

その仕組みをもう少しコンピュータ寄りに描いたのがこのスライドです。

ここでは,コンピュータが1台で仕事を行うのではなく,main 関数と

sum 関数の実行主体を分

けて考えて,それぞれを長方形として図示しています。さらに,sum の引数の値ごとに実行主体 を別なものと考えます。つまり,sum(3),sum(2),sum(1) の計算はそれぞれ別々の人が行うかの ように別々の長方形で図示しています。

コンピュータのふるまいに沿って,順に見ていきましょう。

1.まず

main 関数がsum(3) に仕事を依頼します。

2.

sum(3)

の中では

n=3

となっていて,

sum(2)+3

を計算するのが仕事です。そこで

sum(3)

sum(2)

に仕事を依頼します。

3.sum(2) の中では

n=2 となっていて,sum(1)+2 を計算するのが仕事です。そこでsum(2)

sum(1) に仕事を依頼します。

4.sum(1) の中では

n=1 となっていて,境界条件が成り立つので,計算結果1 をsum(2)に返し

ます。

5.

sum(2)

の中では

n=2

となっていて,計算結果

sum(1)+2 =1+2=3

sum(3)

に返します。

6.sum(3) の中では

n=3 となっていて,計算結果sum(2)+3 =3+3=6

main に返します。

ここで,

sum(3),sum(2),sum(1)

というのは,実際には,必要に応じて

sum(n)

の関数定義から作

られるコピーのようなものと思ってください。大事なことは,各コピーがそれぞれ独自にローカル

変数

n

を持っているということです。それらは名前が同じ

(n)

ですが,異なる変数で,それぞれ別

な値

(3,2,1)

を保持できるのです。

(5)

再帰関数であろうとなかろうと,C言語のプログラミングというのは,一般に複数の関数を定義す ることがその中心的な作業です。しかし,初心者でもプロでも,関数定義の中に誤り(バグ)が潜 んでしまい,正しくプログラムが動作しないこと(不具合)が生じることがあります。プログラムに含 まれているバグの原因を突き止めて修正する活動のことをデバッグといいます。このスライドでは

,関数のデバッグについて,基本的な方法を紹介します。

デバッグの基本は,プログラムの実行が自分が期待しているように進んでいるかどうかを目で確 認することです。そのためには,プログラムの実行途中で変数の値を表示することが効果的です

。特に,関数の場合,その入口(実行の開始時点)と出口(値を戻す時点)での変数の値が重要 です。

このスライドは前のスライドの

sum

関数をデバッグする例です。入口では,

printf

を使って,引 数

n

として受け取ったデータを表示しています。表示の仕方は

call sum(3)

などとなるようにしています。

出口では,戻り値をいったん

ans

という変数に代入し,その値を

return 6

などと表示してから,実際に

return

によって値を戻して実行を終了するようにしています。

ここで示した

sum 関数はバグを含まないので,表示される内容は期待通りになりますが,もしバ

グが含まれていたら,期待と異なる表示がされるので,それをヒントとして,誤りの部分を突き止め

て修正することになります。

(6)

このスライドでは,再帰関数の設計方法を説明します。

基本的には,つぎの4ステップで作業を進めると良いでしょう。sum 関数を例とします。

(1)

まず,定義したい関数の見出しと意味(仕様)を簡潔な言葉で書きます。

int sum(int n)

= 1

から

n

までの整数の和

ここで最初の行はプログラムの一部となりますが,その次の行はコメントにするか,別途用意す るドキュメントに書きます。実際に「書く」ことが重要です。頭の中で「思う」だけではいけません。

(2) つぎに,再帰パターンを(何とかがんばって)発見します。再帰パターンとは,再帰呼び出し

の基本となる関係式のことです。

sum(n) = sum(n-1) + n (if n>0)

なぜなら,「1 から

n までの整数の和」=「1 からn-1 までの整数の和」+ n だから。

(3)

つづいて,上で求めた再帰パターンが無限に続かないようにするための境界条件とそのとき の関数の値(境界値)を求めます。

境界条件:n=1 境界値:sum(1) = 1

(4) 最後に,(1)(2)(3)で得た内容をまとめてC言語で記述します。その記述は多くの場合,スライ

ドに書かれているように,1つくらいの

if 文になります。境界条件が成り立つときには境界値を返

し,そうでないときには再帰パターンに基づいて関数値を求めます。もちろん,数学的な設計段

階で

n=1 などと書いていたものは,C言語ではn==1 などと書く必要があります。

(7)

これは最大公約数を求める関数

gcd(p, q) の再帰的定義の例です。(gcd

greatest common divisor の頭文字)

ユークリッドの互除法と呼ばれる方法に基づいていて,

p

q

の最大公約数 =

q

と 「

p

÷

q

の余り」の最大公約数

であることを使います。このスライドでは,本質的にはそれと同じですが,もっと単純に,割り算の 代わりに引き算を使って

p

q

の最大公約数 =

q

p

q

の最大公約数

(ただし,p>q とする。P<q のときは

p,q

を交換した同様な式となる)

であることを使っています。

これをC言語で書き下すのは,皆さんの演習問題としておきましょう。

(8)

これまで,sum(n)=sum(n-1)+n という関係式に基づき,sum(n) を再帰的に定義しました。この定義のい やな点は,再帰呼び出しsum(n-1) が終わっても,まだ残された仕事(戻された値にn を足すこと)がある ことです。このスライドでは,再帰呼び出しから戻された値をそのまま関数値として返して実行を終了する(

つまり残された仕事がない)ような再帰(末尾再帰)を説明します。また,累積テクニックと呼ばれるプログラミ ング技術を合わせて紹介します。

スライドを見てください。1 からn までの和を求める関数sum(n) はsum_auxという関数(補助関数)を呼び 出して,それが戻した値を返すだけです。重要な仕事は補助関数が行っています。

補助関数は,ループを使ってacc=acc+k; を反復実行し,k=1,2,…,n をつぎつぎとacc (初期値は0)に足 していっても書けますが,この考え方を累積テクニックと末尾再帰を使って書いたのがsum_auxです。ルー プによる計算の「途中の状態」(現在の状態)は,一般につぎの2つの情報によって特徴付けられます。

1.ここまでの計算の中間結果(累積値)の情報。sum の場合はacc。つまり,1 からk までの反復加算の結 果。

2.ここからの計算が行う「残りの仕事」の情報。sum の場合はk+1 とn。つまり,さらにk+1 からn まで加算 する仕事。

補助関数sum_aux(acc, m, n) は,これらの情報を3つの引数として,最終的な計算結果を戻すものです。

ただし,2つめの仮引数をk+1 という式で指定することはできないのでm としました。この関数の意味は sum_aux(acc, m, n) = acc + m +…+ n

と表せます。再帰パターンは

sum_aux(acc, m, n) = sum_aux(acc+m, m+1, n) です。なぜなら,

acc + m +…+ n = (acc+m) + (m+1) +…+ n

(9)

このスライドは,前のスライドで説明した

sum とsum_aux

のふるまいを表しています。その内容 は明らかでしょう。

この計算には末尾再帰の特徴が良く出ています。つまり,関数の実行が終了する時点で末尾 再帰の呼び出しが行われますが,このようなことが次々と繰り返されて,最後に境界条件が成り 立ったところで関数値(この例では

6)が定まります。そしてその関数値が次々とそのまま関数の

呼び出し先にそのまま戻されていって最終的な値となるのです。

スライドの右には,関数のデバッグのところで説明した方法で,関数の入口と出口でデータを

表示した結果を示しました。

(10)

以上で今回の話は終わりですが,ここに参考までに前のスライドの続きを1枚だけ置いておきま す。興味がある人は読んでください。

今回は,関数を再帰的に定義する方法を学びましたが,再帰を使わなくてもループ(for, while) があれば十分とか,再帰で定義できる関数は制限が強くてあまり使う場面がないと思う人もいそう です。

しかし,コンピュータサイエンスの理論では,一般に再帰はループより強力な表現力をもつこと が知られています。ループで書ける関数は,累積テクニックと末尾再帰を用いれば,必ず再帰的 に定義できますが,その逆は成り立たず,ある条件のもとで,再帰的に定義できる関数の中には

,ループでは書けない関数があるということです。ただし,「末尾再帰」で書かれた定義は,必ず ループで書くことができるので,このスライドでそのことを少し補足しています。

関数呼び出しのときには,実引数と仮引数が対応付けられて値がコピーされるわけですが,そ れを代入文として書きます。また,末尾再帰は事実上,ループのように同じ処理の反復となること になるので,それらの代入文はループの本体の中に書きます。さらに,末尾再帰では再帰呼び 出しの後に残された仕事がもうないことを利用して,ループが終了した時点で,acc の値を戻して 関数の実行を終了します。

このような処理はプログラムを機械語に変換するコンパイラと呼ばれるシステムソフトウェアの中 で自動的に行われることもあります。そのときの雰囲気をお伝えするために,このスライドでは,ル ープとして「高級言語」がもつ

for

文や

while

文を使わずに,機械語がもつ「分岐命令」という原 始的な制御機能をC言語内でシミュレートできる

goto

文(「

goto start;

」 という文は「

go to start]

つまり「

start

というラベルが書かれたところへ行け」という意味)というものを使ってみました。

参照

関連したドキュメント

本文書の目的は、 Allbirds の製品におけるカーボンフットプリントの計算方法、前提条件、デー タソース、および今後の改善点の概要を提供し、より詳細な情報を共有することです。

、肩 かた 深 ふかさ を掛け合わせて、ある定数で 割り、積石数を算出する近似計算法が 使われるようになりました。この定数は船

手動のレバーを押して津波がどのようにして起きるかを観察 することができます。シミュレーターの前には、 「地図で見る日本

・本計画は都市計画に関する基本的な方 針を定めるもので、各事業の具体的な

しかしながら、世の中には相当情報がはんらんしておりまして、中には怪しいような情 報もあります。先ほど芳住先生からお話があったのは

 医療的ケアが必要な子どもやそのきょうだいたちは、いろんな

職員参加の下、提供するサービスについて 自己評価は各自で取り組んだあと 定期的かつ継続的に自己点検(自己評価)

「海にまつわる思い出」「森と海にはどんな関係があるのか」を切り口に