ホーム » スタッフ » 斉藤徹 » 講義録 » 情報構造論 (ページ 24)

情報構造論」カテゴリーアーカイブ

2025年6月
1234567
891011121314
15161718192021
22232425262728
2930  

検索・リンク

B木の構造とデータベース

2分探索木の考え方を拡張したもので、B木がある。

B木の構造

B木では、データの増減で木の組換えの発生頻度が高い。そこで、1つのノード内に複数のデータを一定数覚える方法をとる。B木では、位数=Nに対し、最大2N個のデータd0..d2N-1と、2N+1本のポインタp0..p2Nから構成される。piの先には、di-1<x<di を満たすデータが入った B木のノードを配置する。ただし、データの充填率を下げないようにするため、データは最小でもN個、最大で2N個を保存する。

B木からデータの検索

データを探す場合は、ノード内のデータ diの中から探し、見つからない場合は、ポインタの先のデータを探す。位数がある程度大きい場合、ノード内の検索は2分探索法が使用できる。また、1つのノード内の検索が終われば、探索するデータ件数は、1/N〜1/2Nとなることから、指数的に対象件数が減っていく。よって、検索時間のオーダは、O(N) O(logN) 資料修正注意となる。 (さらに…)

式の構文木と評価

2分木の応用ということで、2項演算子の構文木と、意思決定木の説明を行う。また、これらを用いてコンパイラを作るための知識を解説する。

2項演算と構文木

演算子を含む式が与えられたとして、それを保存する場合、演算式の2分木で扱うと都合が良い。

   +
  / \
 1   *
    / \
   2   3

演算子の木のノードで、末端は数値であることに注目し、右枝・左枝がNULLなら数値(data部にはその数値)、それ以外は演算子(data部には演算子の文字コード)として扱うとして…
(さらに…)

意志決定木と式を表す木

意志決定木

2分木の応用で最も単純な物として、意志決定木がある。

yes/no の答えを回答すると、最終的に「あなたの性格は✕✕です」と表示するようなヤツ。

struct Tree {
    char*        q_a ;
    struct Tree* yes ;
    struct Tree* no ;
} ;
               top
                  \
            あなたは勉強が好き?
              /yes     \no
    ものづくりは好き?    人と話すのが好き?
     /yes    \no      /yes    \no
技術者タイプ    ◯◯◯  営業タイプ  ◯◯◯

(さらに…)

2分探索木の演習

先週までで、2分探索木の説明を終えたので、今日は演習を行う。

課題テーマ(基本)

2分探索木を使ったプログラムの作成。データ構造は、以下の中から出席番号にて選ぶ。(出席番号%3)

  1. 名前と生年月日(年月日は別要素が望ましい)
  2. 名前と電話番号
  3. 学科・学年・出席番号・名前の名簿

上記のデータ構造にて、データを入力し「保存」、その後「検索」し目的のデータを表示する機能を実装せよ。

定番ではあるが、(a)プログラム説明、(b)プログラムリスト、(c)動作検証、(d)動作考察の観点で評価を行う。

課題テーマ(オプション)

上記の基本テーマは簡単であるため、オプションテーマを以下に示す。基本テーマの代わりにオプションテーマにてレポートを提出せよ。

数値をデータとする2分木を、その構造がイメージできるようにアスキーアート的に、あるいはグラフィック的に表示するプログラムを作成せよ。

(例1)            (例2)  {[29] 51 [(60) 75 (80)]}
  51
 / \          (例3)          51
29   75                      |
   / \       +------+-------+
    60      80            |              |
                         29              75
                                         |
                                     +---+---+
                                     |       |
                                    60       80

(さらに…)

2分探索木への追加

2分木へのデータの追記

前回の授業で、2分探索木のデータに対する処理を説明したので、今回は木にデータを追加する処理。

struct Tree {
    int data ;
    struct Tree* left ;
    struct Tree* right ;
} ;
struct Tree* tcons( int x, struct Tree*L, struct Tree*R )
{
    struct Tree* ans ;
    ans = (struct Tree*)malloc( sizeof( struct Tree ) ) ;
    if ( ans != NULL ) {
        ans->data  = x ;
        ans->left  = L ;
        ans->right = R ;
    }
    return ans ;
}
struct Tree* top = NULL ;
void insert( int x )
{
    struct Tree** tail = &top ; // 書換えるべき末端ポインタへのポインタ
    while( (*tail) != NULL ) {
        if ( (*tail)->data == x )
            break ; // 同じデータは重複するので、追加処理は行わない
        else if ( (*tail)->data > x )
            tail = &( (*tail)->left ) ; // 左の枝を降りていく
        else
            tail = &( (*tail)->right ) ; // 右の枝を降りていく
    }
    if ( (*tail) == NULL )
        (*tail) = tcons( x , NULL , NULL ) ;
}

単純リストの時は、2重ポインタの混ざった式の部分的に、型を意識する説明をした。今回は、”tail = &( (*tail)->left )“の赤のカッコが省略されたら、青のカッコが省略されたら、どちらが文法エラーかを「演算子の優先順位」の話を交えながら説明する。 (さらに…)

2分探索木

2分木(2分探索木)

struct Tree {
    int data ;
    struct Tree* left ;
    struct Tree* right ;
} ;
struct Tree* tcons( int x, struct Tree*L, struct Tree*R )
{
    struct Tree* ans ;
    ans = (struct Tree*)malloc( sizeof( struct Tree ) ) ;
    if ( ans != NULL ) {
        ans->data  = x ;
        ans->left  = L ;
        ans->right = R ;
    }
    return ans ;
}
void main() {
    struct Tree* top 
        = tcons( 52 ,
                 tcons( 24 ,
                        tcons( 10 , NULL , NULL ) ,
                        tcons( 35 , NULL , NULL ) ) ,
                 tcons( 73 ,
                        tcons( 60 , NULL , NULL ) ,
                        tcons( 95 , NULL , NULL ) ) ) ;
}

出来上がった木構造のイメージ

   top
     \
      52
     / \
    /   \
   24     73
  / \   / \
 10   35 60   95

(さらに…)

リストの利点/欠点と双方向リスト

リストを使った集合演算のように、データを連ねたリストは、単純リストとか線形リストと呼ばれる。特徴はデータ数に応じてメモリを確保する点や、途中へのデータの挿入削除が得意な点があげられる。一方で、配列は想定最大データ件数で宣言してしまうと、実際のデータ数が少ない場合、メモリの無駄も発生する。しかし、想定件数と実データ件数がそれなりに一致していれば、無駄も必要最小限となる。リスト構造では、次のデータへのポインタを必要とすることから、常にポインタ分のメモリは、データにのみ注目すれば無駄となる。

シーケンシャルアクセス・ランダムアクセス

もう1つの欠点がシーケンシャルアクセスとなる。テープ上に記録された情報を読む場合、後ろのデータを読むには途中データを読み飛ばす必要があり、データ件数に比例したアクセス時間を要する。このような N番目 データ参照に、O(N)の時間を要するものは、シーケンシャルアクセスと呼ばれる。

一方、配列はどの場所であれ、一定時間でデータの参照が可能であり、これは ランダムアクセスと呼ばれる。N番目のアクセス時間がO(1)を要する。

このため、プログラム・エディタの文字データの管理などに単純リストを用いた場合、1つ前の行に移動するには、先頭から編集行までの移動で O(N) の時間がかかり、大量の行数の編集では、使いものにならない。ここで、シーケンシャルアクセスでも1つ前にもどるだけでも処理時間を改善してみよう。 (さらに…)

構造体と実体について

構造体と実体の違いや、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分木に対する再帰などを交えたプログラムの説明をしたので、 今週は木の生成について、AVL木などを交えて説明。 後半は、情報処理センターで演習。

#include <stdio.h>
#include <stdlib.h>
// 2分木の宣言
struct Tree {
   int data ;
   struct Tree* left ;
   struct Tree* right ;
} ;
// 木の根
struct Tree* top = NULL ;
// 木のノードを構築する補助関数
struct Tree* tcons( int x , struct Tree* l , struct Tree* r ) {
   struct Tree* n ;
   n = (struct Tree*)malloc( sizeof( struct Tree ) ) ;
   if ( n != NULL ) {
      n->data = x ;
      n->left = l ;
      n->right = r ;
   }
   return n ;
}
// 木を表示する補助関数。枝の左右がわかる様に...
void print( struct Tree* p ) {
   if ( p == NULL ) {
      printf( "x" ) ;
   } else {
      printf( "(" ) ;
      print( p->left ) ;
      printf( " %d " , p->data ) ;
      print( p->right ) ;
   printf( ")" ) ;
   }
}
// 木に1件のデータを追加する補助関数
void entry( int x ) {
   struct Tree** ppt = &top ;

   while( (*ppt) != NULL ) {
      if ( (*ppt)->data == x )
         break ;
      else if ( (*ppt)->data > x )
         ppt = &( (*ppt)->left ) ;
      else
         ppt = &( (*ppt)->right ) ;
   }
   if ( *ppt == NULL )
      (*ppt) = tcons( x , NULL , NULL ) ;
   }
int main() {
   entry( 51 ) ;
   entry( 26 ) ;
   entry( 76 ) ;
   entry( 60 ) ;
   print( top ) ;
   // ((x 26 x) 51 ((x 60 x) 76 x))

   top = NULL ;
   entry( 26 ) ;
   entry( 51 ) ;
   entry( 60 ) ;
   entry( 76 ) ;
   print( top ) ;
   // (x 26 (x 51 (x 60 (x 76 x))))
}

再帰関数の処理と再帰方程式

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

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

前回授業の最後に検討しておくようにと伝えた、 の処理時間のオーダについて、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枚以上で予想が常に成り立つことが証明できた。

システム

最新の投稿(電子情報)

アーカイブ

カテゴリー