双方向リスト
単純リストから双方向リストへ
ここまで説明してきた単純リストは、次のデータへのポインタを持つ。ここで、1つ後ろのデータ(N番目からN+1番目)をアクセスするのは簡単だけど、1つ前のデータ(N-1番目)を参照しようと思ったら、先頭から(N-1)番目を辿るしかない。でも、これは O(N) の処理であり時間がかかる処理。
ではどうすればよいのか?
この場合、一つ前のデータの場所を覚えているポインタがあれば良い。
// 双方向リストの宣言 struct BD_List { struct BD_List* prev ; // 1つ前のデータへのポインタ int data ; struct BD_List* next ; // 次のデータへのポインタ } ;
このデータ構造は、双方向リスト(bi-directional list)と呼ばれる。では、簡単なプログラムを書いてみよう。双方向リストのデータを簡単に生成するための補助関数から書いてみる。
// リスト生成補助関数 struct BD_List* bd_cons( struct BD_List* p , int d , struct BD_List* n ) { struct BD_List* ans ; ans = (struct BD_List*)malloc( sizeof( struct BD_List ) ) ; if ( ans != NULL ) { ans->prev = p ; ans->data = d ; ans->next = n ; } return ans ; } void main() { struct BD_List* top ; struct BD_List* p ; // 順方向のポインタでリストを生成 top = bd_cons( NULL , 1 , bd_cons( NULL , 2 , bd_cons( NULL , 3 , NULL ) ) ) ; // 逆方向のポインタを埋める top->next->prev = top ; top->next->next->prev = top->gt;next ; // リストを辿る処理 for( p = top ; p->next != NULL ; p = p->next ) printf( "%d\n" , p->data ) ; for( ; p->prev != NULL ; p = p->prev ) printf( "%d\n" , p->data ) ; }
双方向リストの関数作成
以上の説明で、双方向の基礎的なプログラムの意味が分かった所で、練習問題。
先のプログラムでは、1,2,3 を要素とするリストを、ナマで記述していた。実際には、どんなデータがくるか分からないし、指定したポインタ p の後ろに、データを1件挿入する処理 bd_insert( p , 値 ) , また、p の後ろのデータを消す処理 bd_delete( p ) を書いてみよう。
// 双方向リストの指定場所 p の後ろに、値 d を要素とするデータを挿入せよ。 void bd_insert( struct BD_List* p , int d ) { struct BD_List*n = bd_cons( p , d , p->next ) ; if ( n != NULL ) { p->next->prev = n ; p->next = n ; } } // 双方向リストの指定場所 p の後ろのデータを消す処理は? void bd_delete( struct BD_List* p ) { struct BD_List* d = p->next ; d->next->prev = p ; p->next = d->next ; free( d ) ; } // この手のリスト処理のプログラムでは、命令の順序が重要となる。 // コツとしては、修正したい箇所の遠くの部分を操作する処理から // 書いていくと間違いが少ない。
番兵と双方向循環リスト
前述の bd_insert() だが、データの先頭にデータを挿入したい場合は、どう呼び出せば良いだろうか?
bd_insert() で、末尾にデータを挿入する処理は、正しく動くだろうか?
同じく、bd_delete() だが、データの先頭のデータを消したい場合は、どう呼び出せば良いだろうか?
また、データを消す場合、最後の1件のデータが消えて、データが0件になる場合、bd_delete() は正しく動くだろうか?
こういった問題が発生した場合、データが先頭・末尾で思ったように動かない時、0件になる場合に動かない時、特別処理でプログラムを書くことは、プログラムを読みづらくしてしまう。そこで、一般的には 循環リストの時にも紹介したが、番兵(Sentinel) を置くことが多い。
しかし、先頭用の番兵、末尾用の番兵を2つ用意するぐらいなら、循環リストにした方が便利となる。このような双方向リストでの循環した構造は、双方向循環リスト(bi-directional ring list)と呼ばれる。
この双方向循環リストを使うと、(1)先頭にデータを挿入(unshift)、(2)先頭のデータを取り出す(shift)、(3)末尾にデータを追加(push)、(4)末尾のデータを取り出す(pop)、といった処理が簡単に記述できる。この4つの処理を使うと、単純リスト構造で説明した、待ち行列(queue)やスタック(stack) が実現できる。この特徴を持つデータ構造は、先頭・末尾の両端を持つ待ち行列ということで、deque (double ended queue) とも呼ばれる。
理解確認
- 双方向リストとはどのようなデータ構造か図を示しながら説明せよ。
- 双方向リストの利点と欠点はなにか?
- 番兵を用いる利点を説明せよ。
- deque の機能と、それを実現するためのデータをリストを用いて実装するには、どうするか?
- 双方向リストが使われる処理の例としてどのようなものがあるか?
2018データベース・ガイダンス
シラバス:2018年度データベースシラバス
インターネットの情報量
インターネット上の情報量の話として、2010年度に281EB(エクサバイト)=281✕1018B(参考:kMGTPEZY)で、今日改めて探したら、2013年度で、1.2 ZB(ゼタバイト)=1.2✕1021B という情報があった。ムーアの法則2年で2倍の概算にも、それなりに近い。 では、今年2018年であれば、どのくらいであろうか?
そして、これらの情報をGoogleなどで探す場合、すぐにそれなりに情報を みつけてくれる。これらは、どの様に実装されているのか?
Webシステムとデータベース
まず、指定したキーワードの情報を見つけてくれるものとして、 検索システムがあるが、このデータベースはどのようにできているのか?
Web創成期の頃であれば、Yahooがディレクトリ型の検索システムを構築 してくれている。(ページ作者がキーワードとURLを登録する方式) しかし、ディレクトリ型では、自分が考えたキーワードではページが 見つからないことが多い。
そこで、GoogleはWebロボット(クローラー)による検索システムを構築した。 Webロボットは、定期的に登録されているURLをアクセスし、 そのページ内の単語を分割しURLと共にデータベースに追加する。 さらに、ページ内にURLが含まれていると、そのURLの先で、 同様の処理を再帰的に繰り返す。
これにより、巨大なデータベースが構築されているが、これを少ない コンピュータで実現すると、処理速度が足りず、3秒ルール/5秒ルール (Web利用者は次のページ表示が3秒を越えると、次に閲覧してくれない) これを処理するには負荷分散が重要となる。
一般的に、Webシステムを構築する場合には、 1段:Webサーバ、2段:動的ページ言語、3段:データベースとなる場合も 多い。この場合、OS=Linux,Web=Apache,DB=MySQL,動的ページ生成言語=PHPの組合せで、 LAMP構成とする場合も多い。
一方で、大量のデータを処理するDBでは、フロントエンド,スレーブDB,マスタDBのWebシステムの3段スキーマ構成となることも多い。
データベースシステム
データベースには、ファイル内のデータを扱うためのライブラリの、 BerkleyDBといった場合もあるが、複雑なデータの問い合わせを実現する 場合には、リレーショナル・データベース(RDB)を用いる。 RDBでは、データをすべて表形式であらわし、SQLというデータベース 問い合わせ言語でデータを扱う。 また、問い合わせは、ネットワーク越しに実現可能であり、こういった RDBで有名なものとして、Oracle , MySQL , PostgreSQL などがある。 単一コンピュータ内でのデータベースには、SQLite などがある。
データベースシステムと呼ばれるには、ACID特性が重要となる。
- A: 原子性 (Atomicity) – 処理はすべて実行するか / しない のどちらか。
- C: 一貫性 (Consistency) – 整合性とも呼ばれ、与えられたデータのルールを常に満たすこと。
- I: 独立性 (Isolation) – 処理順序が違っても結果が変わらない。それぞれの処理が独立している。
- D: 永続性 (Durability) – データが失われることがない(故障でデータが無くならないとか)
しかし、RDBでは複雑なデータの問い合わせはできるが、 大量のデータ処理のシステムでは、フロントエンドDB,スレーブDB,マスタDB の同期が問題となる。この複雑さへの対応として、最近は NoSQL が 注目されている。
データベースが無かったら
これらのデータベースが無かったら、どのようなプログラムを作る 必要があるのか?
情報構造論ではC言語でデータベースっぽいことをしていたが、 大量のデータを永続的に扱うのであれば、ファイルへのデータの読み書き 修正ができるプログラムが必要となる。
こういったデータをファイルで扱う場合には、1件のデータ長が途中で 変化すると、N番目のデータは何処?といった現象が発生する。 このため、簡単なデータベースを自力で書くには、1件あたりのデータ量を 固定し、lseek() , fwrite() , fread() などの 関数でランダムアクセスのプログラムを書く必要がある。
また、データの読み書きが複数同時発生する場合には、排他処理も 重要となる。例えば、銀行での預け金10万の時、3万入金と、2万引落としが 同時に発生したらどうなるか? 最悪なケースでは、 (1)入金処理で、残金10万を読み出し、 (2)引落し処理で、残金10万を読み出し、 (3)入金処理で10万に+3万で、13万円を書き込み、 (4)引落し処理で、残金10万-2万で、8万円を書き込み。 で、本来なら11万になるべき結果が、8万になるかもしれない。
さらに、コンピュータといってもハードディスクの故障などは発生する。 障害が発生してもデータの一貫性を保つためには、バックアップや 障害対応が重要となる。
ポインタと番地の理解
リスト構造とかのプログラミングでは、ポインタが使われるが、番地とポインタをうまく理解していないと、どのような処理をしているのか理解しづらいはず。
今回の補講では、ポインタを理解してもらう。
以下では、ポインタを使った処理(前半)を見て、ポインタの動きを考える。理解できていなければ、同じ処理をポインタ無し、番地を意識させる memory[] 配列による記述(後半)で、動きを追って2つのプログラムが同じ挙動を表している…という説明の繰り返しで、ポインタの理解を図る。
単純な変数の加算
プログラムで、「 c = a + b ; 」と書いてあったら、メモリの「変数aの番地の中身」と「変数bの番地の中身」を加えて、結果を「変数cの番地」に保存する。
// 変数 a と 変数b の加算 int a = 11 ; int b = 22 ; int c ; c = a + b ; // 同じ処理をメモリの番地のイメージを示す。 int memory[ 1000 ] = { 0 , 0 , 0 , 11 , 22 , 0 , 0 } ; #define ADDR_A 3 #define ADDR_B 4 #define ADDR_C 5 memory[ ADDR_C ] = memory[ ADDR_A ] + memory[ ADDR_B ] ;
ポインタのイメージ
// ポインタの処理 int a = 11 ; int b = 22 ; int*p ; p = &a ; (*p)++ ; p = &b ; (*p)++ ; // 同じ処理をメモリ番地のイメージで int memory[ 1000 ] = { 0 , 0 , 0 , 11 , 22 , 0 , 0 } ; #define ADDR_A 3 #define ADDR_B 4 int p ; // int *p ; p = ADDR_A ; // p = &a ; memory[ p ]++ ; // (*p)++ ; p = ADDR_B ; // p = &b ; memory[ p ]++ ; // (*p)++ ;
ポインタ渡し
// ポインタ引数による値の交換 void swap( int*x , int*y ) { int tmp = *x ; *x = *y ; *y = tmp ; } void main() { int a = 11 ; int b = 22 ; swap( &a , &b ) ; } // 同じ処理をメモリ番地のイメージで。 int memory[ 1000 ] = { 0 , 0 , 0 , 11 , 22 , 0 , 0 } ; #define ADDR_A 3 #define ADDR_B 4 void swap( int x , int y ) { // void swap( int*x , int*y ) { int tmp = memory[ x ] ; // int tmp = (*x) ; memory[ x ] = memory[ y ] ; // (*x) = (*y) ; memory[ y ] = tmp ; // (*y) = tmp ; } // } void main() { swap( ADDR_A , ADDR_B ) ; // swap( &a , &b ) ; }
上記のポインタの説明では、番地をintで表現しているから、型の概念が曖昧になりそう。
本当は、以下のように pointer 型を使って説明したいけど、補講の学生に typedef は、混乱の元だろうな。ひとまず、ここまでのポインタのイメージを再学習するネタを見てもらってからなら、typedef int pointer してもいいかな?typedef int pointer ; void swap( pointer x , pointer y ) { int tmp = memory[ x ] ; memory[ x ] = memory[ y ] ; memory[ y ] = tmp ; }プログラミングでは、型の理解が重要。たとえ、Python,Ruby といった型宣言の無い言語でも、どんなデータなのかを意識して書く必要がある。
理解の確認
// 以下のプログラムの実行結果は? void foo( int x ) { x++ ; } void bar( int*p ) { (*p)++ ; } void main() { int a = 111 ; foo( a ) ; // a の中身は? bar( &a ) ; // a の中身は? } // 同じ処理を typedef int pointer ; int memory[ 1000 ] = { 0 , 0 , 0 , 111 , 0 , 0 } ; #define ADDR_A 3 void foo( int x ) { _______________________ ; } void bar( pointer p ) { _______________________ ; } void main() { foo( ________________ ) ; // memory[ ADDR_A ] の中身は? bar( ________________ ) ; // memory[ ADDR_A ] の中身は? }
ポインタと配列
// ポインタの移動 int sum = 0 ; int array[ 3 ] = { 11 , 22 , 33 } ; int*p ; p = array ; sum += *p ; p++ ; sum += *p ; p++ ; sum += *p ; // 同じ処理をメモリ番地のイメージで typedef int pointer ; int memory[ 1000 ] = { 0 , 0 , 0 , 11 , 22 , 33 , 0 , 0 } ; #define ADDR_SUM 2 #define ARRAY 3 pointer p ; // int*p ; p = ARRAY ; // p = array ; memory[ ADDR_SUM ] += memory[ p ] ; // sum += (*p) ; p++ ; // p++ ; memory[ ADDR_SUM ] += memory[ p ] ; // sum += (*p) ; p++ ; // p++ ; memory[ ADDR_SUM ] += memory[ p ] ; // sum += (*p) ;
理解の確認
整数配列にデータが並んでいる。数字は0以上の数字で、データ列の後ろには必ず0が入っているものとする。配列の先頭から0を見つけるまでの値を合計する関数を作れ。
int sum( int*p ) { s = 0 ; for( __________ ; __________ ; __________ ) { ____________________ ; } return s ; } int array_a[ 4 ] = { 11 , 22 , 33 , 0 } ; int array_b[ 5 ] = { 4 , 3 , 2 , 1 , 0 } ; void main() { printf( "%d\n" , sum( array_a ) ) ; // 66 を表示 printf( "%d\n" , sum( brray_b ) ) ; // 10 を表示 printf( "%d\n" , sum( array_a + 1 ) ) ; // 何が表示される? }
リスト構造
では、最後のシメということで、リスト構造でのポインタのイメージの確認。
// リストを次々たどる処理 struct List { int data ; struct List* next ; } ; struct List* cons( int x , struct List* p ) { struct List* ans = (struct List*)malloc( sizeof( struct List ) ) ; if ( ans != NULL ) { ans->data = x ; ans->next = p ; } return ans ; } void main() { struct List* top = cons( 11 , cons( 22 , cons( 33 , NULL ) ) ) ; struct List* p ; for( p = top ; p != NULL ; p = p->next ) { printf( "%d¥n" , p->data ) ; } } // メモリのイメージで typedef int pointer ; int memory[ 1000 ] = { 0 , 0 , 22 , 6 , 11 , 2 , 33 , 0 , 0 , 0 , } ; #define OFFSET_DATA 0 #define OFFSET_NEXT 1 void main() { pointer p ; for( p = 4 ; p != 0 ; p = memory[ p + OFFSET_NEXT ] ) { printf( "%d¥n" , memory[ p + OFFSET_DATA ] ) ; } }
授業アンケート(前期修了)
2018年度前期修了科目の授業アンケート。
オブジェクト指向プログラミング(専攻科)
専攻科オブジェクト指向は、80.5 で前年度ともあまり変わらないポイント。
板書については、Webに授業資料を公開しながらの授業であったため、評価が高かった。興味と関心についても評価が高い一方で、内容理解についてはポイントも低く、レポートなどを見ていると、プログラミングが苦手な人の参加も多かったように思う。もう少し理解に割く時間を増やしても良かったかもしれない。準備についての評価が低い。Web資料も準備しながらなので、準備はもう少し高いことを期待していた。
情報制御基礎(3年学際科目)
今年初めて実施の学際科目で、他学科の学生も受講する内容。このため、プログラミング経験の浅いM,C,Bの学生には、厳しい内容だった。しかし、情報制御という授業でプログラミング無しで、簡単なデータ処理を行うために2重forループも分からないのでは、授業で教える内容が無い。最初のガイダンスで、選択科目だしプログラミングが苦手なら受講を避けるようにすべきだったと思う。
情報構造論の追試のための解答例
情報構造論の成績不振者のための追試を、8/9(木) 15:30 より 4EI 教室で実施します。
インターンシップなどで当日参加困難な場合は、別日に実施しますので、連絡してください。
テスト問題の解答および解説を、以下に示します。
追試では、問題を変更して出題しますので、決して文字の暗記で臨まないこと。
局所変数配列をreturn
情報構造論の前期期末試験で、局所変数返しの解答が多かったので、メモ。
JavaScript でプログラムを書いていると、動くネタだけど、C言語では初心者がよく間違って書いてしまう定番であり、それなりに動いたりするから、誤解が増えてしまう可能性もある。
スタック変数返し
char* foo() { char str[ 20 ] ; strcpy( str , "Hello World" ) ; return str ; } char* bar() { char baz[ 20 ] ; memset( baz , 'A' , sizeof( baz ) ) ; return NULL ; } void main() { char* ans = foo() ; printf( "%s¥n" , ans ) ; // Hello World // 一見正しく動いているように思うかも。 bar() ; // 局所変数を触るだけの処理 printf( "%s¥n" , ans ) ; // AAAAAAAAAAA // ans の中身が壊れている。 }
静的局所変数返し
上記のスタック変数を返す問題点を示すと、じゃあ静的局所変数でいいじゃん….という話がでてくる。
char* foo() { static char str[20] ; strcpy( str , "Hello World" ) ; return str ; }
この方式なら、スタック領域ではなく静的変数領域なので、関数呼び出しで破壊されることはない。
しかし、この方式は「スレッドセーフ」ではない。foo() の処理後に、スレッドを起動した場合、スレッドではメモリ空間が共有されるため、foo() を保持していて、別スレッドがそのメモリを書き換えると、他方は中身が変わってしまう。
(参考「スレッドセーフではないCライブラリ関数」)
実数と整数の変換
情報制御基礎で出題した問題で、5点の移動平均の処理の一部に、以下のコードがあった場合の間違い説明の問題。
int avg() { int s = .... ; return (1/5) * s ; }
の間違いを修正せよ….の答えだけど、return 0.2 * s ; とか return s/5 ; が答えだけど、小数点以下が切り捨てになるのか、四捨五入になるのか気になってきた。
for( double x = 0.0 ; x <= 2.0 ; x += 0.1 ) printf( "%ld %d\n" , x , (int)x ) ;
で確認してみたら、(int)実数は、小数点以下切り捨てだな。となると、四捨五入したいなら、return (int)( 0.2 * s + 0.5 ) ; とか、return (s+2)/5 ; とか書いてほしい…とか微妙な話になるな。
採点は、切り捨て・四捨五入の誤差については問わないで、0.2*s , s/5 を◯でいこう。
情報制御基礎の超優秀レポート
3年の学際科目で、5学科入り乱れての参加のある「情報制御基礎」の科目。幅広い知識を習得してもらう目的ということで、簡単なレポートでも要件を満たしていればA評価、B評価としているが、電気電子の学生さんからオリジナリティあふれる取り組みや、きちんとした考察のレポートがいくつか出てきている。この学生のレポートを100%としたら、よくわからないけど頑張ってついてきている学生さんが厳しくなっちゃうなぁ。
ひとまず嬉しいレポートもらったので、メモメモ。
集合とリスト処理
リスト構造は、必要に応じてメモリを確保するデータ構造であり、データ件数に依存しないプログラム が記述できる。その応用として、集合処理を考えてみる。
2進数を用いた集合計算
リストによる集合の前に、もっと簡単な集合処理を考える。データ件数の上限が少ない場合には、「2進数の列」の各ビットを集合の各要素に対応づけし、要素の有無を0/1で表現する。この方法を用いると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() { // 98,7654,3210 // ba = {1,2,3} = 00,0000,1110 unsigned int ba = (1<<1) | (1<<2) | (1<<3) ; // bb = {2,4,6} = 00,0101,0100 unsigned int bb = (1<<2) | (1<<4) | (1<<6) ; // bc = {4,6,9} = 10,0101,0000 unsigned int bc = (1<<4) | (1<<6) | (1<<9) ; bit_print( ba & bb ) ; // ba ∩ bb = {2} bit_print( bb & bc ) ; // bb ∩ bc = {4,6} bit_print( ba | bc ) ; // ba ∪ bc = {1,2,3,4,6,9} }
このような、2進数を用いた処理で有名なものとして、エラトステネスのふるいによる素数計算がある。このアルゴリズムでは、各bitを整数に対応付けし、素数で無いと判断した2進数の各桁に1の目印をつけていく方式である。
unsigned int prime = 0 ; void filter() { for( int i = 2 ; i < 32 ; i++ ) { if ( (prime & (1 << i)) == 0 ) { // iの倍数には、非素数の目印をつける for( int j = 2*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 ) { // aの要素がbにも含まれていたら、ansに加える 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 ) ; // 順序に注意 } }
一方、前説明の和集合(a ∪ b)のプログラムを以下のように作った場合、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)で解放済み }
このような、リストb,リストcで共有されている部分があると、データの廃棄処理をどのように記述すべきなのか、問題となる。
これらの解決方法としては、(1) set_union() の最初で、ans=b となっている部分を別にコピーしておく、(2) 参照カウンタ法を用いる、(3) ガベージコレクタのある言語を用いる…などがある。(2),(3)は後期授業で改めて解説を行う。
struct List* copy( struct List*p ) { struct List*ans = NULL ; for( ; p != NULL ; p = p->next ) ans = cons( p->data , ans ) ; return ans ; } struct List* set_union( struct List*a, struct List* b ) { struct List* ans = copy( b ) ; : }
理解確認
- 2進数を用いた集合処理は、どのように行うか?
- リスト構造を用いた集合処理は、どのように行うか?
- 積集合(A ∩ B)、和集合(A ∪ B)、差集合(A – B) の処理を記述せよ。