第 4 回 再帰処理

本日の内容


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

4-1. 再帰処理

同じ関数の中で自分自身の関数を呼び出すことができます。 これを再帰処理と言います。 今回は再帰処理の有用性について説明します。

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

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

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


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

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

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

        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; }
  if(n==m){ return 1; }
  return combination(n-1,m-1)+combination(n-1,m);
}

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

演習4-1

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

演習4-2

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

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

なお、この計算では一度計算した値を何度も計算し直すので効率が悪いです。 例えば、グローバルな配列変数を用意し初期値を 0 としておいて、 0 だった らその値を計算して配列変数に代入し、 0 でなければ配列変数の値を使うよ うにすると f(40) 程度まで瞬時に求められるようになります。

また、 f(n) を求める時、f(i) が何回計算されるか、さらに、再帰処理が行わ れるかを n の関数で求めなさい。

演習4-3

アッカーマン関数は次のように定義されます。 この関数は原始帰納関数という関数のクラスでは計算できないほど値が大きく なる関数です。 この関数に対し、小さなパラメータの値を計算し、a(0,m), a(1,m), a(2,m), a(3,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++ 言語固有の話(2)

関数の呼び出し方

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

値呼び出し
呼び出された関数の中で、ローカル変数に引数の値をコピーして使う。
変数呼び出し
引数で指定した変数を、呼び出された関数は使用できる。
名前呼び出し
引数に式を書いた場合、その引数の値が必要かどうかを関数の中で判断す るため、式のまま関数に引き継ぐ。

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


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

なお、 swap は別に引数が int である必然性はないので、本来はテンプレー トで定義すべきでしょう。


#include <cstdio>
template <typename T>
void swap(T &a, T &b){
  T c;
  c=a;
  a=b;
  b=c;
}
int main(){
  int a=3,b=4;
  std::printf("%d, %d\n",a,b);
  swap(a,b);
  std::printf("%d, %d\n",a,b);
  double c=3.1, d=4.5;
  std::printf("%f, %f\n",c,d);
  swap(c,d);
  std::printf("%f, %f\n",c,d);
}

さらに iostream を使えば異なる型に対して同じ関数でテストができます。


#include <iostream>
template <typename T>
void swap(T &a, T &b){
  T c;
  c=a;
  a=b;
  b=c;
}
template <typename T>
void test(T &a, T &b){
  std::cout << a << ", " << b << std::endl;
  swap(a,b);
  std::cout << a << ", " << b << std::endl;
}
int main(){
  int a=3,b=4;
  test(a,b);
  double c=3.1, d=4.5;
  test(c,d);
}

ユーザ定義定数

C++ 言語には const という定数を宣言する演算子があります。 これを使うと、値を変更しないことを宣言でき、不用意な代入操作によるプロ グラムミスなどをコンパイル時にエラーとして通知してもらえるようになりま す。

定数変数

C 言語で定数を宣言するには #define を用いました。 #define で宣言する定数には型がないという欠点があります。 C++ では const を添えて、変数を初期化しながら宣言することで型情報を持っ た定数を宣言できます。

例4-1

#include <iostream>
int main(){
  const int isan = 3;
  const double dsan = 3;
  std::cout << isan/2 << dsan/2 << std::endl;
}

またポインタ宣言に関しては * の前に const を書くと参照先の値を変更でき なくなり、また、 * の後に書くとポインタ自体を変更できなくなります。

例4-2

const char *p="abcde";
char * const q="abcde";
char r[]="abcde";
char * const s=r;
++p ;        //OK
// *p='x';   //NG
//q++;       //NG
//*q='y';    //NG "" で指定した文字列自体は
             //  const char * 型なので変更できない
r[0]='y';    //OK "" で初期化した文字配列は変更できる
//s++;       //NG
*s='z';      //Ok

const 引数

関数定義の際、(特に参照型で)const を宣言すると関数内で値を変更できなく なります。

例4-3

int plusone(const int& x){
  // x++; // NG;
  return x+1; //Ok;
}

const メンバ関数

メンバ関数の定義でメンバ関数名の後に const を指定すると、 メンバ変数を変更しないメンバ関数を定義できます。

例4-4

class X {
  int y;
  int plusone() const {
    // y++; // NG
    return y+1; // Ok
  }
};

このようにプログラムミスを無くすため、値を変化させないものには const を指定するようにします。

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

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

これは、点を打つカーソルが足跡を残しながら相対移動しかできないモデルで す。 この足跡を残すカーソルのことをタートル(亀)と言います。 グラフィックの機能としてはかなり限定されていますが、これでもそれなりの 図形を描くことができることを説明します。 なお、このタートルグラフィックは教育用言語 Logo などで採用されています。

さて、C++ でこのタートルグラフィックを作る場合、クラス Turtle として以 下のメソッドを含むようなものを考えます。

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


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

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

これを点から点へ線を引くことしかできない低レベルなグラフィックから実現 することを考えます。

演習4-5

以下のコメントの部分をプログラムで置き換えて、 Turtle クラスのメソッド を実装しなさい。


#include <cmath>
#include グラフィック系関数のプロトタイプ
// 次の節で Windows 用に改造します。
void Turtle::home(int _x, int _y){
   // x, y に値を代入
}
void Turtle::forward(double n){
  double newx, newy;
  // newx, newy を計算
  line(x,y,newx,newy);
  // x,y に newx, newy を代入
}
void Turtle::turn(double n);
  // angle に n を足す(ラジアンに直す)
}
void Turtle::skip(double n){
  double newx, newy;
  // newx, newy を計算
  // x,y に newx, newy を代入
}

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

Windows の UI(ユーザインターフェイス) はアイコンにメッセージを送ること で操作するので、一応オブジェクト指向になっています。 しかし、内部での処理はオブジェクト指向になってなく、次のようなプログラ ミング技術(イベントドリブン)を使っています。 イベントドリブンとはイベントが発生した時、それを処理すると いう仕組みでプログラムを動作させるプログラミング手法です。 これは端的に言うと、Windows のアプリケーションソフトは、常に動き続ける ように書いてはいけないってことです。 Windows のアプリケーションソフトは Windows が必要に応じて送ってくる 「イベント」を処理しては終了するというプログラムを書き、イベントの管理 は Windows に委ねます。 イベントとは、例えばユーザは画面上のオブジェクトにマウスやキーボードを 使ってメッセージを送りますが、このようなユーザやシステムが発生する刺激 のことをイベントと言います。 この操作を受け取った Windows はイベントとしてアプリケーションに送ります。

コンピュータを操作する上で、使用するアプリケーションごとに操作がまった く異なるのはユーザにとって負担です。そのため、 GUI はある程度共通の方 が都合が良いです。 これを実現するため、Windows のソフトを作る際に、ウィンドウのデザインは 一から全て作るのではなく、標準的なウィンドウのタイプ、メニュー、ボタン などをもとにパラメータで指定します。 この指定のためのファイルはリソースファイルと呼ばれ、リソー スコンパイラで別にコンパイルし、実際の処理を行うプログラムにリンクします。

このため、Windows のアプリケーションソフトは次の形になります。


#include <Windows.h>
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.hpp
#include <Windows.h>
class Turtle {
private:
  HWND hwnd;
  PAINTSTRUCT ps;
  POINT sp;
  double x,y;
  double angle;
public:
  Turtle(HWND);
  ~Turtle();
  void forward(double);
  void turn(double);
  void home(int,int);
  void skip(double);
};


//turtle.cpp
#include <Windows.h>
#include <cmath>
#include "turtle.hpp"
Turtle::Turtle(HWND h){
  hwnd=h;
  BeginPaint(hwnd, &ps);
}
Turtle::~Turtle(){
  EndPaint(hwnd, &ps);
}
void Turtle::forward(double n){
  double dx,dy;
  dx=n*std::cos(angle);
  dy=n*std::sin(angle);
  LineTo(ps.hdc,static_cast<int>(x+=dx),static_cast<int>(y+=dy));
}
void Turtle::turn(double dangle){
  angle += dangle * 3.141592 / 180;
}
void Turtle::home(int _x, int _y){
  x=static_cast<double>(_x);
  y=static_cast<double>(_y);
  angle=0.0;
  MoveToEx(ps.hdc, _x, _y, &sp);
}
void Turtle::skip(double n){
  double dx,dy;
  dx=n*std::cos(angle);
  dy=n*std::sin(angle);
  MoveToEx(ps.hdc, static_cast<int>(x+=dx), static_cast<int>(y+=dy), &sp);
}


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


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

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 = static_cast<int>(
    static_cast<double>(YSIZE)/8.0*tm.tmHeight);
    x_size = static_cast<int>(
    static_cast<double>(XSIZE)/4.0*tm.tmAveCharWidth);
    return TRUE;
  }
  case WM_PAINT:{
    Turtle t(hwnd);
    t.home(x_size/2,y_size/2);
    t.forward(30);
    t.turn(90);
    t.forward(30);
    t.turn(90);
    t.forward(30);
    t.turn(90);
    t.forward(30);
    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.hpp
$(TARGET).ro: $(TARGET).h
$(TARGET).o: $(TARGET).h

なおターゲットファイル一つだけの名前を取り出すために $< という変数 を使用しています。 これにより box.rc を参照でき、正しくリソースファイルを作ることが出来ます。

box.cpp のリファクタリング

関数への分割

この box.cpp にある box 関数は、長く、複数の事柄が詰まっています。 そこで、まず、これらを関数に分割します。


BOOL init( HWND hwnd)
{
    HDC hdc = GetDC(hwnd);
    TEXTMETRIC tm;
    GetTextMetrics(hdc, &tm);
    ReleaseDC(hwnd, hdc);
    y_size = static_cast<int>(
    static_cast<double>(YSIZE)/8.0*tm.tmHeight);
    x_size = static_cast<int>(
    static_cast<double>(XSIZE)/4.0*tm.tmAveCharWidth);
    return TRUE;
}
void drawBox(Turtle t, int size){
    t.forward(size);
    t.turn(90);
    t.forward(size);
    t.turn(90);
    t.forward(size);
    t.turn(90);
    t.forward(size);
}
BOOL paint(HWND hwnd, int size){
    Turtle t(hwnd);
    t.home(x_size/2,y_size/2);
    drawBox(t,size);
    return TRUE;
}
BOOL command(HWND hwnd, WPARAM wp)
{
  if(LOWORD(wp)!=IDCANCEL){
      EndDialog( hwnd, -1);
      return TRUE;
  }
  return FALSE;
}
  
BOOL CALLBACK box( HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
  switch(msg){
  case WM_INITDIALOG:return init(hwnd);
  case WM_PAINT: return paint(hwnd, 10);
  case WM_COMMAND: return command(hwnd, wp);
  }
  return FALSE;
}

オブジェクト化

次に、グローバル変数を含むようなオブジェクトクラス Graphic を作り、図形描 画とWindows のコントロールとを分離します。 また、実際の描画は Turtle の機能の拡張になっているので、 Turtle のサブ クラスを作り、メソッドを paint と呼ぶことにします。 なお、 Graphic オブジェクトは一度生成したら、使い捨てにせず、毎回 Windows から呼び出される度に継続して使いつづけるようにするため、 static 宣言をします。関数内で変数を static 宣言をすると、その変数は使 い捨てではなく、毎回同じものが使われます。


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


//graphic.hpp
#include <Windows.h>
class Graphic {
private:
  int x_size, y_size;
public:
  Graphic(){}
  BOOL init( HWND hwnd);
  BOOL paint(HWND hwnd, int size);
  BOOL command(HWND hwnd, WPARAM wp);
};


//graphic.cpp
#include <Windows.h>
#include "box.h"
#include "graphic.hpp"
#include "box.hpp"
BOOL Graphic::init( HWND hwnd)
{
    HDC hdc = GetDC(hwnd);
    TEXTMETRIC tm;
    GetTextMetrics(hdc, &tm);
    ReleaseDC(hwnd, hdc);
    y_size = static_cast<int>(
    static_cast<double>(YSIZE)/8.0*tm.tmHeight);
    x_size = static_cast<int>(
    static_cast<double>(XSIZE)/4.0*tm.tmAveCharWidth);
    return TRUE;
}
BOOL Graphic::paint(HWND hwnd, int size){
    Box t(hwnd);
    t.home(x_size/2,y_size/2);
    t.paint(size);
    return TRUE;
}
BOOL Graphic::command(HWND hwnd, WPARAM wp)
{
  if(LOWORD(wp)==IDCANCEL){
      EndDialog( hwnd, -1);
      return TRUE;
  }
  return FALSE;
}


//winmain.cpp
#include <Windows.h>
#include "box.h"
#include "box.hpp"
#include "graphic.hpp"

BOOL CALLBACK winCtl( HWND, UINT, WPARAM, LPARAM );

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE, LPSTR, int)
{
  DialogBox( hInstance, "DLG_DATA", HWND_DESKTOP, (DLGPROC)winCtl ) ;
  return 0;
}
BOOL CALLBACK winCtl( HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
  static Graphic *aGraphic;
  switch(msg){
  case WM_INITDIALOG:
     aGraphic = new Graphic();
     return aGraphic->init(hwnd);
  case WM_PAINT: return aGraphic->paint(hwnd, 30);
  case WM_COMMAND: return aGraphic->command(hwnd, wp);
  }
  return FALSE;
}


//box.hpp
#include <Windows.>
#include "turtle.hpp"
class Box : public Turtle {
  public:
  Box(HWND h) :  Turtle(h){}
  ~Box(){}
  void paint(int);
};


//box.cpp
#include "box.hpp"
void Box::paint(int size){
 forward(size);
 turn(90);
 forward(size);
 turn(90);
 forward(size);
 turn(90);
 forward(size);
}


//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: winmain.o $(TARGET).o graphic.ro turtle.o graphic.o
	$(CXX) -o $@ $^ $(LIBS)

turtle.o: turtle.hpp
graphic.ro: $(TARGET).h
graphic.o: $(TARGET).h graphic.hpp box.hpp
$(TARGET).o: $(TARGET).h turtle.hpp
winmain.o: $(TARGET).hpp graphic.hpp

なお、ここで、 Graphic クラスは Box を描く専用のクラスになっていますが、 これは考え方としてはよくありません。 本来であれば、paint() ができるようなオブジェクトをパラメータとして持ち、 Windows のイベントに合わせて paint() を呼び出してあげるようにすべきで す。 これを実現するには paint メソッドを持つ抽象クラスを作り、 Box をその抽 象クラスのサブクラスとして実装する必要があります。 そのため、 Paintable という抽象クラスを作り、完全抽象メソッド paint を 用意します。 そして、Box は Turtle と Paintable の多重継承を行います。 そして、Graphic では Paintable のオブジェクトを受け取り、 paint を呼びます。

このようにするにあたって、毎度 Box オブジェクトを作っては壊すのは無駄 です。 そのため、グラフィックの初期化や後処理は別のメソッドにします。

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

このタートルグラフィックと再帰処理を用いてグラフィックを描きましょう。 木のような図形を考えます。 幹を一本描き、その先から短い幹が左右に出るような図形を考えます。 この図形を描く関数を 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-6

上の tree を Turtle のサブクラスの paint メソッドとして完成させなさい。

演習4-7

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

コッホ曲線

4-6. Java での再帰グラフィック

Java ではグラフィックを扱うのに AWT と Swing の二種類の方法があります。 AWT は古い実装で機種に依存していましたが、 Swing は機種に依存しないグ ラフィックインターフェースライブラリを実現しています。 したがって、ここでは Swing を使うことにします。 ここで、上に示した C++ のタートルグラフィックと同等の機能を実現するた めのサンプルを以下に示します。


import java.awt.*;
import java.awt.geom.*;
import javax.swing.*;
class Test {
    public static void main(String[] args){
	DrawFrame frame = new DrawFrame();
	frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	frame.setVisible(true);
    }
}
class DrawFrame extends JFrame {
    public DrawFrame(){
	setTitle("Turtle Graphics");
	setSize(WIDTH, HEIGHT);
	DrawPanel panel = new DrawPanel();
	Container contentPane = getContentPane();
	contentPane.add(panel);
    }
    public static final int WIDTH = 100;
    public static final int HEIGHT = 200;
}
class DrawPanel extends JPanel {
    final static int size = 30;
    public void paintComponent(Graphics g){
	super.paintComponent(g);
	Box t = new Box((Graphics2D) g);
	t.paint(size);
    }
}
class Box extends Turtle {
  Box(Graphics2D _g){
    super(_g);
  }
  void paint(int size){
	forward(size);
	turn(90);
	forward(size);
	turn(90);
	forward(size);
	turn(90);
	forward(size);	
    }
}
class Turtle {
    double x,y,angle;
    Graphics2D g;
    Turtle(Graphics2D _g){
	x=(double)DrawFrame.WIDTH/2;
	y=(double)DrawFrame.HEIGHT/2;
	angle=0;
	g=_g;
    }
    public void home(double _x, double _y){
	x=_x;
	y=_y;
	angle=0;
    }
    public void forward(double n){
	double dx=n*Math.cos(angle);
	double dy=n*Math.sin(angle);
	g.draw(new Line2D.Double(x,y,x+dx,y+dy));
	x+=dx;
	y+=dy;
    }
    public void turn(double d){
	angle+=d*Math.PI/180;
    }
    public void skip(double n){
	double dx= n*Math.cos(angle);
	double dy= n*Math.sin(angle);
	x+=dx;
	y+=dy;
    }
}

このように Swing では一番外側の Window はフレームと呼ばれ、 JFrame の サブクラスのインスタンスとして生成されます。 そして、フレームの構成要素である contentPain と呼ばれる描画領域に部 品を貼り付けることで描画します。 ここでは Graphics オブジェクト一つだけ貼り付け、 Graphic オブジェクト に Line2D オブジェクトを送ることで線を描きます。


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