教科書 pp.152-155参照。 「ある処理」の方法を、「ある処理」自身を使って定義することを再帰的 (recursive) 定義と言う。
階乗の計算を取り上げ再帰の例を示す。階乗 n! は次のように説明できる。
n! = 1 × 2 × 3 × … × n
しかし、この定義は「…」という部分を含んでいて曖昧である。 そこで、次のような方法で説明することを考える。
n! = (n - 1)! × n ただし n = 0 のとき n! = 1
n!の式の右辺にも (n - 1)! という階乗の計算そのものがあることに注意。 n!を計算するためには、(n - 1)! を計算して n を掛け、 (n - 1)! を計算するためには、(n - 2)! を計算して (n - 1) を掛け、 ということを n が 0 になるまで行えば良いことになる。 これをプログラムで表すと次のようになる。
int fact(int n) { if (n == 0) return 1; else return fact(n - 1) * n; }
このプログラムの実行の様子は以下のとおりである。
fact(5) を計算するためには fact(4) * 5 を計算する必要がある fact(4) を計算するためには fact(3) * 4 を計算する必要がある fact(3) を計算するためには fact(2) * 3 を計算する必要がある fact(2) を計算するためには fact(1) * 2 を計算する必要がある fact(1) を計算するためには fact(0) * 1 を計算する必要がある fact(0) は定義より 1 である fact(0) = 1 の値が計算できたので fact(1) = 1 * 1 = 1 fact(1) = 1 の値が計算できたので fact(2) = 1 * 2 = 2 fact(2) = 2 の値が計算できたので fact(3) = 2 * 3 = 6 fact(3) = 6 の値が計算できたので fact(4) = 6 * 4 = 24 fact(4) = 24 の値が計算できたので fact(5) = 24 * 5 = 120
教科書pp.158-160と類似の例題。 再帰的に定義される有名な数列にフィボナッチ数がある。 n番目のフィボナッチの数を fib(n) で表すと、 次のように定義される。
fib(n) = fib(n - 2) + fib(n - 1) ただし、n = 2 または n = 1 のとき fib(n) = 1
この数列は1, 1, 2, 3, 5, 8, ... と続く数列で、 隣り合う2つの数を足し算すると、その上位の数になる数列である。 例えば、 1+1=2, 1+2=3, 2+3=5, 3+5=8, ... と永遠に続いていく。 また、1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, .... と 急速に大きくなることも知られている。
再帰的定義を用いてフィボナッチ数を求めるプログラムは以下のとおり。
int fib(int n) { if (n == 1 || n == 2) return 1; else { int ans = fib(n - 2) + fib(n - 1); return ans; } }
この計算方法の特徴は、 メソッドの中で2回の再帰呼び出しが行われていることである。 例えば、fib(5)の計算では fib(3) と fib(4) の2回の呼び出しを行う。
この計算の様子を図で表すと次のようになる。
fib(5) / \ / \ / \ fib(3) fib(4) / \ / \ fib(1) fib(2) fib(2) fib(3) / \ fib(1) fib(2)
再帰を理解する練習として、計算の順序を 1 〜 9 まで番号づけしてみよう。
さて、この計算は再帰を使って書くと簡単であるが、実際の手順は複雑であるため 繰り返しで書き換えることは難しい。 このように、複数回にわたって再帰呼び出しを行う計算を「真に再帰的である」と言う。
先週学んだリストと再帰には構造的に類似していることがわかる。 以下に再帰とリストのプログラムを示す。
再帰
int fact(int n) { if (n == 0) return 1; else return fact(n-1) * n; }
リストのノード
class Node { String element; Node next; }
これらのプログラムで注目すべきポイントは、 両方とも自分から自分自身を使うような構造になっていることである。 リストも再帰を使って処理するのが容易なデータ構造の一つである。
follow_r(x)の、xの初期値はheadとする。
void follow_r(Node p) { if (p == null) return; <-- nullすなわちこれ以上たどるノードがなければ、何もせず戻る p に関する処理; follow_r(p.getNext()); <-- 次のノードを引数に与えて、再びこのメソッドを呼び出す }
提出先: /home/submit/JavaLib/[出題日]/[学籍番号]
[注意] 本課題では、1つクラスを1つのファイルとして作成することを想定している。 ファイル名は、「public宣言を行ったクラスの名前.java」 である。 ファイルが別々であっても、同じディレクトリにあるpublic宣言されたクラスは、 相互に利用することができる。 (詳しく知りたい人は、「パッケージ」について調べると良い)
再帰的定義に基づき、2つのint型の数の最大公約数を求めるメソッド gcd を作成しなさい。 また、作成したメソッド gcd を呼び出して最大公約数を求め結果を表示する main メソッドも作成し、正しく動作することを確認しなさい。 提出ファイル名は Gcd.java とする。
public class Gcd { public static void main(String[] args) { /* fill in here */ } static int gcd(int a, int b) { /* fill in here */ } }
リストの回の問題で作成したクラス MyLinkedList を拡張し、 再帰呼び出しを使ってリストの内容を表示するメソッド print_r を作成しなさい。 クラス名は MyLinkedList2 とする。 (提出ファイル名 MyLinkedList2.java)
次のようなmainメソッドで正しく動作することを確認すること。
public class MyLinkedList2Main { public static void main(String[] args) { MyLinkedList2 list = new MyLinkedList2(); System.out.println("空のリストの内容を表示 (何も表示されなければok)"); list.print_r(); System.out.println("======================="); list.addLast("Copernicus"); list.addLast("Newton"); list.addLast("Heisenberg"); list.addLast("Einstein"); list.addLast("Maxwell"); System.out.println("リストの内容を表示 (Copernicus, Newton, Heisenberg, Einstein, Maxwellの順に表示されればok)"); list.print_r(); } }
前の問題で作成したクラス MyLinkedList2 をさらに拡張し、 再帰呼び出しを使ってノードの削除を行うメソッドを作成しなさい。 クラス名は MyLinkedList3 とする。 (提出ファイル名 MyLinkedList3.java)
メソッド remove_r(String elem, Node p) は次のような考え方で リストの再構成を行う。
以下の断片を組み合わせることで remove_r メソッドを完成させることができる。 順番は各自考えること。
もし、pが削除すべきノードであれば、 pの次のノードの在処を返す そうでなければ、 pそのものの在処を返す
再帰呼び出しによって返ってきた「次のノードの在処」をpのnextにセット
if (p == null) return null;
次のようなmainメソッドで、正しく動作することを確認すること
public class MyLinkedList3Main { public static void main(String[] args) { MyLinkedList3 list = new MyLinkedList3(); list.addLast("Copernicus"); list.addLast("Newton"); list.addLast("Heisenberg"); list.addLast("Einstein"); list.addLast("Maxwell"); list.print_r(); list.remove_r("Copernicus"); list.remove_r("Heisenberg"); list.remove_r("Maxwell"); System.out.println("======================="); System.out.println("削除後 Newton, Einstein が残っていればok"); list.print_r(); } }
作成したプログラムが正しく動作するかどうか、必要かつ十分なテストを行うこと。