] > recursive call

第 4 回 再帰処理

本日の内容


このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。

4-1. 再帰処理

関数の中で関数を呼び出すことができます。 これを再帰処理と言います。 問題を解決する際、問題を分解しても分解した部分に同じ解法を適用できるよ うな場合、この再帰処理は有効です。 前々回まで取り上げていた「階乗」の計算では、 n の階乗は for 文を用いて ましたが、次のような漸化式でも書けます。

{ n! = n ( n - 1 )! 0! = 1

これを C の関数にすると次のようになります。


int factor(int n){
  if(n==0){return 1;}
  else{ return n*factor(n-1); }
}

この関数の場合、 for 文で単純に書けるので、再帰処理にする必要はありま せん。 しかし、組合せの数ではこの再帰処理が有効です。 実は、先日紹介している factor をもとに計算する組合せの数では 20C2 = 20*19 2*1 = 190 もオーバフローして計算できません。これは、内部で factor を使い、 20! > 1018 を計算しているからです。 結果は小さい値でも、途中で計算がおかしくなるほど巨大な数を使ってしまう ので、正常に計算できなくなります。 そこで、組合せの数を factor を使わずに計算する必要があります。

そこで、パスカルの三角形を使って計算する方法を考えます。 パスカルの三角形とは次のような数表のことです。

        1   1
      1   2   1
    1   3   3   1
  1   4   6   4   1
1   5  10  10   5   1

各値は上の左右の値を足したものになっています。 そして、その値は組合せの数を表しています。 一番上は 1C0 1C1 を、 二列目は 2C0 , 2C1 , 2C2 を表しています。 もともと、n 個のものから m 個のものを取り出す組合せの数は、 ある n 個の中の 1 つのものに注目して、 (1) そのものを選んだ時の組合せの数と (2)それを選ばなかった時の組合せの数 を足したものになるはずです。 つまり、それは (1)n-1 個の中から m-1 個を取り出す組合せの数と (2)n-1 個の中から m 個を取り出す組合せの数 の和になります。 式で書くと nCm = n-1 Cm-1 + n-1 Cm となります。 なお、 nC0 = 1 , nCn = 1 とします。 これを C 言語で計算させるプログラムは次の通りです。


int combination(int n, int m){
  if(m==0){ return 1; }
  else if(n==m){ return 1; }
  else{ return combination(n-1,m-1)+combination(n-1,m); }
}

このようにすると計算の途中で行われるのは足し算のみなので巨大な値は扱い ません。 従って、大きな値でも求めることができるようになります。

演習4-1

上の combination を使って、組合せの数 5C2 , 20C2 を求めなさい。

演習4-2

フィボナッチ数列は次のように定義されます。 f(50)を求めなさい。

f(0)=1 , f(1)=1 , f(n)= f(n-1)+ f(n-2)

演習4-3

アッカーマン関数は次のように定義されます。 この関数は原始帰納関数という関数のクラスでは計算できないほど値が大きい 関数です。 この関数を計算し、a(0,m), a(1,m), a(2,m) などがどのような関数になるか 考えなさい。

{ a(0,m) = m+1 a(n,0) = a(n-1,1) a(n,m) = a(n-1, a(n,m-1))

演習4-4

ハノイの塔とは次のようなパズルです。 大きさが一回りずつ違う円盤を大きい順に下から並べます。 そして、次のルールでその円盤の山を移動させます。

  1. 一度には一枚しか動かせない。
  2. 移動可能な場所は、現在位置を含み三箇所
  3. 小さい円盤の上に大きい円盤を置くことはできない

以下に 3 枚での例を示します。

abc
1 =
==
===
--a--



--b--



--c--
2
==
===
--a--


=
--b--



--c--
move 1 from a to b
3

===
--a--


=
--b--


==
--c--
move 2 from a to c
4

===
--a--



--b--

=
==
--c--
move 1 from b to c
5


--a--


===
--b--

=
==
--c--
move 3 from a to b
6

=
--a--


===
--b--


==
--c--
move 1 from c to a
7

=
--a--

==
===
--b--



--c--
move 2 from c to b
8


--a--
=
==
===
--b--



--c--
move 1 from a to b

この解法を再帰的に考えます。 もし、hanoi(n-1,'a','b','c') が n-1 枚の円盤を a から b へ移動する解法 を出力するとします(c はもう一つの領域)。 すると、 n 枚の円盤を a から b へ移動する解法は次のように考えられます。

  1. まず、 n の上に乗っている n-1 枚の円盤を a から c へ移動します。つ まり hanoi(n-1,'a','c','b')
  2. そして、n を a から b へ移動します。 move n from a to b
  3. 最後に c にある n-1 枚の円盤を b に移します。 hanoi(n-1,'c','b','a')

このようにすると hanoi(n,'a','b','c') の解法を出力します。 hanoi(3,'a','b','c') と hanoi(4,'a','b','c') の解法を求めなさい。


4-2. 動的な領域確保

C 言語では変数の宣言は常に関数の先頭に書く必要があります(C++ や Java では使う前であればどこでも宣言できます)。 また配列変数は常にあらかじめサイズが決められていました。 関数の実行後に変数領域を確保することは可能でしょうか?

このために用意されているのが、malloc 関数です。 これは引数にサイズを指定するとそのメモリを動的に確保して、そのメモリの 先頭番地をポインタの形で返す関数です。 但し、戻ってきた値に対して適切なポインタ型にキャストする必要があります。 これを利用する時は stdlib.h ヘッダファイルを読み込む必要があります。 また、確保した領域は、使い終ったら free 関数で解放する必要 があります。 解放をきちんとやらないとメモリの利用状況が不安定になります。この状態を メモリリークと言います。 プログラムが突然終了したり、 OS が不安定になったりしますので、この malloc と free は気をつけて使う必要があります。

例えば、二つの文字列つなげるのに、あらかじめ多めに領域を取らずに動的に 割り当てるには次のようにします。


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
main(){
  char x[]="abcd";
  char y[]="efgh";
  char *z;
  int i,j,k;
  i=strlen(x);
  j=strlen(y);
  z=(char *)malloc((i+j+1)*sizeof(char));
  strcpy(z,x);
  strcat(z,y);
  printf("文字列 %s と %s をつなげると %s。\n",x,y,z);
  free(z);
}

4-3. C++ でのオブジェクト指向プログラミング

クラスの作成

C++ ではオブジェクトはクラスの型の変数という形で宣言することで作られま す。


クラス名 変数名;

この時、変数の初期化のためにクラス名と同じ名前のメソッドが呼び出されま す。この、オブジェクトの初期化のために呼び出される、クラス名と同じ名前 のメソッドをコンストラクタと呼びます。 この方法で作成したオブジェクトに対してメッセージを送る時は次のようにし ます。


変数名.メソッド名(引数);

一方、C++ ではもう一つ、クラスのポインタを使い、オブジェクトを 参照する形でもオブジェクトを作成できます。 new 演算子を使い、次のようにコンストラクタを呼び出します。


クラス名 *オブジェクトの参照名;
オブジェクトの参照名 = new クラス名;

または簡単に次のようにも書けます。


クラス名 *オブジェクトの参照名 = new クラス名;

右辺のクラス名はコンストラクタを示します。 この形で宣言したオブジェクトに対してメッセージを送る時は次のようにしま す。


オブジェクトの参照名->メソッド名(引数);

さて、この C++ で導入された new 演算子は malloc 関数と同様に領域を確保 します。 しかし、malloc 関数とは違い、コンパイラとランタイムルーチン(C++ で作っ たプログラムをを実行する時に使うプログラム)で処理されますので安全に使 用できます。 なお、new で確保した領域は関数の終了時などに自動的に廃棄されません。 関数の終りに delete コマンドで消去する必要があります。 delete コマンドが送られたオブジェクトは「~クラス名」という名前のメソッ ドが呼び出されます。このメソッドをデストラクタと言います。

なお、「クラス名 オブジェクト名」で宣言されたオブジェクトもスコー プ(関数内やブロック内)を外れると自動的にデストラクタが呼び出され、 廃棄されます。

演習4-5

次の C++ プログラムを動かすとどのような画面出力が得られるか正確に予想 しなさい。さらに実際に動かして予想と一致するか確かめなさい。 また、予想に反した時、どうしてそのように動いたのか考え説明しなさい。


#include <iostream>
using namespace std;
class a{
private:
  int b;
public:
  a(int i){
    b=i;
    cout << b << " is born."
	 << endl;
  }
  void set(int i){
    b=i;}
  int get(){
    return b;
  }
  ~a(){
    cout << b << " is being deleteded" 
	 << endl;
  }
};
main(){
  a x(2);
  a *y;
  { a z(3);
  y=&z;
  cout << x.get() << ","
       << y->get() << endl;
  }
  a z(4);
  cout << x.get() << ","
       << y->get() << ","
       << z.get() <<endl;
}

値呼び出し

関数の呼び出し方には、理論上三種類あります。

値呼び出し
呼び出された関数の中で、ローカル変数に引数の値をコピーして使う。
変数呼び出し
引数で指定した変数を、呼び出された関数は使用できる。
名前呼び出し

このうち、C 言語では値呼び出ししか出来ないことは前回説明しました。 しかし、C++ 言語では変数呼び出し(C++ 用語では参照呼び出し) ができます。 関数定義の際、引数の定義で変数の前に & を付けると参照呼び出しであ ることが指定できます。 次のプログラムは C 言語ではコンパイルできませんが、 C++ ではコンパイル でき、予想通りの動作をします。


#include <stdio.h>
void swap(int &a, int &b){
  int c;
  c=a;
  a=b;
  b=c;
  return;
}
main(){
  int a=3,b=4;
  printf("%d, %d\n",a,b);
  swap(a,b);
  printf("%d, %d\n",a,b);
}

4-4. タートルグラフィックのクラス

実用的なクラスの例として、多少高級なグラフィックのパッケージを考え ます。 ここで示すタートルグラフィックというのは、次のようなコマンド群からなる ものです。

これを点から点へ線を引くことしかできない低レベルなグラフィックから実現 することを考えます。 クラス Turtle のメソッドとして次のものが考えられます。

クラスの宣言部は次のようになるはずです。


class Turtle {
private:
  double x;
  double y;
  double angle;
public:
  Turtle();
  ~Turtle();
  home(int _x, int _y);
  forward(int n);
  turn(int d);
  skip(int n);
};

このうち、コンストラクタ Turtle::Turtle() はグラフィックの初期化、デス トラクタ Turtle::~Turtle() はグラフィックの後処理をします。

演習4-6

以下の空欄を埋めて Turtle クラスのメソッドを実装しなさい。


#include <math.h>
#include グラフィック 
// 次の節で Windows 用に改造します。
void Turtle::home(int _x, int _y){
   // x, y に値を代入
}
void Turtle::forward(int n){
  double newx, newy;
  // newx, newy を計算
  line(x,y,newx,newy);
  // x,y に newx, newy を代入
}
void Turtle::turn(int n);
  // angle に n を足す
}
void Turtle::skip(int n){
  double newx, newy;
  // newx, newy を計算
  // x,y に newx, newy を代入
}

4-5. Windows のプログラミング

オブジェクト指向言語である Smalltalk で導入された GUI(グラフィックユー ザインタフェース) を Apple 社は真似て Lisa や Macintosh を作りました。 さらに Microsoft 社は Macintosh を真似て Windows を作ったため、 Windows の UIもオブジェクト指向になっています。 しかし、内部での処理はオブジェクト指向になってなく、次のようなプログラ ミング技術(イベントドリブン)を使っています。

Windows のアプリケーションソフトは、常に動き続けているわけではありませ ん。 Windows が必要に応じて送ってくる「イベント」を処理しては終了するという ことを繰り返します。 ユーザは画面上のオブジェクトにマウスやキーボードを使ってメッセージを送 りますが、これを Windows はイベントとして処理をし、アプリケーションに 送ります。

一方、GUI では、ウィンドウの形などはパラメータだけを与えて生成した方が、 様々なオブジェクトでもある程度外観が統一でき、操作性が一貫させられます。 そのため、リソースファイルというファイルを作り、リソースコンパイラで別 にコンパイルしてリンクします。

このため、Windows のソフトは次の形になります。


#include <Windows>
int WINAPI WinMain(引数){
  リソースの指定と本当のアプリケーションの関数を登録;
  終了;
}
void 本当のアプリケーション(イベント)
{
  switch(イベント){
  case メッセージ1:{
    メッセージ1 が来た時の処理;
    終了;
  }
  case メッセージ2:{
    メッセージ2 が来た時の処理;
    終了;
  }
  ...
  }
}

Windows のイベントは UINT 型です。今回は次の 3 つのイベントの処理を説 明します。

WM_INITDIALOG
一番最初に呼び出される時に受けるイベント。ここでウィンドウを作った り、変数を初期化したりする。
WM_PAINT
画面を表示しなければならなくなった時(ウィンドウが開いたとか、上に 乗っているウィンドウが無くなったとか)受けるイベント。 ここに表示したい画面を書くプログラムを置く。
WM_COMMAND
ボタンを押されるなど、ユーザからの指示により発生するイベント。 イベントの内容はさらに引数で与えられる。 特に、 IDCANCEL はウィンドウの閉じるボタン(X)で発生するイベントであり、 アプリケーションの終了操作を行わねばならない。

さて、前節で説明したタートルグラフィックを Windows で実現するには、今 説明した Windows のプログラミングに従い、その上で Turtle クラスを結合 します。 そして、実際にグラフィックを表示させる部分は WM_PAINT イベントの処理で 書きます。 なお、Windows ではウィンドウやグラフィックの対象などいろいろな資源が ハンドルと呼ばれる一種の変数により参照されます。 また、現在の点という概念がタートルグラフィックとは別に存在しており、 MoveToEX という現在点を移動させる関数と LineTo という新しい点まで線を 引く関数が用意されています。 以下に四角を画面に表示するプログラムを示します。HWND や POINT などは Windows で管理しなければならないウィンドウや点を示します。


//turtle.h
class Turtle {
private:
  HWND hwnd;
  PAINTSTRUCT ps;
  POINT sp;
  double x,y;
  double angle;
public:
  Turtle(HWND);
  ~Turtle();
  void forward(int);
  void turn(double);
  void home(double,double);
  void skip(int);
};


//turtle.cpp
#include <Windows.h>
#include "turtle.h"

Turtle::Turtle(HWND h){
  hwnd=h;
  BeginPaint(hwnd, &ps);
}
Turtle::~Turtle(){
  EndPaint(hwnd, &ps);
}
void Turtle::forward(int n){
  double dx,dy;
  dx=n*cos(angle*3.141592/180);
  dy=n*sin(angle*3.141592/180);
  LineTo(ps.hdc,(int)(x+=dx),(int)(y+=dy));
}
void Turtle::turn(double dangle){
  angle += dangle;
}
void Turtle::home(int xsize, int ysize){
  x=(double)_x;
  y=(double)_y;
  angle=0.0;
  MoveToEx(ps.hdc, _x, _y, &sp);
}
void Turtle::skip(int n){
  double dx,dy;
  dx=n*cos(angle*3.141592/180);
  dy=n*sin(angle*3.141592/180);
  MoveToEx(ps.hdc, (int)(x+=dx), (int)(y+=dy), &sp);
}


//box.h
#define XSIZE 100
#define YSIZE 100


//box.cpp
#include <Windows.h>
#include <math.h>
#include "box.h"
#include "turtle.h"

BOOL CALLBACK box( HWND, UINT, WPARAM, LPARAM );
int x_size, y_size;

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE, LPSTR, int)
{
  DialogBox( hInstance, "DLG_DATA", HWND_DESKTOP, (DLGPROC)box ) ;
  return 0;
}

BOOL CALLBACK box( HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
  static HWND hwnd_gr;

  switch(msg){
  case WM_INITDIALOG:{
    HDC hdc = GetDC(hwnd);
    TEXTMETRIC tm;
    GetTextMetrics(hdc, &tm);
    ReleaseDC(hwnd, hdc);
    y_size = (int) (YSIZE/8.0*(float)tm.tmHeight);
    x_size = (int) (XSIZE/4.0*(float)tm.tmAveCharWidth);
    return TRUE;
  }
  case WM_PAINT:{
    Turtle t(hwnd);
    t.home(x_size/2,y_size/2);
    t.forward(10);
    t.turn(90);
    t.forward(10);
    t.turn(90);
    t.forward(10);
    t.turn(90);
    t.forward(10);
    return TRUE;
  }
  case WM_COMMAND:
    switch(LOWORD(wp)){
    case IDCANCEL:
      EndDialog( hwnd, -1);
      return TRUE;
    }
  }
  return FALSE;
}


//box.rc
#include <windows.h>
#include "box.h"

DLG_DATA DIALOG DISCARDABLE 10, 10, XSIZE, YSIZE
STYLE DS_MODALFRAME | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU
CAPTION "Box"
FONT 10, "system"
BEGIN
END


# Makefile
TARGET = box
CXX = g++
RESC = windres
RES_INCS = 
LIBS = -lgdi32
.SUFFIXES: .ro .rc
.rc.ro:
	$(RESC) $(RES_INCS) -i $< -o $@ -O coff

$(TARGET).exe: $(TARGET).o $(TARGET).ro turtle.o
	$(CXX) -o $@ $^ $(LIBS)

turtle.o: turtle.h
$(TARGET).ro: $(TARGET).h
$(TARGET).o: $(TARGET).h

なお Makefile で $< は依存ファイルの最初のファイル名を示します。こ の例だと $^ は box.rc box.h と二つのファイルになってしまい、リソースファ イルがうまく作れません。しかし、 $< とすると box.rc のみになり、正 しくリソースファイルを作ることが出来ます。

4-6. 再帰処理によるグラフィック

このタートルグラフィックと再帰処理を用いてグラフィックを描きましょう。 木のような図形を考えます。 幹を一本描き、その先から短い幹が左右に出るような図形を考えます。 この図形を描く関数を tree(int length) とします。 するとこの関数は次のような手順で描けます。

  1. length 分だけ幹を描きます。
  2. 左に曲がり、 tree(length / ○) を描く。
  3. 同じ場所で右に曲がり、 tree(length / ○) を描く。

ここで、左右にそれぞれ 30 度曲がることにします。また、長さは毎回 1/2 になることにします。 そして、上の手順にあるように左右で同じ地点から出発しなければなりません ので tree の手続きが終ったら元の位置に戻ることにします。 この時向きは始めの向きから 180 度曲がった方向を向いているものとします。 まとめると次のようになります。

  1. length が 1 より小さければ 180 度回転して終了する。
  2. length 分だけ幹を描きます。
  3. 左に 30 度曲がり、 tree(length / 2) を描く。
  4. 左に 120 度曲がり、 tree(length / 2) を描く。
  5. 左に 30 度曲がり、 length 分だけ進みます。
木を描く手順

void tree(Turtle &t, int length){
  if(length < 1){
    t.turn(180);
    return;
  }
  t.forward(length);
  t.turn(30); tree(t, length/2);
  t.turn(120); tree(t, length/2);
  t.turn(30); t.skip(length);
  return;
}

演習4-7

上の tree を完成させなさい。

演習4-8

Koch 曲線とは次のような曲線です。この曲線を描きなさい。

コッホ曲線

坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科