第12回:木構造

Download Report

Transcript 第12回:木構造

アルゴリズムとデータ構造
講義スライド 3
 スタック・キュー
 リスト
茨城大学 工学部 知能システム工学科 井上 康介
E2棟407号室
第5章 データ構造
 授業の最初で述べたとおり,大量・複雑なデータを扱う上
では,データを構造化・組織化する必要がある.
 どのようなデータ構造 (data structure) を採用するかに
よって,問題解決のアルゴリズムも異なってくる.
 つまり,アルゴリズムとデータ構造とは密接な関係にあり,
よいデータ構造を選ぶことがよいプログラムを作ることに
つながる.
 特に重要なデータ構造として,リスト,ツリー,グラフが
上げられる.ツリー (木) は第6章,グラフは第7章で扱う.
 この章では,スタック (stack: 棚),キュー (queue: 待ち
行列),リスト (list) を説明する.
p.213
2
データ構造とは
 代表的なデータ構造として,以下のものが挙げられる:
1. 表 (table: テーブル)
2. 棚 (stack: スタック)
3. 待ち行列 (queue: キュー)
4. リスト (list)
5. 木 (tree: ツリー)
6. グラフ (graph)
 表については,ふつうの配列で扱うことができるので,
ここでは扱わない.
 良いデータ構造とは何かの基準は一概には言えないが,
データの追加・削除・検索が効率よく行えること,およ
び,複雑な構造が簡潔に表現できることなどがある.
p.214~215
3
これから学ぶデータ構造
 テーブル (ただの配列)
a[0]
a[0][0]
a[0][1]
a[1]
a[1][0]
a[1][1]
a[2]
a[2][0]
a[2][1]
a[3]
a[3][0]
a[3][1]
a[4]
a[4][0]
a[4][1]
a[5]
a[5][0]
a[5][1]
 スタック (後入れ先だし)
 キュー (先入れ先出し)
4
これから学ぶデータ構造
 リスト (一本鎖)
データ
データ
データ
次のデータへのポインタ
終端は NULL
 ツリー (分岐を含む)
データ
データ
データ
データ
データ
データ
5
これから学ぶデータ構造
 グラフ・ネットワーク (ループも含む)
3分
秦病院
油縄子
Bulldog
秦病院
6分
油縄子
Bulldog
4分
7分
5分
カワチ
茨大
カスミ
グラフ (接続関係だけ)
カワチ
10分
茨大
カスミ
ネットワーク (区間に属性)
6
スタック
 スタック (stack) は,一時的データ置き場 (buffer) と
して利用される代表的なデータ構造の一つである.
 データを棚に上から順に積んでいき,取り出すときも上
から取り出す.データを放り込む操作を プッシュ
(push),取り出す操作を ポップ (pop) という.
pop
push
 このように,取り出せるのは最後に
Data-1
Data-0
入れたデータからである. Data-2
 このような方式を LIFO (Last In First
Out: 後入れ先出し) と呼ぶ.
 スタックの実装は,1次元配列を
用いて行われる.
スタック (棚)
p.216~218 (前半)
7
スタック
 スタックは,データを格納する配列と,「現在いくつの
データが入っているか」を示すスタックポインタから実現
される.
 配列を stack[],スタックポインタを sp とするとき,
次のようになる.
 sp==4 での push は「あふれ」
プッシュ
Data-A
Data-B
Data-D
Data-C
 sp==0 での pop は「空っぽ」
ポップ
 これらはエラー処理
stack[3]
stack[2]
stack[1]
stack[0]
sp= 4
3
2
1
0
8
スタック (ソースコード)
 p.217.
 プリプロセッサでスタックのサイズ MaxSize を定義.
 スタックの実体である配列 stack[] はグローバル変数
として定義.スタックポインタ sp もグローバル変数と
して定義し,0 に初期化している (ほんとうは,グロー
バル変数は極力使わないことが望ましい).
 プッシュ関数 int push(int n); とポップ関数 int
pop(int *n); では,int型の返値を返す.
 メイン関数では,while文でループさせてユーザからの
文字入力を繰り返し受けている.
 int getchar(void) は,キーボードから文字を読み
込み,int型に変換して返す関数.
9
スタック (ソースコード)
 while文のかっこ内でキーボードから1文字を読み込む.
 読み込んだ文字が 'i' あるいは 'I' であるときは,
ユーザからデータを入力して,それをスタックにプッ
シュする.
 スタックが既にいっぱいの場合, それ以上プッシュでき
ない.このとき,関数 push() は -1 を返す.
 読み込んだ文字が 'o' あるいは 'O' であるときは,ス
タックからデータを1つポップする.
 スタックが空の場合は取り出せないので関数 pop() は
-1 を返す.
 プッシュもポップも,正常に実行した場合は返値は 0
となる.
10
スタック (ソースコード)
 さて, ポップを行ったとき,データが正しく取り出せれば
0 を,スタックが空のときは -1 を返す.このために「返
値」という情報チャンネルは使われてしまう.
 では, 2つめの情報「ポップにより取り出したデータ」を
どうやって呼出側に返せばよいだろうか?
 データを返すために,ここでは ポインタ渡し (参照渡し)
という方法が使われている (通常は 値渡し).
 ポップしたデータを入れる変数は,main関数のローカル
変数 n であり,main関数で pop() を呼び出すとき,引
数としてその n へのポインタ (n のアドレス) を pop()
に渡す.
 pop() 内では,もらったアドレスにデータを書き込む.
11
スタック (ソースコード)
 それぞれの関数を見ていこう.
 push() では,最初にサイズあふれ (sp >= MaxSize)
が起きていないかチェックしている.
 起きていない場合は,配列の sp の位置に,値渡しで受け
取ったデータ n を書き込み,sp を1つ増やす.正常終了
なので,この場合は返値として 0 を返す.
 サイズあふれが起きていた場合,何もせずに -1 を返す.
 値渡しでデータを受け取る際,main関数のローカル変数
n がコピーされ,これが関数に渡されている.従って,関
数 push() の中で n に変更を加えたとしても,main関数
内の n の値には影響しない.
12
スタック (ソースコード)
 一方,pop() では,main関数内のローカル変数 n のポイ
ンタを受け取る.
 関数 pop() 内においては,n はそのポインタ (アドレス)
を示す変数であり, そのアドレスに書かれた情報を読み書
きするときは *n としてアクセスする.
 関数では,まずスタックが空 (sp==0) でないかチェック
し,空でないときは sp を1だけ減らしてから 配列の sp
番要素を *n に書き込み,正常終了を示す返値 0 を返す.
 空の場合は何もせずに異常終了の返値 -1 を返す.
13
スタックからのポップ
 関数 pop()の引数はポインタ渡しによる n.これは
main()関数内のローカル変数 n への参照 である.
グローバル変数
sp
0
関数
main()
3
2
pop(&n)
stack
12
25
-51
c
n
’o’
?
-51
?
int pop(int
int *n)
*n
n
?
キュー
 スタックでは,データを入れたのとは逆の順番でデータ
を取り出していた (LIFO方式).このやり方は,まさに再
帰呼出で紹介したような用途に向く.一方,例えば役所
の窓口に人がどんどん来る場合を考えよう.
 LIFO方式では,最後に列に加わった人から処理を行うが,
これは言うまでもなく不公平である.列に並んだ人々を,
最初に並んだ人から順番に捌いていく必要がある.これ
を FIFO (First In First Out: 先入れ先出し) という.
 このモデルを キュー (queue: 待ち行列) という.
 例)PCがインターネットで通信するとき,ネットからは
続々とデータが流入するが,処理が追いつかない場合が
ある.このとき,データを一時的にキューに置いておき,
手が空き次第,先に来たデータから順に処理する.
p.222~224
15
キュー




実装は,スタックと同様,1次元配列.
待ち行列の先頭位置を head,終端を tail とする.
tail の位置にデータを入れ,head から出す.
キューのサイズは, (配列サイズ)ー1 になる.
queue[5]
Data-F
 head  tail
queue[4]
Data-E
 head  tail
queue[3]
Data-D
 head  tail
queue[2]
Data-C
 head 
tail
これが限界!
queue[1]
Data-B
Data-H
 head  tail
queue[0]
Data-A
Data-G
 head  tail
16
キュー (ソースコード)
 p.223.
 プリプロセッサで配列サイズ MaxSize を100としている.
よって,容量は99となる.
 キューの実体である配列 queue[],および先頭・末端位
置 head,tail はグローバル変数として定義.
 キューにデータを入れる関数は int queuein(int n),
データを取り出す関数は int queueout(int *n).
 main関数では,スタックの場合と同様,キーボードから1
文字ずつの入力を繰り返し受け付け,それが 'i' または
'I' のときはデータを入れ,'o' または 'O' の時はデー
タを出している.
17
キュー (ソースコード)
 キューがいっぱいの時に更にデータを入れようとすると
queuein() が -1 を返す,キューが空の時に取り出し
をしようとすると queueout() が -1 を返す点はスタッ
クと同じ.また,queueout() において,取り出した
データを返すために参照渡しを使っている点も同じ.
 キューにデータを入れる関数 queuein() においては,
キューがいっぱいであるかどうかを,tail の次の位置が
head と同じかどうかによって調べている.
 「次の位置」を求めるために,(tail+1)%MaxSize とい
う計算をしている.MaxSize が 100 とするとき,もしも
tail が配列最後の位置 99 にあったとすれば,次の位置
は先頭 0 であり,それはこの計算で求まる.
18
キュー (ソースコード)
 tail の次の位置が head と重ならなければ,キューには
まだ余裕がある.このとき,tail の位置にデータ n を書
き込み,tail を1つ進める.
 queueout() では,最も古いデータを取り出す.その位
置は head である.
 最初にキューが空かどうかを調べる.head == tail な
らキューは空っぽなので取り出せない.
 head 位置のデータ queue[head] を,ポインタ変数 *n
に代入することで main関数に返した後,head を一つ進
める.
 head の「次の位置」は,(head+1)%MaxSize である.
 例)配列サイズが100のとき,99の次は(99+1)%100=0.
19
リストとは
 リスト (list) とは,データを鎖のようにつなぎ合わせて
管理するデータ構造である.
 各データは次のデータを指すポインタを持っている.この
ポインタで次々とデータをつないでいく (リンクする) こ
とによって,任意の数のデータを扱える.
 一方,データを配列で扱う場合は,あらかじめ決められた
配列サイズを超えるデータは格納できない.
 リストでは,データを新たに格納するたびに,そのデータ
のための記憶領域を新規に確保していくので,サイズの制
限がない (もちろんコンピュータ上のメモリサイズ以上は
格納できないが).
 といっても理解しづらいので,絵的に理解しよう.
p.228~229
20
リストとは
 リスト構造においてつながれる各データ要素 (ノード:
node) は,レコードによって構成される.
 ノードはデータを入れるデータ部の他に,次のノードのア
ドレスを指し示すポインタ部を含む.
 最後尾のポインタ部は NULL としておく.
 プログラムからリスト構造にアクセスするため,リストの
先頭へのポインタ head を変数として記録しておく.
head
ノード
A
データ部
B
ポインタ部
C
(NULLポインタって分かりますか?)
21
リストとは
 リストのノードはレコードであるから,C言語において
は構造体を用いて実装することができる.
struct tfield {
char name[20]; データ部
char tel[20];
struct tfield *pointer; ポインタ部
};
 このように,自分自身と同じ構造体へのポインタを持つ
構造体を 自己参照構造体 (self-referential structure)
という.
 教科書 p.228 のコードに誤植.
tfieled ではなく tfield 型です.
22
リストとは
 前述の通り,リスト構造では,必要に応じてノードを追
加・削除することができる.
 このためにはノードのためのメモリ領域を動的に確保・解
放しなくてはならない.
 この目的で使われるのがメモリの動的確保という方法であ
る.
 C言語ではメモリの動的確保のための関数が2つ用意され
ている.これらの間には微妙な違いがあるがほとんど同
じ.ここでは malloc() を使う.
void *malloc(size_t size);
void *calloc(size_t nmemb, size_t size);
23
リストとは
 関数 malloc() は,引数 size で与えられたバイト数
のメモリ領域をそのプログラムのために確保し, 確保さ
れた領域の先頭アドレスを返値として返す.
void *malloc(size_t size);
 例えば,int型の配列 (要素数10) の領域を確保して配
列 a[] を作るには,以下のようにする.
int *a;
a = (int *)malloc(sizeof(int)*10);
 ただし,sizeof(型名) は,その型のサイズ (バイト
数) を表す.
24
リストとは
 メモリ動的確保関数 malloc() を使って,ノードの動的
確保を以下の関数で記述できる.
struct tfield *talloc(void)
{
return (struct tfield *)malloc(sizeof(struct tfield));
}
tfield型ポインタへの型キャスト
tfield型のサイズ
 この関数を用いて,
struct tfield *p;
p = talloc();
p
初期化前では,何が入って
いるかは分からない
?
?
とすると,tfield型のデータ領域を新たに確保し,それ
へのポインタが p に代入される.
25
リストとは
 ノード内部のデータの参照方法は以下の通り.
 下記のように,ノードへのポインタ p によってノードが
指定されている場合,名前欄は p->name,電話番号欄は
p->tel,ポインタ欄は p->pointer により参照され
る.(*p).nameとかと同じ意味.
p
inoue
0123-45-6789
 一方, ノードがポインタでなく実体で記述されている場
合,つまり struct tfield node; として指定されて
いる場合は,それぞれの欄は node.name,node.tel,
node.pointer として参照される.
 何を言っているか分からない人は十分復習すること.
26
入力とは逆順なリストの作成
 では次に,リスト構造をどのように作るかを学ぶ.
 例として,キーボードから次々に名前・電話番号が入力さ
れたとき,入力されたのとは逆の順序のリストが作られる
アルゴリズムを取り上げる.
 リストの先頭位置を示すポインタを head とする.
 最初はリストの中身に何もない状態.このとき head は
NULLである.データ追加手順は以下のとおり:
 ノードを1つ新規生成し,p から指し示す.
 p が指すノードの中身を書き込む.
 p->pointer に head の内容をコピー, head から指す.
p = talloc();
head
p
p->pointer = head;
head = p;
p.230~231
Keisatsu
?
110
?
?
27
入力とは逆順なリストの作成
 次以降も同じ手順を繰り返すことで,逆順となるリストを
作成できる.
 (1) 新しいノードを生成し,そのアドレスを p に代入.
 (2) p のポインタ部を次のノード (head) に向ける.
 (3) head を新ノード p に向ける.
 つまり,先頭ノードの手前に次々挿入していく.
head
(2) と (3) の順序を
間違うと…?
head = p;
p = talloc();
(データ入力処理)
p
Keisatsu
110
p->pointer = head;
Kyuukyuu
119
28
入力とは逆順なリストの作成
 これを繰り返すと,入力されたのとは逆の順番のリスト構
造ができあがる.
head
A
B
head
C
B
A
D
C
B
head
A
29
入力とは逆順なリストの作成 (ソース)
 p.231.
 動的メモリ確保関数 malloc() を使うため stdlib.h
をインクルードする.
 main関数では,ローカル変数として,リストの先頭を
示すポインタ head,新規ノードのポインタ p を宣言,
head を NULL で初期化している.
 次の while文では,まず talloc() により新規ノード
を作成してそのアドレスを p に代入し,ユーザから入
力された2つの文字列を p->name,p->tel に代入.
 もしユーザから入力終了信号を受けた場合は, ループを
抜ける.そうでないときは, (1) 新ノードのポインタ部
に旧リストの先頭アドレス (head) を代入,(2) head
の指し示す先を新ノードに変更.
30
入力とは逆順なリストの作成 (ソース)
 while文を抜けた時点で,ユーザから入力されたデータ群
を逆順に保存したリスト構造ができている.
 その次の処理は,リスト構造の内容を表示する.
 最初にポインタ変数 p に head をコピー.これにより,p
は先頭ノードのアドレスを示す.
 次の while文は p が NULL でない限り繰り返す.
 まず,p が指し示すノードの内容を表示.
 次に,p = p->pointer とすることで,ポインタ変数 p
が,今表示したノードの次のノードのアドレスを指し示す
ようにする.つまり,ここで p は「視点」の役割.
 末尾のノードのポインタ部は NULL なので,これにより p
に NULL が入って while文を抜けることになる.
 次ページで説明
31
リストの内容表示について
 前のページの,リストの内容表示の部分を説明する.
 今回は,変数 p は「視点」として使う.
 p=head;
 while(p!=NULL){ ループを抜けて終了
 printf("%15s%15s\n",p->name,p->tel);
 p=p->pointer; p がもともと指し示していたノードのポイン
}
タ部の情報を p にコピーすると言うことは…
p
 視点 p はもう一つ次のノードを指し示す!
head 作ったリスト構造
表示
名3 電3
表示
名2 電2
表示
名1 電1
※ 講義スライドでは,いちいち変数 p から矢印を描くのは煩雑なので,
p が示しているノードの上に単に「p」と表記することが多いです.
32
入力順のリストの作成
 では,入力した順番にデータを記述したリストを作るには
どうすればよいか?
 新たに作成されたノードへのポインタを, 既存のリストの
末尾のノードのポインタ部に書き込むことで, 最後尾への
追加ができる.
 最初は head に直結するノードを1つ作成 (前処理).
 最後に作ったノードのアドレスを old として記録.
 次以降は old->pointer につなげる.
 最後に,末尾 (old) のポインタ部を NULL に (後処理).
head
?
p.232~233
old
A ?
old p
B ?
old
C ?
old
D ?
p=talloc(); としてデータ部を記入,
old->pointer=p; として,old=p; とする.
33
入力順のリストの作成 (ソース)
 p.232-233.
 「前処理」と書かれた部分で,まず head に直結する
ノードを作成している. 作成されたノードへのポインタ
を old に記録.
 次の while文では,新規ノード (p) を作成し,最後に
作った末尾ノード (old) のポインタ部に p へのポイン
タを記録して,old を新規ノード p へのポインタとす
る.これを入力がなくなるまで繰り返す.
 最後に作ったノードのポインタ部 (old->pointer) は
初期化されていないので, でたらめな数値が入っている
ことに注意.
 「後処理」の部分で,最後に作った末尾ノードから出る
ポインタを NULL にセット.
34
ダミー・ノード
 先程のソースコードでは, 最初に作成するノードだけは
head に直結させるため,処理の仕方が異なり,前処理と
して別個に作成していた.このため,while文のループと
は別に, データ入力部を前処理のためにもう一つ書かなく
てはならず, コードが煩雑.
 そこで,先頭ノードはダミー・ノードとし,データを含ま
ない「使わないノード」としてしまうと,先程のような
「別個の処理」を書き加える必要がなくなる.
head
?
old
? ?
ダミー・ノード
p.234~235
old
A ?
old
B ?
old
C ?
実際のデータ
35
ダミー・ノード (ソース)
 p.234-235.
 main関数の最初で,head=talloc(); として,headに
直結するダミー・ノードを作成し, そこへのポインタを
old としている.
 リスト作成手順は同様.
 最後にリスト内容の表示をしている部分では, 実データの
入ったノードは head ではなく head->pointer の位置
から始まるので,視点位置 p を p=head->pointer; と
初期化している.
36
リストへの挿入
 リスト構造は,データ列の途中の任意の位置においてデー
タを挿入・削除するのに適している.
 配列の場合,挿入・削除に伴って他のデータの移動が必要
となるが,リストではつなぎ換えだけでよい.
A
リストの場合
C
?
B
D
?
a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8]
配列の場合
A
C
D
B
p.236~240
37
リストへの挿入
 教科書の例では,既存のリストの中から,特定のデータ (キーデータ)
を逐次探索で探し出し,そのノードの次の位置にデータを挿入してい
る.
 例えば B のデータの次に C を挿入する場合:
 視点 p を head で初期化.p の位置のデータが B なら発見.そうで
ないなら視点を右へずらす (p=p->pointer;).
 発見したら p->pointer のところにノードを挿入.
head
p
ノード挿入手順:
p
A
B
n=talloc(); scanf("%s",n->name);
n
n->pointer=p->pointer;
p->pointer=n;
C
?
?
D
新規ノード n を作成
そのポインタ部を次の
ノードへ
p のポインタ部を新しい
ノードへ
38
リストへの挿入 (ソース)
 p.236-238.
 リストの先頭位置を示すポインタ head はグローバル変数として宣言
(struct tfield の型定義の部分).
 リストの生成は関数 genlist() にまとめられている.ここでは逆順
リストの生成を行っている.
 リスト内容表示は関数 displist() にまとめられている.
 ノードの挿入を行っているのは関数 link() である.引数は char型
へのポインタ key であり,キーデータの文字列に対応している.
(key というポインタ (アドレス) は,char型の配列の位置を表して
おり, その配列に文字コード列が入っている. )
key の実体 (配列) は main関数
のローカル変数である!
key
'I' 'n' 'o' 'u'
73 110 111 117
key[0] key[1] key[2] key[3]
39
リストへの挿入 (ソース)
 関数 link() の中で行っている処理を見てみよう.
 ローカル変数としてノードへのポインタ p と n がある.
 最初にノードを一つ作成し,それへのポインタを n に代
入.その中に入れるデータをキーボードから入力してい
る.
 次に p を視点として,head で初期化.
 while (p!=NULL) のループを回し,いま視点を置いてい
るノードの name欄が key と一致するかどうかを
strcmp() によって調べる.
 合致した場合は,視点位置 p の次にノード n を挿入.ま
ず,n のポインタを p の次 (p->pointer) に向ける.次
に,p のポインタを n に向ける.
 合致しない場合は視点を次へ送る (p=p->pointer).
40
リストへの挿入 (ソース)
 キーデータが存在しなかった場合, 最後のノードで合致し
ないことが分かった後,p=p->pointer; の処理によっ
て,p には NULL が入る.
 これにより while文の条件 (p が NULL でない限り続け
ろ) が満たされなくなり,while文を抜ける.
 すると, 関数の最後でエラーメッセージを表示して終了す
る.つまり,新しいノード n は作りっぱなしで放置され
てしまう.
 (見つかったノード p がリストの最後尾であった場合も,
新規ノード n のポインタを n->pointer=p->pointer
とするので,n->pointer はきちんと NULL になる.)
41
リストへの挿入 (キーデータ不在時)
 キーデータが見つからないときはリストの先頭へデータを
追加するように書き直したコードが p.238-240.
 単に,先程の whileループを抜けたところでエラーメッ
セージを出すだけではなく, そこに先頭への挿入手順を書
き加えればよい.
 先頭への挿入手順 (復習):(1) 新しいノード n のポイン
タ部を,既存リストの先頭位置 (それまでの head) に向
ける.(2) head を新しいノード n に向ける.
head
Keisatsu
110
n
Kyuukyuu
119
42
リストからの削除
 既に存在するリストからキーデータを逐次探索で探し出し,
そのノードをリストから削除する関数 del() を作る.
 ダミー・ノードを用いない場合は,先頭データを削除する
場合と先頭以外の場合とで手順が異なるため,場合分けが
必要である.
 視点2つ,p と old を用意し,双方 head で初期化.
 while(p!=NULL) のループで,視点 p を回す.
 キーデータが先頭 (A) だったとき (キーデータ発見の時点
で p==head)  head を p->pointer に向ける.
head
p,old
A
B
C
ほんとは不要になったノードの消去を!
p.243~247
43
リストからの削除
 キーデータが先頭ではなかったとき (例えば C)
 キーデータと一致するノード (p) の一つ手前のノード
(old) のポインタを,p の次のノード位置 (p->pointer)
に向けかえる.
 p, old は head で初期化していた.最初の位置 (A) では
発見しなかった場合,old=p としてから p を一つ進める
(p=p->pointer).
 発見したら old のポインタを p->pointer へ向ける.
old->pointer=p->pointer;
キーデータが C の時:
p, old
A
p, old
B
p
C
D
44
リストからの削除
 発見したノード (p) が最後尾だとした場合も今のアルゴリ
ズムできちんと処理可能.
ノードが複数の場合 (検索キー:C)
p,old
A
p,old
B
p
C
old->pointer = p->pointer; = NULL
ノードが一つだけしかなかった場合 (検索キー:A)
p,old
A
head = p->pointer; = NULL
45
リストからの削除 (ソース)
 p.243-244.削除の関数は del().
void del(char *key) キー文字列へのポインタを受け取っている
{
struct tfield *p,*old; p:視点,old:視点の一つ前のノード
p=old=head; 初期化
while (p!=NULL){
if (strcmp(key,p->name)==0){ 視点位置 p での照合
if (p==head) head=p->pointer;
else
old->pointer=p->pointer;
return; 抜ける
場合分けしてポインタ向けかえ
}
old=p;
p=p->pointer; 視点を右にずらす(old は一個遅れて追従)
}
エラーメッセージ
printf("キーデータが見つかりません\n");
}
46
リストからの削除 (ダミー・ノード版)
 先程の例では,削除されるべきノードが head に直結して
いる場合と,次以降である場合とで,「head を変更す
る」場合と「old->pointer を変更する」場合の場合分
けが必要となる.
 前の議論と同様, この場合もダミー・ノードが有用.
 これにより head 直後に対する場合分けがなくなるので,
「一つ手前の視点」である old を使う必要がなくなる.
 視点位置を p とするとその次を見る.「p の次のノー
ドの名前欄」は p->pointer->name,「次のノードのポ
インタ欄」は p->pointer->pointer で参照できる.
 ソースは p.246.p を head で初期化.p->pointer が
NULL でない限り続ける while ループを回す.
47
リストからの削除 (ダミー・ノード版)
 ループ内部では,p->pointer->name (pの一つ先のノー
ドのデータ部) と key との照合を行う.
 合致しない場合は,単に p を1つ進める.
 合致した場合,p->pointer を p->pointer->pointer
に向けかえる.つまり,p のポインタ部を p の二つ先につ
ないでしまう.
検索データが B の場合:
p
p
?
A
B
C
ダミー・ノード
p->pointer->name,つまり A を調べる  A≠B より,合致せず
ここで合致:p を2つ先 (p->pointer->pointer) につなぐ
48
ご不要となったメモリなど, ございましたら…
 教科書のやり方では,削除されたノードへのポインタを向け変
えてすましている.つまり,削除されたノードのメモリ領域
は,どこからも参照されないムダ領域として残る.
 ノードはもともと,malloc() によって動的に確保されたも
の.つまり, プログラムのためにコンピュータから分けても
らった領域である.
 教科書のやり方では,ノードを削除してもそのノードのための
メモリ領域はプログラムのために確保され続けるため,大量に
ノードの作成・削除を繰り返すと,無駄に確保され続ける無用
のメモリ領域 (ゴミ) が大量に生まれてしまう.
 コンピュータを「ゴミ屋敷」のようにしないためには,きちん
とゴミを処分しなくてはならない.即ち,不要になったメモリ
領域を開放し,コンピュータに返してやる.
49
ご不要となったメモリなど, ございましたら…
 malloc() を使って動的に確保したメモリを解放してシス
テム (コンピュータ) に返還する関数:free()
 例えば,ポインタ p で参照されるノードを解放するに
は,free(p); とすればよい.これだけで,スライド上に
大きな赤のバッテンで描いた消去処理が実現できる.
 例えば教科書 p.244 のコードでは,以下を追加すればよい.
if (strcmp(key,p->name)==0)
赤ペンなどで
if (p==head)
書き加えてください
{ head=p->pointer; free(p);}
else
{ old->pointer=p->pointer; free(p);}
return;
}
お行儀の良いプログラミングを!
50
いろいろなリスト構造
 ここまでで扱ってきたリスト構造:head から順にノード
を後ろへたどっていき,最後は NULL で終わる.このよう
なリストを 線形リスト (linear list) と呼ぶ.
 リストにはこれ以外の形式もある.
 循環リスト (circular list):線形リストの最後が NULL で
はなく,先頭ノードへのポインタとなっている.従って,
データの終端はない.
線形リスト
A
B
C
D
A
B
C
D
循環リスト
p.248~253
51
双方向リスト
 また,線形リストでは常に前から後ろへ進むことしかできな
い.「次のノードへのポインタ (順ポインタ)」はあるが,「手
前のノードへのポインタ (逆ポインタ)」はない.
 順ポインタとともに,逆ポインタも付け加えたリストがあり,
これを 双方向リスト (doubly-linked list) と呼ぶ.
 ノードの構造体にはこれら2つのポインタ部, すなわち 順方向
のポインタ right,逆方向のポインタ left が入る.
 循環型でない場合は,両端の外側ポインタは NULL とし,両端
ノードへのポインタ head と tail を用いる.
順ポインタ
head
A
left
tail
B
right
C
逆ポインタ
52
双方向リストの作成
 教科書の例では,末端である tail を起点にして「逆順リ
ストの作成」を行い,左端を head からつなぐことによ
り,逆順リストの生成手順を使って入力順リストを作って
いる.
 ソースは p.249-250.
head
p
?
p
?
A
?
?
p
?
B
?
?
tail
?
C
?
p=talloc(); p=talloc(); p=talloc();
p->left=tail; p->left=tail; p->left=tail;
tail=p;
tail=p;
tail=p;
p->right=head;p->right=head;p->right=head;
head=p;
head=p;
head=p;
p=p->left; = NULL
p=p->left;
p=p->left;
53
循環・双方向リスト
 双方向リストの両端のノード同士を相互に接続すれば, 循
環・双方向リストとなる (tail は不要となる).
head
tail
?
A
B
ダミー・ノード
 ダミー・ノードが以下の2点で重要な役割を果たす.
(1) 先頭ノードに対する特別な処理の排除
(2) データ探索における番兵の役割
 リストの作成時には,まず空きリストから初めて新しい
ノードを後ろへつなげていく. head
?
?
?
?
ダミー・ノード
54
循環・双方向リスト
 既存の循環・双方向リストに新たなノードを加える手順は
以下のようになる (順番に注意).
 (1) p (新ノード) の right を先頭ノード (head) へ.
 (2) p の left を最後尾ノード (head->left) へ.
 (3) 最後尾ノード (head->left) の right を p へ.
 (4) 先頭ノード (head) の left を p へ.
 教科書に誤植:p.252 一つめの A は,正しくは D .
初期状態
p
head
?
?
p
?
A
?
?
B
?
?
この順番を間違うと何が起こるか考えてみること
55
自己再編成探索 (試験範囲外)
 線形リストとして記録されたデータに対して逐次探索を行
うことを考える.
 逐次探索ではデータの先頭から1つ1つ調べていくので,
後ろにあるものほど探索に時間がかかる.
 ここで,一つの傾向を利用して探索効率を高めることを考
える:自己再編成探索 (self re-organizing search)
 傾向:一度使われたデータは再度使われる可能性が高い
 そこで,データが探索されるたびに,探索されたデータを
前の方に移動する.
 例)PC上で漢字変換をした場合,直前に変換した漢字が
今回の第1候補となる「学習機能」.
 データの挿入・削除を頻繁に行う  リストが適切
p.266~269
56
自己再編成探索 (探索データを先頭へ)
 まず逐次探索を行う.視点 p と視点の手前 old を用意し
て head で初期化.
 p の位置を照合.一致しないときは old を p のところへ
引っ張ってきてから p を進める (p=p->pointer).
 一致した場合,まず手前ノード (old) を p の次のノード
(p->pointer) へつなげる.
 次に,p のポインタを先頭ノード (head) へ向け,head
を p に向ける.
探索データが C の場合
head
p, old
p, old
A
B
p
C
D
57
自己再編成探索 (探索データを1つ前へ)
 探索データを1つだけ前に持って行くのは少し大変.
 探索データと1つ前との入れ替えのために, 「2つ前」の位
置も分かっていなければならない:ダミー・ノード,
old1, old2 の利用
 p で一致しないとき:old1 を old2 へ,old2 を p へ,
p を次へ.
 一致したら:(1) old1->pointer を q として保存,(2)
old1 のポインタを p に,(3) old2 のポインタを p の次
に,(4) p のポインタを q に.
探索データが C の場合
head old2,old1
?
p, old2
A
見つかったのがAまたはDのときは?
q
p
B
C
D
58