情報構造論ガイダンス
前年度のプログラミング応用の次のステップとしての情報構造論として、 どういった内容を行うのかガイダンスを行う。
最初に、使用する教科書のタイトルにもある、アルゴリズムという言葉の説明として、 アルゴリズムとは、計算の方法の考え方、 そのアルゴリズムをどう表現するのかがパラダイム、 そのパラダイムに沿って実際に書いたものがプログラム。
いいプログラムとは?
次に、ある程度プログラム記述を学んできたところだし「あなたはどうプログラムを書くといいと思うか?」 を次々と聞いてみた。 学生さんから帰ってくる言葉は、 「わかりやすい、行数が短い、関数が使われている、変数名が解りやすい、バグが少ない…」 といった物がほとんど。 中には、「プログラムが再利用しやすい」といったオツな答えもあったけど、 どれも大きくまとめれば「わかりやすい」という観点。
途中で、「処理速度が速い」という答えもあったけど、「メモリの使用量が少ない」という 観点がなかなか出てこない。
処理速度という観点では、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年の復習となることから、C言語の文法のおさらい。 特に制御構文のfor,ifを中心にフローチャートとの対応をとる説明とした。
最初に、簡単なプログラムでの式の実行順序の理解の確認。
// (1) int i ; for( i = 0 ; i < 4 ; i++ ) { if ( (i % 2) == 0 ) { printf( "%d\n" , i ) ; } } // (2) int i , j ; for( i = 0 ; i < 3 ; i++ ) { for( j = 0 ; j < 2 ; j++ ) { printf( "%d\n" , i * j ) ; } }
次に、break文とcontinue文の説明を行う。
// for文 int i ; for( i = 0 ; i < N ; i++ ) { : if ( .... ) break ; if ( .... ) continue ; : } // break , continueの動き i = 0 ; // for初期化 LOOP: if ( i >= N ) goto BREAK ; // for終了判定 : if ( .... ) goto BREAK ; // break(ループ脱出) if ( .... ) goto CONTINUE ; // continue(次の繰返し) : CONTINUE: i++ ; // for繰返し変数更新 goto LOOP ; BREAK:
オブジェクト指向ガイダンス
専攻科2年のオブジェクト指向の最初の講義で、前半ガイダンスで後半、C言語の構造体などの復習。
オブジェクト指向に関連する歴史
簡単にオブジェクト指向プログラミング(Object Oriented Programming – 略してOOP)が出てくるまでの歴史的流れを最初に説明。
最初のプログラム言語のFortran(科学技術計算向け)の頃は、処理を記述するだけだったけど、 COBOL(商用計算向け)ができた頃には、データをひとまとめで扱う「構造体」(C言語ならstruct …}の考えができた。 その後のALGOLの頃には、処理をブロック化して扱うスタイル(C言語なら{ 文 … }の複文で 記述する方法ができて、処理の構造化・データの構造化ができる。これが「構造化プログラミング(structured programming)」 の始まりとなる。
Wikipediaの記事だと、構造化プログラミングは手続きの構造化のことしか書いてないなぁ… 個人的にはOOPの話の一環として話すため、データ構造化も構造化プログラミングの一部としています。
この後、様々なプログラム言語が開発され、C言語などもできてきた。 一方で、シミュレーションのプログラム開発(例simula)では、 シミュレーション対象(object)に対して、命令するスタイルの書き方が生まれ、 データに対して命令するという点で、擬人法のようなイメージで直感的にも分かりやすかった。 これがオブジェクト指向の始まりとなる。
この考え方を導入した言語の1つが Smalltalk であり、この環境では、プログラムのエディタも Smalltalk で記述したりして、オブジェクト指向がGUIのプログラムと親和性が良いことから普及が拡大する。
C言語にこのオブジェクト指向を取り入れて、C++が開発される。さらに、この文法をベースとした、 Javaなどが開発される。最近の新しい言語では、どれもオブジェクト指向の考えが使われている。
構造体の導入
C++でのオブジェクト指向は、構造体の表記がベースになっているので、まずは構造体の説明。
// 構造体の宣言 struct Person { char name[ 20 ] ; // 要素1 int phone ; // 要素2 } ; // 構造体変数の宣言 struct Person saitoh ; struct Person data[ 10 ] ; // 実際にデータを参照 構造体変数.要素名 strcpy( saitoh.name , "t-saitoh" ) ; saitoh.phone = 272925 ; for( int i = 0 ; i < 10 ; i++ ) { scanf( "%d%s" , data[ i ].name , &(data[ i ].phone) ) ; }
値渡し、ポインタ渡し、参照渡し
構造体の使い方の話では、関数との構造体のデータ渡しでポインタなどが出てくるので、 値渡し・ポインタ渡し・参照渡しの復習。(参照渡しはC++で導入された考え方)
C言語の基本は、値渡し。呼び出し側の実引数は、関数側の仮引数に値がコピーされる。 このため、呼び出し側の変数(下の例ではa)の中身は変化しない。 よって、関数の呼び出しで呼び出し側の変数が勝手に中身が変わらないので、予想外の変数の中身の変化が無く分かりやすい。
// 値渡し(call by value)の例 void foo( int x ) { x++ ; printf( "%d¥n" , x ) ; } void main() { int a = 123 ; foo( a ) ; // 124が表示 foo( a ) ; // 124が表示 }
しかし、上の例では、foo()の呼び出しで、変数aの中身が変化してくれたほうが都合が良い場合もある。 この場合は、C言語ではポインタを使って記述する。 このように、関数を呼び出して、手元の変数が変化することは、副作用と呼ばれる。 副作用の多いプログラムは、変数の値の管理がわかりにくくなるので、副作用は最小限に記述すべき。
// ポインタ渡し(call by pointer)の例 void foo( int *px ) { (*px)++ ; printf( "%d¥n" , (*px) ) ; } void main() { int a = 123 ; foo( &a ) ; // 124が表示 foo( &a ) ; // 125が表示 }
しかし、ポインタを多用すると、ポインタを動かしてトラブルも増えることから、ポインタはあまり使わない方が良い。 そこで、C++では参照型というものがでてきた。
// 参照型(call by reference)の場合 void foo( int &x ) { x++ ; printf( "%d¥n" , x ) ; } void main() { int a = 123 ; foo( a ) ; // 124が表示 foo( a ) ; // 125が表示 }
参照型は、ポインタを使っていないように見えるけれども、機械語レベルでみればポインタを使ったものと同じ。
オブジェクト指向の講義メモ
テンプレート
古い開発モデル
最近の開発モデル
自宅で監視カメラを動かしてみた
今度の週末は、家族旅行の予定。 だけど、子どもがその間、最近病院に連れて行った婆ちゃん猫を 心配している。
そこで、使っていなかったWebカメラで、急遽、監視カメラを動かしてみた。 奥さんからは、解像度低いじゃんとツッコミを受けるが、 もともとのWebカメラの性能だと思うし…
余りにも他愛もないコードだけど、 この手のお遊びプログラムは、 学生さんに「こんなこと簡単にできるよ」と見せることも多いので、 PHPコードに改良&コメント付けをしておく。

監視カメラを動かすまで
fswebcamが一番簡単に使えそうだったので…
(( fswebcamをインストール)) $ sudo aptitude install fswebcam
fswebcamの出力をそのまま画像形式で返すプログラム。
(( webcam.php )) <?php // 画像形式、キャッシュさせない header( "Content-Type: image/jpg" ) ; header( "Cache-Control: no-store, no-cache, must-revalidate" ); // 連続読み出し用にファイルロック $flock = fopen( "/var/www-support/.webcam.lock" , (file_exists( "/var/www-support/.webcam.lock" ) ? "rb+" : "wb+" )) ; if ( flock( $flock , LOCK_EX ) ) { system( "/usr/bin/fswebcam -q -d /dev/video1" ." -p YUYV" // 画像形式 ." -D 0" // delay ." -S 3" // skip frame ." -r 320x240" // 解像度 //." -r 640x480" ." --title \"tsaitoh.net's webcam\"" ." --jpeg 75 -" ) ; fclose( $flock ) ; } ?>
webcam.php の出力画像を、連続で読み出すように、 JavaScriptで設定を書き加える。 <meta http-equiv=”refresh” content=”5″> みたいな方法だと、reload の度に画像がちらつくので、JavaScriptで reload させることになった。
(( index.php )) <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" id="sixapart-standard"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta name="viewport" content="width=320,height=320,initial-scale=1"/> <title>自宅監視カメラ</title> <script type="text/javascript"> <!-- var count = 0 ; function reload() { count = (count + 1) % 100 ; // 画像を更新(キャッシュ画像を使わないように番号付) var img = document.getElementById( "image" ) ; img.src = "webcam.php?"+String(count) ; img.id = "image" ; // 動きの少ない相手用に確認表示 var txt = document.getElementById( "text" ) ; txt.innerText = "自宅監視カメラ: "+String( count ) ; } // --> </script> </head> <body> <p align="center"> <img id="image" src="webcam.php" onload="reload()" /> <div id="text"></div> </p>
ハッシュ法(2)
授業進度が予定よりも早いため前半講義で、後半はまだ提出者の少ないことから 課題の時間とした。
前回のハッシュ法では、数値(電話番号)をキーとする検索であった。 このためハッシュ関数は、数値の不規則性の部分を取り出すだけでよかった。 それでは、文字をキーとしてデータを探す場合はどのようにすればよいか?
文字の場合は、文字コードを用いてハッシュ値を作れば良い。
int hash_func( char* s ) { int sum = 0 ; for( int i = 0 ; s[i] != '¥0' ; i++ ) sum += s[i] ; return sum % HASE_SIZE ; }
しかし、この方法では、文字の並び順が違うだけ(“ABC”,”CBA”,”BBB”)に対して、 同じハッシュ値となってしまう。 英文字の場合は文字数も限られ、文字コード的には末尾4bitが変化するだけであり、 上位ビットは文字数に左右されることになる。 この場合、同じような文字数であれば、末尾4bitの不規則性も平均化されて、 近いハッシュ値になることが懸念される。
そこで、文字コード的に変化のある部分が、数値的に全体に影響を及ぼし、 最終的な値が広く分布するように以下のような計算とする場合が多い。
// 積算部のみ sum = ( sum * (小さめの素数) + s[i] ) % (大きめの素数) ;
このような考え方は、疑似乱数を生成する方式と近い部分が多い。
ハッシュ法
2分木なども終わって、検索処理のさらなる高速化として、 ハッシュ法を説明する。
オープンアドレス法
電話番号が記録されているのかどうかを探す処理を考えると、 最も単純な方法は、電話番号を配列の添字にする方法となる。
int array[ 1000000 ] ; // 局番2桁+4桁 void entry( int phone ) { array[ phone ] = phone ; } int search( int phone ) { if ( array[ phone ] == 0 ) return 0 ; else return 1 ; }
しかしこの方法では、0778621111 といった番号も考えると、 巨大なメモリを必要として、非現実的となる。 この際の解決法がハッシュ法で、データ件数が少なければ、 例えば電話番号の末尾2桁がおなじデータの発生する確率は低い。 もし、電話番号末尾2桁が同じでも、その番号の次の場所に保存するなどすればよい。
#define SIZE 100 // ハッシュ表のサイズ int hash[ SIZE ] ; // ハッシュ表 int hash_func( int phone ) { // ハッシュ関数 return phone % SIZE ; } void entry( int phone ) { int idx = hash_func( phone ) ; while( hash[ idx ] != 0 ) // データ件数100で無限ループだけど... idx = (idx + 1) % SIZE ; hash[ idx ] = phone ; } int search( int phone ) { int idx = hash_func( phone ) ; while( hash[ idx ] != 0 ) { if ( hash[ idx ] == phone ) return 1 ; idx = (idx + 1) % SIZE ; } return 0 ; }
チェイン法
オープンアドレス法では、次の空き場所を探して保存するが、 表サイズよりもデータ件数を多く保存することはできない。
表が埋まって、同じハッシュ値のデータがでてきたら、同じハッシュ値同士を リスト構造で保存する方法はチェイン法と呼ばれる。
struct List *hash[ SIZE ] ; // ポインタはNULLで全て初期化 void entry( int phone ) { int idx = hash_func( phone ) ; hash[ idx ] = cons( phone , hash[ idx ] ) ; } int search( int phone ) { int idx = hash_func( phone ) ; struct List* p ; for( p = hash[ idx ] ; p != NULL ; p = p->next ) { if ( p->data == phone ) return 1 ; } return 0 ; }
データベースの用語など
データベースの2回目ということで、データベースの形式などの説明のあと、 データベースの数学的な表記方法などを示す。
データベースの基本
データベースを利用する際、プログラマにしてみれば、SQLというデータ問い合わせ言語を 用いれば、簡単にデータを扱える。 これにより、応用プログラマにしてみれば、データをプログラムから分離ができ、 実際の内部の構造を知ることなく独立性が得られる。 また、データの一貫性を保つことは、データベースを利用しないと大変であるが、 データベースでは(a)正当性確認,(b)同時実行制御,(c)障害回復などを行ってくれるので、 一貫性のあるデータ管理が容易となる。 一般的には、データベースのACID特性として(a)atomicity原子性,(c)consistency一貫性,(i)isolation独立性,(d)dulability耐久性といった特性が求められる。
データベースに対する視点として、エンドユーザからの要求を応用プログラマが処理する場合、 データベースの一部分だけであったり、複数のデータベースを大きな単独の表で扱えると 便利であったりする。こういう応用プログラマの視点を外部スキーマと呼ぶ。 しかし実際には、複数の表からなるデータベースでは更新なども容易な(正規化された) 表で あるほうが良い。このような視点は、概念スキーマと呼ばれる。 さらにデータベースの内部では、このデータを高速に処理するための インデックスが付加されたり、実際のファイル上に記録されるデータ形式がとられる。 これらは内部スキーマと呼ばれる。 この利用者の視点に応じたスキーマ構成を、3層スキーマ・アーキテクチャと呼ぶ。
これらのデータを扱う際、木構造で表現できるようなものや、さらにそれらが複雑に からみあったネットワーク構造などは、自由にデータを表現できる一方、それらの データを扱うシステムは複雑化してしまう。 そこで、IBMのコッド博士の提唱した、データを簡単な表構造の 組み合わせで表す関係データベースは、処理の簡潔さもあって広く普及している。
データベースの表現
具体的なデータを組み合わせたデータベースは数学的に以下のように表現する。 データの集合A={s,t,u},B={p,q}とした場合、 直積A×Bとは、A×B={(x,y)|x∈A,y∈B}で表される。 例をあげると、AとBの全ての組み合わせであり、 A×B={(s,q),(t,q),(u,q),(s,q),(t,q),(u,q)} となる。 関係とは、直積の部分集合 R(A,B)⊂A×B であり、すべての組み合わせのうち、 実際に存在するものといえる。