双方向リスト
単純リストから双方向リストへ
ここまで説明してきた単純リストは、次のデータへのポインタを持つ。ここで、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ループも分からないのでは、授業で教える内容が無い。最初のガイダンスで、選択科目だしプログラミングが苦手なら受講を避けるようにすべきだったと思う。
学科サイトを安全に保護されたページに
最近のWebページでの SSL 接続必須の状況の中、メジャーなブラウザは https 接続が不完全だと、ページURL横の🔒マークが表示されなくなってきている。
その中で、学科のページも https の SSL 鍵なども登録していたけど、久々にページを確認したら🔒マークが消えている。原因は、サイドメニューにプロコン公式ページへのリンクのためのバナー画像を http で表示させていた。かといって、プロトコル欄を https に変更したら、プロコンのサイトが SSL 鍵が公式でなかった。
プロコンのページは電子情報では興味を持つ人も多いので、バナーアイコンをコピーしておくことにした。
IchigoJamでの公開講座
電子情報工学科のOBの jig.jp 福野氏に講師として協力してもらい、中学生向けの公開講座が行われました。
高専ワイヤレスIoT技術実証コンテスト
高専フォーラムにて紹介のあった、高専ワイヤレスIoT技術実証コンテスト。
今年は、「5G活用部門」及び「ワイヤレスIoT活用部門」にて実施だそうです。
高専フォーラム2018参加
名古屋大学で8/20,21,22と開催された高専フォーラムにて、電子情報の西先生、村田先生、川上先生が発表されました。
これ以外にも、様々なテーマでのセッションが開催され、高専特有のテーマを聴講したりしました。
PCN武生のお手伝い
今日はPCN武生の最初の講座。
小学校の一年生で、簡単な命令と早押しゲームの体験でした。
緊急連絡システムのOS更新など
緊急連絡システムのOSが、Ubuntu/trusty でそろそろ、サポート期間に近づいているので、OS の更新を行った。
Ubuntu 更新
更新自体は、do-release-upgrade コマンドで、xenial に更新が進む。
$ sudo do-release-upgrade
しかしながら、openssh-server が、更新でエラーがでて、かなり悩んだ。解ってみれば、単純なミスで、自作の /etc/init.d/script を書いてあったけど、その insserv ヘッダで、ssh をコピーしてヘッダ部の # Provides: sshd の行を書き換えてなかった。
このおかげで、insserv が、ssh はすでに登録されている…と勘違いしていた。
緊急連絡システムの文字コードUTF-8 に変更
Ubuntu の更新をかけたら、php5 が使えなくなったため、php7.0 に更新を行うが、これに合わせ、取り扱い文字コードを EUC-JP から UTF-8 に変更を行った。
緊急連絡システムは、内部でデータベースを利用せず、テキストファイルですべてを管理しているが、最初は利用組織毎の設定ファイルや、データファイルをエディタでチマチマと修正を始めたけど、大量の組織のため、途中で断念。perl でファイル名や記載時のエンコーディングを修正するスクリプトを書いて一発変換。
あとは、プログラム中のエンコーディング依存の部分を修正し、送信できることを確認してひとまず移行作業完了。