情報構造論の最近のブログ記事

構造体と実体について

構造体と実体の違いや、Javaに慣れている学生さんに あらためて、データ構造のイメージを持って欲しいので、 以下のコードを示す。

struct Complex {
    double re ;
    double im ;
} ;
struct Complex2 {
    double* pre ;
    double* pim ;
} ;
void main() {
    // メモリ確保失敗のNULLチェックは省略
    struct Complex a ;
    struct Complex* p ;
    struct Complex2 b ;
    struct Complex2* q ;
    a.re = 1.2 ;
    a.im = 2.3 ;

    p = (struct Complex*)malloc( sizeof( struct Complex ) ) ;
    p->re = 1.2 ;
    p->im = 2.3 ;
    // in Java
    // p = new Complex ;
    // p.re = 1.2 ;
    // p.im = 2.3 ;

    b.pre = (double*)malloc( sizeof( double ) ) ;
    *(b.pre) = 1.2 ;
    b.pim = (double*)malloc( sizeof( double ) ) ;
    *(b.pim) = 2.3 ;

    q = (struct Comple2*)malloc( sizeof( struct Complex2 ) ) ;
    q->pre = (double*)malloc( sizeof( double ) ) ;
    *(q->pre) = 1.2 ;
    q->pim = (double*)malloc( sizeof( double ) ) ;
    *(q->pim) = 2.3 ;
}

上記プログラムのメモリの格納イメージ図を以下に示す。

集合の処理

リスト構造は、必要に応じてメモリを確保するデータ構造であり、データ件数に依存しないプログラム が記述できる。その応用として、集合処理を考えてみる。

2進数を用いた集合計算

リストによる集合の前に、もっと簡単な集合処理を考える。 データ件数の上限が少ない場合には、2進数を用いるとC言語のビット演算命令で 和集合、積集合が考えられるので、簡単となる。

以下のプログラムは、0〜31の数字を2進数の各ビットに対応付けし、 ba = {1,2,3} , bb = {2,4,6} , bc= {4,6,9} を要素として持つ集合で、ba ∩ bb , bb ∩ bc の計算を行う例である。

void bit_print( unsigned int x ) {
  for( int i = 0 ; i < 32 ; i++ )
    if ( (x & (1 << i)) != 0 )
      printf( "%d " , i ) ;
  printf( "\n" ) ;
}
void main() {
  unsigned int ba = (1<<1) | (1<<2) | (1<<3) ; // ba = {1,2,3}
  unsigned int bb = (1<<2) | (1<<4) | (1<<6) ; // bb = {2,4,6}
  unsigned int bc = (1<<4) | (1<<6) | (1<<9) ; // bc = {4,6,9}

  bit_print( ba & bb ) ; // ba ∩ bb
  bit_print( bb & bc ) ; // bb ∩ bc
  bit_print( ba | bc ) ; // ba ∪ bc
}

このような、2進数を用いた処理で有名なものとして、エラトステネスのふるいによる素数がある。 このアルゴリズムでは、各bitを整数に対応付けし、 素数で無いと判断した2進数の各桁に1の目印をつけていく方式である。

unsigned int prime = 0 ;
void filter() {
   for( int i = 2 ; i < 32 ; i++ ) {
      if ( (prime & (1 << i)) == 0 ) {
         for( int j = i + i ; j < 32 ; j += i )
            prime |= (1 << j) ;
      }
   }
   for( int i = 2 ; i < 32 ; i++ ) {
      if ( (prime & (1 << i)) == 0 )
         printf( "%d\n" , i ) ;
   }
}

リスト処理による積集合

前述の方法は、リストに含まれる/含まれないを、2進数の0/1で表現する方式である。 しかし、2進数であれば、unsigned int で 32要素、unsigned long long int で 64 要素が上限となってしまう。 (32bitコンピュータ,gccの場合)

しかし、リスト構造であれば、リストの要素として扱うことで、要素件数は自由に扱える。 また、今までの授業で説明してきた cons() などを使って表現すれば、 簡単なプログラムでリストの処理が記述できる。

// 先週までに説明してきたリスト構造と補助関数
struct List {
   int     data ;
   struct List* next ;
} ;
struct List* cons( int x , struct List* n ) {
   struct List* ans ;
   ans = (struct List*)malloc( sizeof( struct List ) ) ;
   if ( ans != NULL ) {
      ans->data = x ;
      ans->next = n ;
   }
   return ans ;
}
void print( struct List* p ) {
   for( ; p != NULL ; p = p->next ) {
      printf( "%d " , p->data ) ;
   }
   printf( "\n" ) ;
}
int find( struct List* p , int key ) {
   for( ; p != NULL ; p = p->next )
      if ( p->data == key )
         return 1 ;
   return 0 ;
}

例えば、積集合(a ∩ b)を求めるのであれば、リストa の各要素が、リストb の中に含まれるか find 関数でチェックし、 含まれたものだけを、ans に加えていく...という考えでプログラムを作ると以下のようになる。

struct List* set_prod( struct List* a , struct List* b ) {
   struct List* ans = NULL ;
   for( ; a != NULL ; a = a->next ) {
      if ( find( b , a->data ) )
         ans = cons( a->data , ans ) ;
   }
   return ans ;
}

void main() {
   struct List* a = cons( 1 , cons( 2 , cons( 3 , NULL ) ) ) ;
   struct List* b = cons( 2 , cons( 4 , cons( 6 , NULL ) ) ) ;
   struct List* c = cons( 4 , cons( 6 , cons( 9 , NULL ) ) ) ;

   print( set_prod( a , b ) ) ;
   print( set_prod( b , c ) ) ;
}
例題として、和集合、差集合などを考えてみよう。

リストの共有と削除の問題

リスト処理では、mallocを使うが、メモリリークをさせないためには、使用後のリストの廃棄は重要である。 リストの全要素を捨てる処理であれば、以下のようになるであろう。

void list_free( struct List* p ) {
   while( p != NULL ) {
      struct List* d = p ;
      p = p->next ;
      free( d ) ; // 順序に注意
   }
}

一方、前説明の和集合のプログラムを以下のように作った場合、list_freeの処理は問題となる。

struct List* set_union( struct List*a , struct List*b )
{
   struct List* ans = b ;
   for( ; a != NULL ; a = a->next )
      if ( !find( b , a->data ) )
         ans = cons( a->data , ans ) ;
   return ans ;
}
void main() {
   struct List*a = cons( 1 , cons( 2 , cons( 3 , NULL ) ) ) ;
   struct List*b = cons( 2 , cons( 3 , cons( 4 , NULL ) ) ) ;
   struct List*c = set_union( a , b ) ;
   // a,b,cを使った処理

   // 処理が終わったので、a,b,cを捨てる
   list_free( a ) ;
   list_free( b ) ;
   list_free( c ) ;
   // c = { 1 , (bのリスト) }
   // (b)の部分は先のlist_free(b)で解放済み
}

前回の授業で、処理速度のオーダ記法について解説し、 実際例と処理速度の問題について解説する。 後半は、再帰呼び出しの処理速度見積もりのための再帰方程式を示す。

特殊な処理時間の見積もり

前回授業の最後に検討しておくようにと伝えた、 の処理時間のオーダについて、Nが巨大となった時の、最大項を見つければ良いことから、 が、N→∞において 発散するのか収束するのかを求める方法にて説明する。

また、2つのアルゴリズムがNの増加で処理時間が変化する時の事例として、 「データ件数が10件で、最大選択ソートが10msec、クイックソートが20msec出会った場合、 データ100件では、どちらが速いか?、この結果から、 データ件数がいくつ以上であれば、どちらの方法が良いか?」 といった問題について説明する。

最大選択ソートであれば、 より、 であるとする。 一方、クイックソートは、 より、 であるとする。 より、 より、

これより、データ100件の場合には、 となる。 また、 となりクイックソートの方が速い。

さらに、 となるNを求めれば、N件以上は クイックソートが速い...といった件数を求められる。

再帰関数と再帰方程式

再帰関数は、自分自身の処理の中に「問題を小さくした」自分自身の呼び出しを含む関数。

// 階乗
int fact( int x ) {
   if ( x <= 1 )
      return 1 ;
   else
      return x * fact( x-1 ) ;
}
// ピラミッド体積
int pyra( int x ) {
   if ( x <= 1 )
      return 1 ;
   else
      return x*x + pyra( x-1 ) ;
}
// フィボナッチ数列
int fib( int x ) {
   if ( x <= 2 )
      return 1 ;
   else
      return fib( x-1 ) + fib( x-2 ) ;
}

これらの関数の結果について考えるとともに、この計算の処理時間を説明する。 最初のfact(),pyra()については、 x=1の時は、関数呼び出し,x<=1,return といった一定の処理時間を要し、 で表せる。 x>1の時は、関数呼び出し,x<=1,*,x-1,returnの処理に加え、x-1の値で再帰の処理時間がかかる。 このことから、 で表せる。 これを代入によって解けば、 で表せる。 こういった式は、再帰方程式と呼ばれ、一般式を求めることとなる。

最後に、再帰方程式の事例として、ハノイの塔の処理時間について説明し、 数学的帰納法での証明を示す。

ハノイの塔

ハノイの塔は、3本の塔にN枚のディスクを積み、ディスクの上により大きいディスクを積まずに、移動させるパズル。 ハノイの塔の移動回数を とした場合、 少ない枚数での回数の考察から、 ということが予想される。

この予想が常に正しいことを証明するために、ハノイの塔の処理を、 最も下のディスク1枚と、その上の(N-1)枚のディスクに分けて考える。 これより、


ということが言える。 ディスクが 枚で、予想が正しいと仮定すると、 枚では、




となり、 枚でも、予想が正しいことが証明された。 よって数学的帰納法により、1枚以上で予想が常に成り立つことが証明できた。

今回は、情報処理センターの機種更新に伴うパスワード再発行やら、 授業アンケートの作業に前半の時間をとられ、そのまま演習室にて授業。

2分探索法の処理時間分析

最初に、先週説明の単純サーチ と、2重ループの最大選択法 との比較をしながら、 以前のBLOG資料を使って、 2分探索法の処理時間が であることを説明する。

オーダ記法

次に、定番の説明であるけれど、 「単純サーチで、100件で1msecかかった。 データ10000件なら何秒かかる?」 同様に、「最大選択法のプログラムで、100件で10msecかかったら、10000件なら何秒?」 という質問から、オーダ記法の導入を説明する。

最後に、 , , といった、Nが大きな値になった時に、式で大きな値となる主要な部分を抜き出したもの がオーダといった説明を行う。

次回の授業での予習ネタとして、以下の式のオーダについて考えておくように...

情報構造論ガイダンス

前年度のプログラミング応用の次のステップとしての情報構造論として、 どういった内容を行うのかガイダンスを行う。

最初に、使用する教科書のタイトルにもある、アルゴリズムという言葉の説明として、 アルゴリズムとは、計算の方法の考え方、 そのアルゴリズムをどう表現するのかがパラダイム、 そのパラダイムに沿って実際に書いたものがプログラム

いいプログラムとは?

次に、ある程度プログラム記述を学んできたところだし「あなたはどうプログラムを書くといいと思うか?」 を次々と聞いてみた。 学生さんから帰ってくる言葉は、 「わかりやすい、行数が短い、関数が使われている、変数名が解りやすい、バグが少ない...」 といった物がほとんど。 中には、「プログラムが再利用しやすい」といったオツな答えもあったけど、 どれも大きくまとめれば「わかりやすい」という観点。

途中で、「処理速度が速い」という答えもあったけど、「メモリの使用量が少ない」という 観点がなかなか出てこない。

処理速度という観点では、3秒ルールなどの雑談を交えながら計算時間について説明。 また、メモリの使用量という観点では、限られた主記憶を有効に使わないと、 プログラムが動かなかったり、仮想記憶などを使うことになり、頻繁なスワップ処理で 処理速度の低下につながることを説明する。

これらの「プログラムの分かりやすさ」、「処理速度」、「メモリの使用量」は、一般的に、 どれかを優先すれば、ほかの観点が悪化してしまうトレードオフの関係にあることを説明。

処理時間を式で表現

まず、プログラムの処理時間を分析するために、簡単なプログラムがどんな式で表現できるかを示す。

// 配列(順序はデタラメ)の中の特定データを探す処理。
int data[ N ] ;
for( i = 0 ; i < N ; i++ ) {
    if ( data[ i ] == key )
        break ;
}

このようなループ処理であれば、ループ回数の平均を考えて式にすると、

で表現できることを説明。

// 最大選択法の並び替え
int data[ N ] ;
for( i = 0 ; i < N-1 ; i++ ) {
   int m = i ;
   for( j = i+1 ; j < N ; j++ ) {
      if ( data[m] > data[j] )
         m = j ;
   }
   tmp = data[ m ] ;
   data[ m ] = data[ i ] ;
   data[ i ] = tmp ;
}

このような処理であれば、ループ回数をΣなどを用いて表しながら、以下のような式で示されることを説明。

2分木による式の表現とB木

今日は、2分木を用いた式の表現とB木について説明を行う。 前回、2項演算子の話で、逆ポーランド記法などの説明をしたので、 そのプログラム例から話を行う。

逆ポーランド記法のスタック処理

逆ポーランド記法による式は、スタックを用いると簡単にその値を求める処理が記述できる。

// スタックの記述
int stack[ 10 ] ;
int *sp = stack ;
void push( int x ) {
  *sp++ = x ;
}
int pop() {
  return *--sp ;
}
// 逆ポーランド記法の式を処理する関数
int eval( char* s ) {
  for( ; *s != '\0' ; s++ ) {
    if ( *s >= '0' && *s <= '9' ) {
      push( *s - '0' ) ;
    } else {
      switch( *s ) {
      case '+' : push( pop() + pop() ) ; break ;
      case '*' : push( pop() * pop() ) ; break ;
      }
    }
  }
  return pop() ;
}
void main() {
  printf( "%d\n" , eval( "12+3*" ) ) ;
}

2分木による式の表現

これが本来説明したい2分木による式の表現。 データ構造を簡単にしたいので、 左右の枝がNULLなら、opr部に整数値、非NULLなら、opr部に演算子の文字コードとする。 この方式であれば、要素名は異なるものの、2分木のデータ宣言とまるっきり同じ。

// まずはデータ構造の宣言と、そのデータを生成する補助関数。
struct Expr {
  int opr ;
  struct Expr* left ;
  struct Expr* right ;
} ;
// 数値の場合
struct Expr* Integer( int x ) {
  struct Expr* ans ;
  ans = (struct Expr*)malloc( sizeof( struct Expr ) ) ;
  if ( ans != NULL ) {
    ans->opr = x ;
    ans->left = NULL ;
    ans->right = NULL ;
  }
  return ans ;
}
// 演算子の場合
struct Expr* Operator( int op ,
                       struct Expr* l ,
                       struct Expr* r ) {
  struct Expr* ans ;
  ans = (struct Expr*)malloc( sizeof( struct Expr ) ) ;
  if ( ans != NULL ) {
    ans->opr = op ;
    ans->left = l ;
    ans->right = r ;
  }
  return ans ;
}

データ構造を作る処理が記述できたところで、その式の表示と、その式の計算。

// 2分木による式を表示する関数。
// 演算子の優先順位を無視して、全てにカッコをつける
void print_expr( struct Expr* e ) {
  if ( e->left == NULL && e->right == NULL ) {
    printf( "%d" , e->opr ) ;
  } else {
    printf( "(" ) ;
    print_expr( e->left ) ;
    printf( "%c" , e->opr ) ;
    print_expr( e->right ) ;
    printf( ")" ) ;
  }
}
// 式の値を計算する関数
int eval_expr( struct Expr* e ) {
  if ( e->left == NULL && e->right == NULL ) {
    return e->opr ;
  } else {
    switch( e->opr ) {
    case '+' :
      return eval_expr( e->left )
             + eval_expr( e->right ) ;
    case '*' :
      return eval_expr( e->left )
             * eval_expr( e->right ) ;
    }
  }
}
// 実際に計算させてみる。
void main() {
  struct Expr* e
    = Operator( '*' ,
                Integer( 1 ) ,
                Operator( '+' ,
                          Integer( 2 ) ,
                          Integer( 3 ) ) ) ;

  printf( "%d\n" , eval_expr( e ) ) ;
}

B木

2分木で式の表現を説明したが、枝の数は左右2本と限定する必要はあるのだろうか? 2本をN本に拡張したものが、B木と呼ばれる。

// 位数2のBTree
struct BTree {
  int size ; // B木のノード内のデータ数
  int d[ 4 ] ; // 実際のデータ
  struct BTree* p[ 5 ] ; // d[i] < ● < d[i+1] のデータは、
                         // p[i+1]の枝に保存 
} ;

B木では、枝に関する条件に加え、位数Nの場合では、ノード内のデータ件数を必ず、 N <= (データ件数) <=2N を満たすようにする。

データの追加で、ノード内のデータ件数が2Nを越える場合には、 加えるデータを含め、データ順に並べた時の中央値を上位ノードに移動させ、 ノードを、左右に分割を行う。

          |
  [ 12 | 21 | 23 | 29 ]  ← 18を加えるなら、12,18,21,23,29 で、
                 21を上位に上げる。
   [ 8 |(21)| 39 | × ]
       |    |
       |    +---------------+
       |                    |
      [ 12 | 18 | × | × ]  [ 23 | 29 | × | × ]

2分木演習(2)

先週の2分木の演習に引き続き、後半を演習とした。

前半では、プログラミングへの興味を持って欲しいし、 先週のプロコンの状況を紹介した。

課題部門での作品の背景として、FireChat というソフトがあり、 香港の学生デモでも活躍していたことを話す。 また、情報統制でインターネットでどういうことが発生するかということで、 FireWallによる制限や、DNS 統制などの雑談も行った。

演算子の話

2分木の応用で、式の取扱いを来週解説する予定なので、 演算子の一般的な知識を説明。

なにげない「1+2*3」という式も、演算子には優先順位があり、 「1+(2*3)」を意味している。この優先順位を()で表さないためにはどうすればいいのか?

C言語で使われている演算子も細かく分類すると、 単項演算子、2項演算子、3項演算子(条?真値:偽値)がある。 単項演算子も単純な「-(マイナス)、!(NOT)」という前置型の他に、 「++x , x--」といった前置・後置で処理が異なるものもある。 2項演算子も、「1+2+3」は「(1+2)+3」のように処理される左結合演算子と、 「x=y=0」が「x=(y=0)」として処理される右結合演算子があることを説明。

演算子の表記法にも、「1+2*3」を、「1,2,3,*,+」のように、 演算子の前に式を置く、後置記法(逆ポーランド記法)という方式もある。 逆ポーランド記法は、日本語の文法に近いということもあげられるが、 スタックを使い、『数値はスタックに詰む、演算子がでたらスタック上位の 2つの値を取り出し、計算結果を改めてスタックに詰む。』という処理を 繰り返せば、複雑な計算式も正しく値を求めることができる....といった説明を 行う。

ちなみに、「+,1,*,2,3」のような演算子を前に置くものは、前置記法(ポーランド記法)、「1+2*3」のような書き方は、中置記法と呼ぶ。

例年より情報構造論が早く進んだので、STLとかBoostを少しだけ紹介するために、 その基本のコンテナとテンプレートを紹介する。

コンテナクラス

情報構造論の授業では、リストといった様々なアルゴリズムの説明では、 ひとまず整数でリストを...演習で名前と年齢の構造体で...といった 方式で進めてきたが、リストは次のデータのポインタと、実体のデータと考えれば、 実体データは、整数や名前と年齢といったように、必要に応じて変わってくる。

しかし、アルゴリズムが単純リスト・双方向リスト・2分木と変わっても、 実体データが違うだけで、自分自身で全てを記述することになる。 ただ、複雑なアルゴリズムになればなるほど、この作業が大変になる。

ここで、コンテナクラスという考え方が出てくる。 オブジェクト指向の派生・継承の考え方を使い、例えば単純リストを作りたい場合、 次のポインタだけの基本クラスを作り、そのリストを扱う処理をライブラリとして作っておく。 整数のリスト処理がしたければ、単純リストクラスに、整数を加えた派生クラスを作ったり、 名前と年齢を加えた派生クラスを作れば良い。

// 概念の説明用のクラス宣言であり、
// このままでは使いやすいクラスにはならない
class List {
private:
    List* next ;
public:
    List( List* n ) : next( n ) {}
} ;
class ListInt : public List {
private:
    int data ;
    :
} ;
class ListNameAge : public List {
private:
    char name[20] ;
    int  age ;
    :
} ;

ただ、上記のプログラムでは、小さなオブジェクトでも仮想関数を使ったり、 肝心のアルゴリズムに相当する部分が記述が困難になるので、実際はもう少し 面倒な宣言になってしまう。 このため、C++などではテンプレートクラスがよく利用される。

テンプレート機能

C++でのテンプレート機能とは、型の部分を「仮の型名」としてプログラムを記述し、 そのプログラムを使用する時に具体的な型名を"<>"の中に記載する方式。 コンパイラ的には、異なる型でテンプレートが利用される度に、 その型用の機械語を生成するので、コードが肥大化する可能性があるのが問題ではある。

#include <stdio.h>

template <class T>
class List {
public:
  T data ;
  List<T>* next ;
public:
  List<T>( T x , List<T>*n ) : data( x ) , next( n ) {}
} ;

int main() {
  // 整数型のリスト処理
  List<int>* p = new List<int>( 1 ,
                        new List<int>( 2 , NULL ) ) ;
  for( ; p != NULL ; p = p->next ) {
    printf( "%d\n" , p->data ) ;
  }
  // 実数型のリスト処理
  List<double>*	r = new	List<double>( 1.2 ,
                         new List<double>( 2.3 , NULL ) ) ;
  for( ; r != NULL ; r = r->next ) {
    printf( "%f\n" , r->data ) ;
  }
  return 0 ;
}

このテンプレートクラスの考え方を、究極まで活用したものとして、STL(Standard Template Library)や、 Boostといったライブラリが有名である。

// Cの場合
#include <stdio.h>
int main() {
   int* a ;
   // 配列の作成
   if ( (a = (int*)malloc( sizeof( int ) * 10 )) != NULL ) {
      // 配列の初期化
      for( int i = 0 ; i < 10 ; i++ )
         a[ i ] = i ;

      // 配列の合計
      int sum = 0 ;
      for( int i = 0 ; i < 10 ; i++ )
         sum += a[ i ] ;
      printf( "%d¥n" , sum ) ;
   }
   return 0 ;
}

// STLの場合
#include <numeric>
#include <vector>
#include <list>
using namespace std ;
int main() {
   vector<int> a ; // 配列サイズはSTL任せ...

   for( int i = 0 ; i < 10 ; i++ )
      a.push_back( i ) ; // 配列末尾にiを追加

   int sum = accumulate( a.begin() , a.end() , 0 ) ;
   printf( "%d¥n" , sum ) ;

   // 同じ処理を実数型の単純リストで...
   list<double> b ;
   for( int i = 0 ; i < 10 ; i++ )
      b.push_back( (double)i ) ;
   printf( "%f¥n" , accumulate( b.begin() , b.end() , 0.0 ) ) ;

   return 0 ;
}

例年より授業の進度が速いので、 全体の総括の前に、追加ネタを加える。

関数ポインタは、オブジェクト指向の仮想関数の実装であったり、 イベント駆動型のプログラミングで重要なテクニックなので紹介する。

関数ポインタの基礎

基本的には、関数ポインタは以下のように使用する。 以下のサンプルプログラムでは、(*f)( 2 , 3 ) にて、fに代入されている関数を、 引数(2,3) にて呼び出す。

int fadd( int x , int y ) {
    return x + y ;
}
int fmul( int x , int y ) {
    return x * y ;
}
void main() {
    // 型は2つのintを引数を持つintを返り値とする関数へのポインタ
    int (*f)( int , int ) ;

    f = fadd ; // 関数名の後ろに丸カッコがない点に注目
    printf( "%d¥n" , (*f)( 2 , 3 ) ) ; // 2+3=5
    f = fmul ;
    printf( "%d¥n" , (*f)( 2 , 3 ) ) ; // 2*3=6
}

関数ポインタは、ライブラリを利用する際に、自分なりの変更を加えたい部分があったら、 その処理を関数で記述しておき、その関数のポインタをライブラリに渡す。 ライブラリは、必要に応じて関数ポインタを使って、関数を呼び出す。 このように、ライブラリの中から自分の渡した関数を呼び出してもらうのは、 「コールバック関数」と呼ばれる。

以下に簡単な例として、配列の最大データの添字を返すプログラムの例を示す。 この方法であれば、異なるデータの型でもコールバック関数を専用に作るだけで良い。

int intcmp( int* x , int* y ) { // 整数比較関数
    if ( *x > *y )
        return 1 ;
    else if ( *x < *y )
        return -1 ;
    else
        return 0 ;
}

int vmax( void* array ,    // 配列先頭アドレス
          int size ,       // 配列データ件数
          int sizeofdata , // 1件あたりのbyte数
          int(*f)( void*,void* ) ) { // 比較関数
    int i , max = 0 ;
    for( i = 0 ; i < size ; i++ )
        if ( (*f)( array + max * sizeofdata ,
                   array + i   * sizeofdata ) < 0 )
            max = i ;
    return max ;
}

int idata[ 4 ] = { 11 , 33 , 22 , 44 } ;
char sdata[ 4 ][ 4 ] = { "ab" , "bc" , "aa" , "c" } ;

void main() {
    int m ;
    // intcmp関数を使って、idata から最大値を探す
    m = vmax( idata , 4 , sizeof(int) , intcmp ) ;
    printf( "%d" , idata[ m ] ) ;

    // strcmp関数を使って、sdata から最大値を探す
    m = vmax( sdata , 4 , sizeof(sdata[0]) , strcmp ) ;
    printf( "%s" , sdata[ m ] ) ;
}

この方式をそのまま使った便利な関数が、C言語のクイックソートの標準関数 qsort( void* , sizt_t , size_t , int(*)( void*,void* ) ) である。この関数を使えば、データ比較用のコールバック関数を書くだけで、 配列の高速ソートが可能となる。

// LLVMなどの新しいC言語では、無名関数みたいな
// block pointer ってぇのが使えるらしい。
int (^f)( int , int ) ;
f = ^( int x , int y ) {
    return x + y ;
} ;
printf( "%d¥n" , f( 2 , 3 ) ) ;
f = ^( int x , int y ) {
    return x * y ;
} ;
printf( "%d¥n" , f( 2 , 3 ) ) ;

オブジェクト指向での仮想関数

関数ポインタは、オブジェクト指向での仮想関数機能の実現に利用されている。

// 純粋仮想基底クラス
class A {
public:
    virtual void print() { printf( "A¥n" ) ; }
} ;

// 派生クラス A_int
class A_int : public A {
private:
    int data ;
public:
    A_int( int x ) { data = x ; }
    virtual void print() { printf( "A_int : %d¥n" , data ) ; }
} ;

// 派生クラス A_str
class A_str : public A {
private:
    const char* data ;
public:
    A_str( const char* x ) { data = x ; }
    virtual void print() { printf( "A_str : %s¥n" , data ) ; }
} ;

int main() {
    A      aa ;
    A_int  ai( 10 ) ;
    A_str  as( "hello" ) ;
    // array[] には、異なる型のデータ(同じ基底クラス)が入ってる。
    A* array[] = { &aa , &ai , &as } ;
    for( int i = 0 ; i < 3 ; i++ ) {
        array[ i ]-> print() ;
    }
    // 実行結果
    // A
    // A_int : 10
    // A_str : hello
    return 0 ;
}

JavaScriptと無名関数

JavaScript では、イベント駆動のプログラムを実現するために、無名関数を利用することが多い。

var func ;
func = function( x , y ) {
    return x + y ;
} ;
func( 2 , 3 ) ; // 答えは5
function foo( func ) {
    alert( func( 2 , 3 ) ) ;
}
foo( function(int x,int y) { return x * y ; } ) ;
// alertで6が表示される

2分木の課題

前回の2分木へのデータの追加処理の説明を受け、 2分木の課題時間とした。

課題テーマ

名前と誕生日といった複合データを、2分木に保存し、これらのデータに対して 処理を施すプログラムの作成。 ただし、分岐するときのキーとなるデータに対する検索処理は、 ループで記述できるのに対し、非キーに対する検索処理は再帰呼び出しを を必要とすることから、「検索はキーと非キーの2種類で検索ができること」 とした。

オプション課題として、データは単純に整数として、以下のように ASCIIアートやGUIを用いて、視覚的に2分木表示するプログラム作成も可とする。

  58
 / \
25   80

演習中は、真面目に課題に取り組んでいたようだが、未完成の人も多いようなので、 来週は前半に次の内容(2分木による演算子の処理)を説明し、後半を演習とする予定。

2017年1月

1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31        

アーカイブ

Google

このアーカイブについて

このページには、過去に書かれたブログ記事のうち情報構造論カテゴリに属しているものが含まれています。

前のカテゴリは情報工学基礎演習です。

次のカテゴリは計算機システムです。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。