ホーム » スタッフ » 斉藤徹

斉藤徹」カテゴリーアーカイブ

2024年12月
1234567
891011121314
15161718192021
22232425262728
293031  

検索・リンク

データベースとB木

2分探索木の考え方を拡張したものでB木があり、データベースシステムではB木を基本としたデータ構造が活用されている。

B木の構造

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

B木からデータの検索

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

B木へのデータの追加

B木にデータを追加する場合は、ノード内に空きがあれば、単純にデータの追加を行う。ノード内のデータが2N個を越える場合は、以下のような処理を行う。

ノード内のデータと追加データを並べ、その中央値を選ぶ。この中央値より大きいデータは、新たにつくられたノードに移す。中央値のデータは上のノードに追加処理を行う。このような方法を取ることで、2分木のような木の偏りが作られにくい構造となるようにする。

データを削除する場合も同様に、データ件数がN個を下回る場合は、隣接するノードからデータを取ってくることで、N個を下回らないようにする。

B木とデータベース

このB木の構造は、一般的にデータベースのデータを保存するために広く利用されている。

データベースシステムでは、データを効率よく保存するだけでなく、データの一貫性が保たれるように作られている。
例えば、データベースのシステムが途中でクラッシュした場合でも、データ更新履歴の情報を元にデータを元に戻し、データを再投入して復旧できなければならない。データを複数の所からアクセスした場合に、その順序から変な値にならないように、排他制御も行ってくれる。

データベースで最も使われているシステムは、データすべてを表形式で扱うリレーショナル・データベースである。

((リレーショナル・データベースの例))
STUDENT[]                           RESULT[]
ID   | name     | grade | course    ID   | subject | point
-----+----------+-------+--------   -----+---------+-------
1001 | t-saitoh |  5    | EI        1001 | math    | 83
1002 | sakamoto |  4    | E         1001 | english | 65
1003 | aoyama   |  4    | EI        1002 | english | 90
                                   外部キー
((SQLの例 2つの表の串刺し))
-- 60点以上の学生名,科目名,点数を出力 --
select STUDENT.name, RESULT.subject, RESULT.point --射影--
   from STUDENT , RESULT                          --結合--
   where STUDENT.ID = RESULT.ID     -- 串刺し --   --選択--
         and RESULT.point >= 60 ;

((上記SQLを Java で書いた場合))
STUDENT[] student = { ... } ;
RESULT[]  result  = { ... } ;
for( int st = 0 ; st < student.length ; st++ )           // 結合(from)
   for( int re = 0 ; re < result.length ; re++ )
      if ( student[ st ].ID == result[ re ].ID           // 選択(where)
           && result[ re ].point >= 60 )
           System.out.println( student[ st ].name + " "  // 射影(select)
                               + result[ re ].subject + " "
                               + result[ re ].point ) ;

B+木

データベースの処理では、目的のデータを O(log N) で見つける以外にも、全データに対する処理も重要である。この場合、全てのデータに対する処理では、単純なB木では再帰呼び出しが必要となる。しかし、他の表でも再帰処理を伴うと、プログラムは複雑になってしまう。

そこで、B木のデータを横方向に並べて処理を行う場合に、その処理が簡単になるように B+木が用いられる。
この方法では、末端のノードは、隣接するノードへのポインタを持つ。下図で示すB+木では、青で示す検索用のB木の部分と、赤で示す順次処理を行うためのシーケンスセットの部分から構成される。

ドメイン名とDNS

ドメイン名とDNS

インターネットでの通信では、IPプロトコルでコンピュータを指定するが、IPアドレスは無機質で覚えるのが大変であり、コンピュータに名前をつけて利用する。この際に、コンピュータの所属などが分かるようにしたものをドメイン名と呼ぶ。

例えば、電子情報工学科のドメイン名 www.ei.fukui-nct.ac.jp は、ピリオド部分で区切られ、以下のような意味を持つ。

  • .jp – 国ドメイン(.uk イギリス,.ch 中国,アメリカは無し)
  • .ac – 種別ドメイン(.co.jp,.com:会社,.ne.jp,net:ネットワーク系)
  • fukui-nct – 組織ドメイン
  • .ei. – サブドメイン(組織内が細分化されている場合)
  • www. – ホスト名

このような省略されていない、対象となるコンピュータを指定するためのドメイン名は、FQDN(Fully Qualified Domain Name)と呼ばれる。FQDNでの名前を ホスト名 と呼ぶことも多い。

ただしアメリカでは、国ドメインを一般的に使わない。また最近では、世界的な企業では国ドメインが意味をなさないので、アメリカ以外でも .com や .net といった、汎用トップレベルドメイン(gTLD)が使われる。様々なサービスを展開している企業では、組織種別が意味をなさないため、toyota.jp といった種別ドメインがない.jpドメイン名も増えてきた。高専機構のドメイン名 kosen-ac.jp も、”kosen-ac” が高専機構の組織ドメイン名なので注意。”-ac”は種別ドメインではない。

以下に、主要な組織ドメイン・国ドメインをあげる。

国ドメイン 国名
.jp 日本
.us アメリカ
.uk イギリス
.fr フランス
.de ドイツ
.cn 中国
.to トンガ
.tv ツバル
.gl グリーンランド
種別ドメイン(日本) 種別ドメイン 国名
.ac.jp .edu 教育機関
.go.jp .gov 政府機関
.co.jp .com 企業
.ne.jp .net ネットワーク組織
.or.jp .org 公益法人
.biz ビジネス用途
.info 情報関係用途
.name 名前

はgTLD

 

DNSのしくみ

DNSは、Domain Name Service であり、コンピュータ名(ドメイン名)から、IPアドレスを調べるサービスで、ポート番号53,UDPを使っている。

インターネットに接続する際には、最も身近なDNSの情報が与えられ、ユーザがコンピュータ名を問い合わせると、身近なDNSがコンピュータのIPアドレスを返してくれる。この際に、検索結果はキャッシュとして一定期間保存される。身近なDNSがそのコンピュータ名を知らない場合は、上位のDNSに問い合わせを行い、DNSルートサーバもコンピュータ名をキャッシュしていない場合は、管理元の組織のDNSに問い合わせが行われる。このようにすることで特定のDNSサーバに問い合わせが集中しないようになっている(負荷分散)。 DNSサーバの情報は DHCP サーバからIPアドレスなどと一緒に取得することができる。

以前の説明で DHCP(IPアドレス,サブネットマスク,ゲートウェイなどのネットワーク設定のサービス)を紹介しているが、IPアドレス以外にも、DHCPはそのネットワークで使える最寄りの DNS サーバの情報を得ることができる。


DNSと正引きと逆引き

DNSの使い方としては、一般的な使い方は、ドメイン名からIPアドレスを調べる正引きが多い。ブラウザは http://www.fukui-nct.ac.jp/ というURLが与えられたら、DNSに www.fukui-nct.ac.jp を問い合わせ、104.215.53.205 の結果が得られることで、http://104.215.53.205/ のコンピュータに接続を試みる。

これとは逆に、サーバ側では接続してきた相手のコンピュータが信頼できる相手か調べたい時がある。この時には IPアドレスからドメイン名を調べる逆引きを行う。これにより、IP アドレスをきちんと管理している組織であれば、ドメイン名が分かるのでどの組織から接続されているのか確認ができる。

DNSの情報を調べるためのコマンドは、nslookup を用いる。

DNSと様々な情報

DNS では、様々な情報が取得できる。IPアドレス以外にも、メールを送ってもらうサーバのIPアドレス(MXレコード)なども取得できる。

((( 正引きの例 )))
$ nslookup www.google.com
Server:         172.31.208.1
Address:        172.31.208.1#53

Non-authoritative answer:
Name:   www.google.com
Address: 142.250.206.228                                          # 調べる度に異なる値が返ってくるかも
Name:   www.google.com
Address: 2404:6800:400a:804::2004

((( 逆引きの例 )))
$ nslookup 142.250.206.228
228.206.250.142.in-addr.arpa    name = kix06s10-in-f4.1e100.net.  # 正引きと逆引きが一致していない例

Authoritative answers can be found from:

((( MX レコードを調べる例 )))
$ nslookup -query=MX fukui-nct.ac.jp   # MXレコード = そのドメイン宛のメールはどのコンピュータに送ればいい?
Non-authoritative answer:
fukui-nct.ac.jp mail exchanger = 10 fukuinct-ac-jp01c.mail.protection.outlook.com.

((( AAAA レコードを調べる例 )))
$ nslookup -query=AAAA www.google.com  # AAAAレコード = IPv6アドレスを指定した正引き
Non-authoritative answer:
Name:   www.google.com
Address: 2404:6800:400a:813::2004

((( 正引きと逆引きの異なる例 )))
$ nslookup tsaitoh.net
Name:   tsaitoh.net
Address: 64.33.3.150
$ nslookup 64.33.3.150
150.3.33.64.in-addr.arpa  name = ttn64-33-3-150.ttn.ne.jp.

DNSとセキュリティ

DNSは、コンピュータ名とIPアドレスを対応付けるものであり、これには正引き(コンピュータ名からIPアドレスを求める)と、逆引き(IPアドレスからコンピュータ名を求める)がある。セキュリティ対策が厳しい場所では、

  • 正引きを使うことで、特定の組織のドメイン名を持つコンピュータからのアクセスを許可/禁止する。(例:国ドメイン.xxからは接続拒否)
  • 正引きで、コンピュータ名が登録されている所からのみ許可する。(例:組織ドメイン.fukui-nct.ac.jpからは接続許可)
  • IPアドレスから逆引きして求めたコンピュータ名をさらに正引きして同じIPアドレスが求まるかを確認

といった対策を行う。

  • DNSのドメイン名は、当初は最初に申請した人に割り当てられる。このため、nintendo.com といったドメイン名を、関係ない人が取得するといったトラブルがあった。(サイバースクワッティング)
  • DNSを用いたクラッキングでは、ウィルスに感染させたパソコンに偽物のIPアドレスを教えることで、偽装した別コンピュータに誘導し個人情報を盗む手口がある。(DNSポイズニング/スプーフィング)
  • 他にもウィルスに感染させた大量のパソコンから、同時にルートサーバに大量のDNSの問合せを送ることで、処理能力を低下させると、インターネット全体でDNS参照ができなくなる攻撃もある。(DNSルートサーバへの分散DoSアタック)
  • DNSは、他のコンピュータに接続するための重要な情報だが、独裁国家などでは国にとって不都合な情報が得られるドメイン名のIPアドレスを改ざんしアクセスできないようにすることもある。このため、Google 社では 覚えやすい 8.8.8.8 という IPアドレスの DNS サーバを提供している。この 8.8.8.8 は、DNS の返答速度も速いことから、ブラウザの表示速度を高速化するために自分のPCに設定する人も多い。
  • 暗号化されていない通信は、同一ネットワーク上の機器がパケット解析ソフトなどを実行すると、相手がどういうDNS参照をしているか見られてしまう。この対策として最近は DNS over HTTPS という方式も出てきている。

ドメイン名と罠

データべースの設計と正規形

テスト問題の返却および解答の説明を行い、その後、データベースの設計において、重要な正規形についての説明の導入。

正規形

データベースにおいて、様々な不整合を防ぐために正しい設計が必要であることを 改めて説明し、それには正規形としての条件を満たしている必要があることを説明する。一般的に不整合が発生しないためには、以下の第1正規形第2正規形第3正規形を満たすように表を分ければよい。

第一正規形は、すべての要素が原子値である条件を満たせばいい。 要素の中が複数の項目であったり表形式のデータがあると、 表構造のリレーショナルデータベースにはできない。


キーの説明超キー(スーパーキー)とは、データベースで1つのデータを 選び出すために必要なデータ項目であり、複数の項目で1データを指定 できる場合もある。

候補キーとは、必要最小限の項目となっているものを指す。 1項目が抜けても選別できなくなるようであれば、候補キーとは言わない。 主キーとは、候補キーのなかで管理の都合上便利なもの。

データ項目の値が決まると、他のデータ項目が自動的に決まるものは、 従属関係があるという。

第1正規化 第2正規化

第二正規形は、部分従属がなく、すべての非キーデータ項目が、候補キーに 完全従属する場合をいう。

  • 完全従属とは、候補キーを構成する全てのデータ項目に、非キーデータ項目が従属していること。
  • 部分従属とは、候補キーを構成するデータ項目の一部のデータ項目に、非キー項目が従属していること。

この例において、単価は商品が決まれば自動的に求まる情報。 (単価が日々変化することはないという条件で…) これは、部分従属となる。他に部分従属となっている属性は何か?

  • 推移従属性とは、データ項目でA→B→Cと、次々と値が求められる関係を指す。

第三正規形とは、 候補キー以外の非キーデータ項目は、候補キーに完全従属し、 かつどの候補キーにも推移従属しない関係をいう。

第3正規化

上記の例では、単価と個数が決まれば、金額が求まる推移従属の関係が含まれている。

おまけ:BC正規形,第4,5正規形

この他にも、 さらに「非キーからキーに関数従属性がある場合にそれを取り除く」、 ボイスコッド正規形(BC正規化)。 「対称性のある多値従属性(キーを決めると複数データが該当)を分解」して得られる第4正規形や、 「元になるテーブルの結合従属性を維持して分解」することにより得られる第5正規形などがある。

トップダウン設計・ボトムアップ設計

データベースの設計にあたって、実際の設計手順の説明を行う。

トップダウン設計では、対象業務を記述し、その中から名詞となっている実体を抽出する。 さらに動詞や形容詞のように記載されている関連を抽出する。 抽出した実体・関連では、あいまいであったり冗長であったりするので、整理したうえで、 その実体・関連をER図に表す。

ボトムアップ設計では、対象業務で実際に使われている入力帳票や結果の出力などを 見ながら、第1正規形を満たすように表を作っていく作業からおこなう。

トップダウン設計やボトムアップ設計で、 ER図や第一正規形を満たすような表が出来上がったら、 その属性の中で従属性を確認しながら、第2正規形・第3正規形へと整理していく。

データベース後半課題

データベース後半の課題は「卒業研究の対象をデータベースとして設計」とする。

情報系の卒研テーマであれば、処理対象のデータの中にはデータベースで管理するのがふさわしい対象について設計せよ。実験系の卒研テーマであれば、実験結果の表をデータベースで管理するとした場合の設計を行うこと。どちらでもない卒研で、卒研のテーマの中にデータベース化すべき対象が無い場合は、身の回りの帳票(例えばコンビニのレシートなど)をデータベース化することを検討すること。

レポートで記載する内容は、以下の通りとする。

  • 卒業研究におけるデータベース化する対象の説明
  • データベースをトップダウン設計する際の
    • 実体と関連を抽出するまでの説明
    • 正規化を行う経過の説明
    • 上記を踏まえたトップダウン設計でのER図
  • データベースをボトムアップ設計する際の
    • 対象とする帳票に相当するデータの一例と説明
    • レベル分けや正規化を行う経過の説明
    • 上記を踏まえたボトムアップ設計でのER図
  • 考察
    • トップダウン設計とボトムアップ設計に違いがあれば、設計の見直しの過程の説明
    • 両設計方法から分かったこと

Node-REDのインストール

卒研学生用に、古いPCに Ubuntu 24 をインストールし、Node-RED 環境を構築

Ubuntu のセットアップ後に以下の作業を行う。

nodejs のインストール

新しい nodejs 22 を入れたいので apt パッケージは使わない。

#--- nodejs 22 install
$ sudo apt install curl
$ curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
$ sudo apt install -y nodejs

Node-REDのインストール

#--- node-red install
$ sudo npm install -g npm@10.9.1
$ sudo npm install -g -unsafe-perm node-red node-red-admin

Node-REDのsystemd登録

OS起動と共に Node-RED を起動したいので、Systemd に登録

Systemd のファイルを作成

#--- node-red systemd setup
$ sudo vi /etc/systemd/system/node-red.service

[Unit]
After=syslog.target network.target
Documentation=http://nodered.org/

[Service]
Environment="NODE_OPTIONS=--max-old-space-size=128"
Environment="NODE_RED_OPTIONS=-v"
ExecStart=/usr/bin/node-red $NODE_OPTIONS $NODE_RED_OPTIONS
WorkingDirectory=/root/
User=root
Group=root
Nice=10
SyslogIdentifier=Node-RED
StandardOutput=syslog
Restart=on-failure
KillSignal=SIGINT

[Install]
WantedBy=multi-user.target

Systemd の有効化と起動と確認

$ sudo systemctl enable node-red
$ sudo systemctl start node-red
$ sudo systemctl status node-red
● node-red.service
     Loaded: loaded (/etc/systemd/system/node-red.service; enabled; preset: enabled)
     Active: active (running) since Wed 2024-12-04 14:25:50 JST; 10min ago
       Docs: http://nodered.org/
   Main PID: 23602 (node-red)
      Tasks: 11 (limit: 9334)
     Memory: 58.2M (peak: 68.4M)
        CPU: 2.243s
     CGroup: /system.slice/node-red.service
             └─23602 node-red

12月 04 14:25:51 tsaitoh-lab Node-RED[23602]: 復元することはできません。その場合、ファイルを削除してクレデンシャルを
12月 04 14:25:51 tsaitoh-lab Node-RED[23602]: 再入力しなければなりません。
12月 04 14:25:51 tsaitoh-lab Node-RED[23602]: 設定ファイル内で 'credentialSecret' オプションを使って独自キーを設定
12月 04 14:25:51 tsaitoh-lab Node-RED[23602]: します。変更を次にデプロイする際、Node-REDは選択したキーを用いてクレ
12月 04 14:25:51 tsaitoh-lab Node-RED[23602]: デンシャルを再暗号化します。
12月 04 14:25:51 tsaitoh-lab Node-RED[23602]: ---------------------------------------------------------------------
12月 04 14:25:51 tsaitoh-lab Node-RED[23602]: 4 Dec 14:25:51 - [warn] 暗号化されたクレデンシャルが存在しません
12月 04 14:25:51 tsaitoh-lab Node-RED[23602]: 4 Dec 14:25:51 - [info] サーバは http://127.0.0.1:1880/ で実行中です
12月 04 14:25:51 tsaitoh-lab Node-RED[23602]: 4 Dec 14:25:51 - [info] フローを開始します
12月 04 14:25:51 tsaitoh-lab Node-RED[23602]: 4 Dec 14:25:51 - [info] フローを開始しました

後は、ブラウザを起動して、http://127.0.0.1:1880/ を開くだけ。

トランスポート層・TCPとUDP

前回の授業でブロードキャストなどの説明をしたことを踏まえ、DHCP について改めて説明

DHCP

前回の IP では、異なるサブネットを繋ぐ役割としての Internet Protocol (IP) について説明をした。IP での通信では、IPアドレスが必要だが、正しく接続ができるためには、(1)IPアドレス、(2)サブネットマスク、(3)ゲートウェイアドレス が必要となる。しかし、この情報は初心者には設定が難しいし、IPアドレスが他の利用者と重複させないためには、きちんとした管理が必要となる。

この時に使われるのが DHCP(Dynamic Host Configuration Protocol) であり、通常のパソコンは、IPの自動設定としておけば、DHCP を用いて前述の(1),(2),(3) の情報が自動で設定される。DHCP機能は、一般的にルータや WiFi の AP(アクセスポイント)の中に組み込まれている。

DHCPサーバには、使用可能なIPアドレスを登録しておく。利用者が DHCP クライアントとして接続する際には、ブロードキャストパケットを使い、同じサブネット内に DHCP リクエストを送る。このリクエストを DHCPサーバが受信したら、サーバはクライアントに向かって、貸し出し用の IP アドレスの1つをクライアントに提供する(サブネットマスクやゲートウェイなども提供)。


データ通信ではノイズなどの影響で通信に失敗することがある。これらを補うためのTCPがある。またTCPの通信の欠点を補うUDPがある。この授業では、TCPとUDPによるトランスポート層の説明を行う。

TCP

TCP(Transmission Control Protocol/トランスミッションコントロールプロトコル)では、分割されたパケットを元の順序に組み上げたり、パケットが途中で消えた場合の再送などの処理を行う。この機能により確実に相手に送る機能が実現されている。

3way ハンドシェーク

TCPの通信では、最初に相互に通信が可能かを確認するハンドシェークが行われる。パケットには、SYN,ACK,FINといった種別を表すフラグがついており、SYNは接続確立の要求を表す。ACKは了解を表す。FINは切断要求を表す。通信開始の時には、(1)通信OK?、(2)OKだよ,そっちもOK?、(3)OKだよ! といった3つの通信パケットで確認してから通信を行う。この最初のやり取りを3way ハンドシェークという。

  • SYN flood攻撃 – 3wayハンドシェークは、この後に送られてくるパケットを並び替えるためのメモリを準備などが必要となる。このため通信ルールを無視して相手にSYNパケットだけを大量に送ると相手はムダな準備作業により他の通信が困難になる場合がある。

SEQ番号,ACK番号

通信パケットには、SEQ番号(シーケンス番号/32bit)ACK番号(アクノリッジ番号/32bit)という情報がついており、送信元はACK番号を確認することで、どのパケットが正しく届いたのかを認識できる。3wayハンドシェーク時には、相手のSEQ番号に1を加えたACK番号をつけてパケットを返送することで通信が始められることが確認できる。実際のデータを送信する際には、受け取ったデータ長をSEQ番号に加えた値を、ACK番号にして受信に成功したことを相手に伝える。これにより、小分けにされたパケットで次に何を送れば良いのか判別できる。
(Acknowledge = 承認する)

通信で、パケット分割して送って、その一つ毎の返答を待つと、通信の待ち時間が増えてしまう。このため、相手が受け取り可能であれば、一度に前回の2倍のパケットを返信を待たずに送る。(ウィンドウサイズの拡大)

チェックサムとタイムアウト

通信では、送る途中でデータにノイズが混入したり、パケットが消失することがある。このため、パケットにはパケットのチェックサム(バイトデータを加算した値)を付けて送り、受信時に比較してノイズ混入でデータが壊れていないかを確認する。こういった際には、パケットが正しく届かない。パケットが消失したりして、通信相手からの返送が届かないで一定の待ち時間が経過することをタイムアウトと呼ぶ。この時、返信パケットにはデータのSEQ番号とACK番号の情報があるため、受け取りに失敗したパケットが判別できるので、送り側は失敗したパケットを再送する。

受け取り側は、同じくSEQ番号やACK番号を元にパケットの順番を正しく並べ戻すことができる。

TCP FINパケット

通信を切断する場合には、相互に切断して良いか確認する4回の通信で終了する。

UDP

TCPによる通信は、相手側からの受け取った返事を待ちながら通信を行う。このため、通信にかかる時間を要する。また、複数の利用者に一斉にデータをばらまくブロードキャスト通信では、個別のパケット欠落を修復しようとすると、処理が複雑になる。

これらの対応策として、UDP(User Datagram Protocol)がある。これは、TCP通信でのパケット分割や再送処理を行わない極めて単純な送信方法である。このため、相手側に正しくデータが送られる保証はない。確実に相手に送る必要があれば、確認や再送は上位プロトコルの責任となる。

UDP通信は返信を待つ必要がないので、動画・音声配信などのリアルタイム性が求められる通信でよく使われる。UDPは正しく通信ができずパケットが途絶えても、一時的に動画が止まるなり音声が止まるといったように、問題が少ないような場合に有用となる。

ICMP/ping

IPプロトコルのプロトコルの1つとして ICMP (Internet Control Message Protocol) がある。このプロトコルは、ネットワーク機器(ノード)の間で、通信の確認をするためのもので、ping コマンドや traceroute コマンドで使われる。

基本的に、ICMPパケットを相手コンピュータに送り、返事が返ってくるかを確認する。ping や traceroute は、返事が返ってくるまでの時間や、パケットをいくつ送っていくつ返ってきた…などの情報を表示することができ、相手コンピュータまでの通信路が正常かどうかが判断できる。

$ ping www.google.co.jp
PING www.google.co.jp (172.217.25.163) 56(84) バイトのデータ
64 バイト応答 送信元 syd09s13-in-f3.1e100.net (172.217.25.163): icmp_seq=1 ttl=115 時間=7.85ミリ秒
64 バイト応答 送信元 syd09s13-in-f3.1e100.net (172.217.25.163): icmp_seq=2 ttl=115 時間=8.02ミリ秒
^C   # 途中で強制終了させるために Ctrl-C で止める
]$ traceroute www.google.co.jp
traceroute to www.google.co.jp (172.217.25.163), 30 hops max, 60 byte packets
 1  airstation.ei.fukui-nct.ac.jp (192.168.2.1)  0.355 ms  0.529 ms  0.549 ms
(略)
 9  108.170.243.33 (108.170.243.33)  8.245 ms 108.170.243.65 (108.170.243.65)  6.893 ms  6.936 ms
10  72.14.239.25 (72.14.239.25)  6.899 ms  7.125 ms 72.14.238.23 (72.14.238.23)  7.140 ms
11  syd09s13-in-f163.1e100.net (172.217.25.163)  7.014 ms  7.007 ms  6.961 ms

トランスポート層

OSI参照モデルでは、TCPプロトコルとUDPプロトコルをあわせてトランスポート層と呼び、TCP+UDPとネットワーク層のIPプロトコルでの通信が、今日のインターネット通信の基本プロトコルとなっており、総称して TCP/IPとかインターネット・プロトコル・スイート と呼ぶ。(suite=”一連のものや一緒に機能するものの集まり”/sweetじゃない)

木構造の探索・深さ優先探索・幅優先探索

深さ優先探索(前順)

以前紹介した2分木の表示では、再帰呼び出しで木の処理を記述していた。

しかし、スタックを使って未処理のノードを保存しながらノードに対する処理を行えば、ループ処理で「すべての木のノード」に対する処理を記述できる。このような木に対する処理は、末端方向に処理が進むことが優先されるため深さ優先探索と呼ぶ。

以下のプログラムで示す深さ優先探索では、ノードに対する処理を行ってから左の枝葉の処理が行われる。このような深さ優先探索は前順(pre-order)と呼ぶ。

このような探索は、ゲームですべての戦略の先読みを行う処理で用いられる。

import java.util.*;

class BTreeNode {
    int       data ;
    BTreeNode left ;
    BTreeNode right ;
    
    BTreeNode( int x , BTreeNode l , BTreeNode r ) {
        this.data  = x ;
        this.left  = l ;
        this.right = r ;
    }
}

public class Main {
    // 再帰呼び出しによる木の操作 = 深さ優先探索(前順)
    public static void print_rec_depth_first_pre_order( BTreeNode p ) {
        if ( p != null ) {
            System.out.print( p.data + " " ) ;
            print_rec_depth_first_pre_order( p.left ) ;
            print_rec_depth_first_pre_order( p.right ) ;
        }
    }
    // 深さ優先探索(前順)
    public static void print_depth_first_pre_order( BTreeNode p ) {
        // 未処理ノードを保存するスタック
        LinkedList stack = new LinkedList<>() ;
        stack.addFirst( p ) ; // push
        while( !stack.isEmpty() ) {
            // スタックトップを取り出す
            p = stack.removeFirst() ; // pop
            if ( p != null ) {
                System.out.print( p.data + " " ) ;
                // 枝を未処理スタックに保存
                stack.addFirst( p.right ) ; // 右を後回し
                stack.addFirst( p.left ) ;
            }    
        }
    }
    public static void main(String[] args) throws Exception {
        BTreeNode top =
            new BTreeNode( 42 ,
                    new BTreeNode( 27 ,
                            new BTreeNode( 13 , null , null ) ,
                            new BTreeNode( 38 , null , null ) ) ,
                    new BTreeNode( 72 ,
                            new BTreeNode( 64 , null , null ) ,
                            new BTreeNode( 81 , null , null ) ) ) ;
        // 再帰による深さ優先探索(前順)
        print_rec_depth_first_pre_order( top ) ;
        System.out.println() ;
        // スタックを使った深さ優先探索(前順)
        print_depth_first_pre_order( top ) ;
        System.out.println() ;
    }
}

幅優先探索

同じように、未処理のノードを待ち行列に保存しながら、ノードに対する処理を行うことでも、すべての木に対する処理が記述できる。この場合、根本の処理をすべて終えてから、末端に対する処理が行われる。このような処理は幅優先探索と呼ぶ。

幅優先探索の処理と、前順の深さ優先探索を比べると、ノードを覚えるデータ構造に待ち行列を使う(幅優先探索)か、スタックを使う(深さ優先探索)の違いしかない。

幅優先探索は、ゲームの戦略で先読みを行う場合に用いられるが、チェス・将棋・囲碁といったすべての先読みを行うと手数が爆発的に増加するため、不利な戦略の先読みを途中で打ち切る必要がある場合に有用である。

public class Main {
    // 幅優先探索
    public static void print_breadth_first( BTreeNode p ) {
        // 未処理ノードを保存する待ち行列
        LinkedList queue = new LinkedList<>() ;
        queue.addFirst( p ) ; // put
        while( !queue.isEmpty() ) {
            // 待ち行列の先頭を取り出す
            p = queue.removeLast() ; // get
            if ( p != null ) {
                System.out.print( p.data + " " ) ;
                // 枝を未処理待ち行列に追加
                queue.addFirst( p.left ) ; // 左から処理
                queue.addFirst( p.right ) ;
            }    
        }
    }
    public static void main(String[] args) throws Exception {
        BTreeNode top = (略) ;
        // 幅優先探索で表示
        print_breadth_first( top ) ;
        System.out.println() ;
    }
}

深さ優先探索(中順)

2分探索木のようなデータで、昇順で並べて表示する必要がある場合、再帰呼び出しのプログラムでは、「左枝の処理、ノードへの処理、右枝の処理」の順で記述する必要がある。このような深さ優先探索は中順(in-order)と呼ばれる。しかし、先に説明した再帰による深さ優先探索(前順/pre-order)では、「ノードへの処理、左枝の処理、右の枝の処理」で記述されている。

スタックを用いた深さ優先探索で、2分探索木の昇順のように中順で処理を行うには、未処理のノードの保存順序に工夫が必要となる。

import java.util.*;
public class Main {
    // 再帰呼び出しによる木の操作 = 深さ優先探索(中順)
    public static void print_rec_depth_first_in_order( BTreeNode p ) {
        if ( p != null ) {
            print_rec_depth_first_in_order( p.left ) ;
            System.out.print( p.data + " " ) ;
            print_rec_depth_first_in_order( p.right ) ;
        }
    }
    // 深さ優先探索(中順)
    public static void print_depth_first_in_order( BTreeNode p ) {
        // 未処理ノードを保存するスタック
        LinkedList stack = new LinkedList<>() ;
        for( ;; ) {
            // ノードを保存しながら左末端まで進む
            while( p != null ) {
                stack.addFirst( p ) ;
                p = p.left ;
            }
            // 未処理が残っていないなら終了
            if ( stack.isEmpty() )
                break ;
            // スタックトップを取り出す
            p = stack.removeFirst() ; // pop
            System.out.print( p.data + " " ) ;
            p = p.right ;
        }
    }
    public static void main(String[] args) throws Exception {
        BTreeNode top = (略) ;

        // 再帰による深さ優先探索(中順)
        print_rec_depth_first_in_order( top ) ;
        System.out.println() ;
        // スタックを使った深さ優先探索(中順)
        print_depth_first_in_order( top ) ;
        System.out.println() ;
    }
}

2分木による構文木

コンパイラの処理の流れ

構文の構造を表すために、2分木を使うという話をこの後に行うが、その前にコンパイラが機械語を生成するまでの処理の流れについて説明をする。

Cコンパイラのソース
  ↓
プリプロセッサ (#define,#includeなどの処理)
  ↓
コンパイラ
・字句解析(ソースコードをトークンに切り分ける)
・構文解析(トークンから構文木を生成)
・最適化(命令を効率よく動かすために命令を早い命令に書き換え)
・コード生成(構文木から中間コードを生成)
  |
  | リンカでライブラリと結合
 (+)←---ライブラリ
  ↓ 
 機械語

バイトコードインタプリタ

通常、コンパイラとかインタプリタの説明をすると、Java がコンパイラとか、JavaScript はインタプリタといった説明となる。しかし、最近のこういった言語がどのように処理されるのかは、微妙である。

(( Java の場合 ))
foo.java (ソースコード)
 ↓       Java コンパイラ
foo.class (中間コード)
 ↓
JRE(Java Runtime Engine)の上で
中間コードをインタプリタ方式で実行

あらかじめコンパイルされた中間コードを、JREの上でインタプリタ的に実行するものは、バイトコードインタプリタ方式と呼ぶ。

ただし、JRE でのインタプリタ実行では遅いため、最近では JIT コンパイラ(Just-In-Time Compiler)により、中間コードを機械語に変換してから実行する。

また、JavaScriptなどは(というか最近のインタプリタの殆どPython,PHP,Perl,…は)、一般的にはインタプリタに分類されるが、実行開始時に高級言語でかかれたコードから中間コードを生成し、そのバイトコードをインタプリタ的に動かしている。

しかし、インタプリタは、ソースコードがユーザの所に配布されて実行するので、プログラムの内容が見られてしまう。プログラムの考え方が盗まれてしまう。このため、変数名を短くしたり、空白を除去したり(…部分的に暗号化したり)といった難読化を行うのが一般的である。

字句解析と構文解析

トークンと正規表現(字句解析)

字句解析でトークンを表現するために、規定されたパターンの文字列を表現する方法として、正規表現(regular expression)が用いられる。

((正規表現の書き方))
選言     「abd|acd」は、abd または acd にマッチする。
グループ化 「a(b|c)d」は、真ん中の c|b をグループ化
量化    パターンの後ろに、繰り返し何回を指定
      ? 直前パターンが0個か1個
       「colou?r」
      * 直前パターンが0個以上繰り返す
       「go*gle」は、ggle,gogle,google
      + 直前パターンが1個以上繰り返す
       「go+gle」は、gogle,google,gooogle

正規表現は、sed,awk,Perl,PHPといった文字列処理の得意なプログラム言語でも利用できる。こういった言語では、以下のようなパターンを記述できる。

[文字1-文字2...] 文字コード1以上、文字コード2以下
      「[0-9]+」012,31415,...数字の列
^     行頭にマッチ
$     行末にマッチ
((例))
[a-zA-Z_][a-zA-Z_0-9]* C言語の変数名にマッチする正規表現

構文とバッカス記法

言語の文法(構文)を表現する時、バッカス記法(BNF)が良く使われる。

((バッカス記法))
<表現> ::= <表現1...> | <表現2...> | <表現3...> | ... ;

例えば、加減乗除記号と数字だけの式の場合、以下の様なBNFとなる。

((加減乗除式のバッカス記法))
<加算式> ::= <乗算式> '+' <乗算式>    【要注意】わざと間違っている部分あり
          | <乗算式> '-' <乗算式>
          | <乗算式>
          ;
<乗算式> ::= <数字> '*' <乗算式>
          | <数字> '/' <乗算式>
          | <数字>
          ;
<数字>   ::= [0-9]+
          ;

上記のバッカス記法には、間違いがある。”1+2+3″を正しく認識できない。どこが間違っているだろうか?

このような構文が与えられた時、”1+23*456″と入力されたものを、“1,+,23,*,456”と区切る処理が、字句解析である。

また、バッカス記法での文法に合わせ、以下のような構文木を生成するのが構文解析である。

  +
 / \
1   *
   / \
  23   456

2項演算と構文木

演算子を含む式が与えられたとして、古いコンパイラではそれを逆ポーランド変換して計算命令を生成していた。しかし最近の複雑な言語では、計算式や命令を処理する場合、その式(または文)の構造を表す2分木(構文木)を生成する。。

   +
  / \
 1   *
    / \
   2   3

演算子の木のノードで、末端は数値であることに注目し、右枝・左枝がNULLなら数値(data部にはその数値)、それ以外は演算子(data部には演算子の文字コード)として扱うとして、上記の構文木のデータを作る処理と、その構文木の値を計算するプログラムを示す。

import java.util.*;

class ExpTreeNode {
    String      op_val ;
    ExpTreeNode left ;
    ExpTreeNode right ;
    
    ExpTreeNode( String op , ExpTreeNode l , ExpTreeNode r ) {
        this.op_val = op ;
        this.left   = l ;
        this.right  = r ;
    }
    ExpTreeNode( String val ) {
        this.op_val = val ;
        this.left   = null ;
        this.right  = null ;
    }
}

public class Main {
    public static int eval( ExpTreeNode p ) {
        if ( p.left == null && p.right == null ) {
            return Integer.parseInt( p.op_val ) ;
        } else {
            int l_val = eval( p.left) ;
            int r_val = eval( p.right ) ;
            switch( p.op_val ) {
            case "+" : return l_val + r_val ;
            case "*" : return l_val * r_val ;
            }
            return -1 ; // error
        }
    }
    public static void main(String[] args) throws Exception {
        ExpTreeNode top =
            new ExpTreeNode( "+" ,
                             new ExpTreeNode( "1" ) ,
                             new ExpTreeNode( "*" ,
                                              new ExpTreeNode( "2" ) ,
                                              new ExpTreeNode( "3" ) ) ) ;
        
        System.out.println( eval( top ) ) ;
    }
}

オブジェクト指向を使った構文木(テスト範囲外)

前述の2分木を用いた構文木では、式を構成する「整数値」の式、「式 演算子 式」の演算式の二つのパターンを、無理やり1つのクラスで扱っていた。整数値と演算子の文字列の違いがあるため、数値も文字列で保存したものを parseInt() で数値に変換している。

こういった、大きくは式と分類できるけど、数値を表す式、演算子からなる式と異なるデータの場合、Java では、オブジェクト指向プログラミングの抽象クラス仮想関数のテクニックを用いる。式を表現する基底クラス ExpNode から、数値を表す ExpNodeInteger , 演算子からなる式を表す ExpNodeOperator を派生させ、式の値を求める際には、各クラスに応じた計算処理を行う仮想関数 eval() を使って処理を行っている。

import java.util.* ;

// 抽象基底クラス
abstract class ExpNode {
    abstract public int eval() ;
}

// 整数で具体化
class ExpNodeInteger extends ExpNode {
    int  value ;
    
    ExpNodeInteger( int i ) {
        this.value = i ;
    }
    // 仮想関数
    public int eval() {
        return this.value ;
    }
}

// 演算子で具体化
class ExpNodeOperator extends ExpNode {
    String  op ;
    ExpNode left ;
    ExpNode right ;
    
    ExpNodeOperator( String s , ExpNode l , ExpNode r ) {
        this.op    = s ;
        this.left  = l ;
        this.right = r ;
    }
    // 仮想関数
    public int eval() {
        int l_val = this.left.eval() ;
        int r_val = this.right.eval() ;
        switch( this.op ) {
        case "+" : return l_val + r_val ;
        case "*" : return l_val * r_val ;
        }
        return -1 ;
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
        ExpNode top =
            new ExpNodeOperator( "+" ,
                                 new ExpNodeInteger( 1 ) ,
                                 new ExpNodeOperator( "*" ,
                                                      new ExpNodeInteger( 2 ) ,
                                                      new ExpNodeInteger( 3 ) ) ) ;

        System.out.println( top.eval() ) ;
    }
}

ERモデル

データベースの設計

リレーショナル・データベースでは、データは表形式であればなんでも良い訳ではない。

例えば、学生の成績データが以下のような1つのテーブルで構成されるデータだった場合、

ID name grade subject teacher
20101 青山 1 データベース 斉藤
20101 青山 1 ソフトウェア工学 村田
20002 鈴木 2 データベース 斉藤
20002 鈴木 2 コンパイラ 西
30203 山田 3 情報メディア 小松
× × × 情報ネットワーク 森田
  • 修正不整合: 授業担当が 斉藤高久 のように変更になったら、複数のテーブルを修正しなければならない。大量のレコード修正は、時間がかかるし、その途中にシステムダウンしたらデータの整合性に問題が発生するかも。
  • 挿入不整合: 新しい科目 情報ネットワーク を追加したいけど、受講学生が決まらないとデータを挿入できない。
  • 削除不整合: 山田 が受講を取りやめたら、科目 情報メディア も消えてしまう。

これらを考慮すると、以下のような3つの表で設計するべきである。

■学生(実体)

ID name grade
20101 青山 1
20002 鈴木 2
30203 山田 3
■受講(関係)

ID (FK) SubID (FK)
20101 1001
20101 1002
20002 1001
20002 1003
30203 1004

30203を消しても情報メディア
の科目のデータは残る
FK=外部キー

■科目(実体)

SubID Subject teacher
1001 データベース 斉藤
1002 ソフトウェア工学 村田
1003 コンパイラ 西
1004 情報メディア 小松
1005 情報ネットワーク 森田

斉藤高久に変更したら
複数のレコードを要修正。

情報ネットワークを追加しても問題なし

データベースの設計では、(1)概念設計、(2)論理設計、(3)物理設計が行われる。

  • 概念設計:概念スキーマの決定(実体・関係モデルを使う)。上記の受講データベースの設計例
  • 論理設計:論理スキーマの決定。関係データベースで実装?ほかのデータベース?
  • 物理設計:物理スキーマの決定。データの格納方法や管理方法を決める。

実体関連モデル(ERモデル)

データベース設計では、実体関連モデル(ERモデル:Entity-Relation model)が使われる。 実体とは、モデル化しようとする対象で独立した存在となれるもの。 実体が持つ色々な特性は属性と呼ばれる。 属性の取りうる値の集合を定義域、同一種類の実体の集まりを実体集合と呼ぶ。 関連とは、実体同士の相互関係をモデル化したもの

実体関連図(ER図)では、実体を長方形、関連をひし形、属性を楕円で表現する。 属性で、キーとなるものには下線をつけて表す。

ER図で調べると、実際にはもっと細かい規定で表現が行われている。 参考:IDEF1X表記とIE表記

専攻科実験・コンパイラと関数電卓プログラム作成

2分木の応用(意思決定木と演算子)

データをO(log N)で検索するための2分探索木以外2分木のデータ構造について解説を行う。

意思決定木

意思決定木の説明ということで、yes/noクイズの例を示しながら、2分木になっていることを 説明しプログラムを紹介。

   ((意思決定木の例:小さい子供が発熱した時))
       38.5℃以上の発熱がある?
      no/         \yes
   元気がある?        むねがひいひい?
 yes/    \no      no/     \yes
様子をみる 氷枕で病院  解熱剤で病院  速攻で病院

このような判断を行うための情報は、yesの木 と noの木の2つの枝を持つデータである。これは2分木と同じである。左右に枝のあるものは質問であり、yesの枝もnoの枝もない末端は最終決断を表す。このようなデータ構造は意思決定木と呼ばれ、質問と決断の処理は以下のように記述ができる。

import java.util.* ;
import java.util.Scanner ;

class TreeNode {
    String qa ;
    TreeNode    yes ;
    TreeNode    no ;
    
    TreeNode( String s , TreeNode y , TreeNode n ) {
        this.qa  = s ;
        this.yes = y ;
        this.no  = n ;
    }
} ;

public class Main {
    public static void main(String[] args) throws Exception {
        Scanner sc = new Scanner( System.in ) ;
        TreeNode top =
            new TreeNode( "38.5℃以上の発熱がある?" ,
                          new TreeNode( "胸がひぃひぃ?" ,
                                        new TreeNode( "速攻で病院" ,   null , null ) ,
                                        new TreeNode( "解熱剤で病院" , null , null ) ) ,
                          new TreeNode( "元気がある?" ,
                                        new TreeNode( "様子をみる",    null , null ) ,
                                        new TreeNode( "氷枕で病院",    null , null ) ) ) ;
        TreeNode p = top ;
        while( p.yes != null || p.no != null ) {
            System.out.println( "Question:" + p.qa ) ;
            if ( sc.nextInt() == 1 ) {
                System.out.println( "yes" ) ;
                p = p.yes ;
            } else {
                System.out.println( "no" ) ;
                p = p.no ;
            }
        }
        System.out.println( "Answer:" + p.qa ) ;
    }
}
struct Tree {
   char *qa ;
   struct Tree* yes ;
   struct Tree* no ;
} ;
struct Tree* dtree( char *s ,
                    struct Tree* l , struct Tree* r )
{  struct Tree* n ;
   n = (struct Tree*)malloc( sizeof( struct Tree ) ) ;
   if ( n != NULL ) {
      n->qa = s ;
      n->yes = l ;
      n->no = r ;
   }
   return n ;
}
void main() {
   struct Tree* p =
      dtree( "38.5℃以上の発熱がある?" ,
             dtree( "胸がひぃひぃ?" ,
                    dtree( "速攻で病院",  NULL,NULL ) ,
                    dtree( "解熱剤で病院",NULL,NULL ) ) ,
             dtree( "元気がある?" ,
                    dtree( "様子をみる",  NULL,NULL ) ,
                    dtree( "氷枕で病院",  NULL,NULL ) ) ) ;
   // 決定木をたどる
   struct Tree* d = p ;
   while( d->yes != NULL || d->no != NULL ) {
      printf( "%s¥n" , d->qa ) ;
      scanf( "%d" , &ans ) ;
      // 回答に応じてyes/noの枝に進む。
      if ( ans == 1 )      // yesを選択
         d = d->yes ;
      else if ( ans == 0 ) // noを選択
         d = d->no ;
   }
   // 最終決定を表示
   printf( "%s¥n" , d->qa ) ;
}

2分木の応用として次週以降に式の表現の説明を行うけど、その前に計算式の一般論の説明を行う。

逆ポーランド記法

一般的に 1*2 + 3*4 と記載すると、数学的には演算子の優先順位を考慮して、(1*2)+(3*4) のように乗算を先に行う。このような優先順位を表現する時に、()を使わない方法として、逆ポーランド記法がある。

演算子の書き方には、前置記法、中置記法、後置記法があり、後置記法は、「2と3を掛ける、それに1を加える」と捉えると、日本語の処理と似ている。

中置記法 1+2*3
前置記法 +,1,*,2,3
後置記法 1,2,3,*,+  # 1と「2と3をかけた値」をたす。

後置記法は、一般的に逆ポーランド記法(Reverse Polish Notation)とも呼ばれ、式をコンピュータで実行する際の処理と似ている。

演算子の右結合・左結合

例えば、”1/2*3″という式が与えられたとする。この結果は、1/6だろうか?3/2だろうか?

一般的な数学では、優先順位が同じ演算子が並んだ場合、左側から計算を行う。つまり”1/2*3″は、”(1/2)*3″を意味する。こういった左側の優先順位が高い演算子は左結合の演算子という。

ただしC言語やJavaでは、”a = b = c = 0″ と書くと、”a = (b = (c = 0))” として扱われる。こういった代入演算子は、 右結合の演算子である。

理解度確認

以下の式を指定された書き方で表現せよ。

逆ポーランド記法 1,2,*,3,4,*,+ を中置記法で表現せよ。
中置記法 (1+2)*3-4*5 を逆ポーランド記法で表現せよ。

以前の情報処理技術者試験では、スタックの概念の理解の例題として、逆ポーランド記法への変換アルゴリズムのプログラム作成が出題されることが多かったが、最近は出題されることはなくなってきた。

システム

最新の投稿(電子情報)

最近の投稿(斉藤 徹)

アーカイブ

カテゴリー