Presentation is loading. Please wait.

Presentation is loading. Please wait.

茨城大学 工学部 知能システム工学科 井上 康介 E2棟801号室

Similar presentations


Presentation on theme: "茨城大学 工学部 知能システム工学科 井上 康介 E2棟801号室"— Presentation transcript:

1 茨城大学 工学部 知能システム工学科 井上 康介 E2棟801号室
アルゴリズムとデータ構造 講義スライド 4 スタック・キュー リスト 茨城大学 工学部 知能システム工学科 井上 康介 E2棟801号室

2 第5章 データ構造 授業の最初で述べたとおり,大量・複雑なデータを扱う上では,データを構造化・組織化する必要がある.
どのようなデータ構造 (data structure) を採用するかによって,問題解決のアルゴリズムも異なってくる. つまり,アルゴリズムとデータ構造とは密接な関係にあり,よいデータ構造を選ぶことがよいプログラムを作ることにつながる. 特に重要なデータ構造として,リスト,ツリー,グラフが上げられる.ツリー (木) は第6章,グラフは第7章で扱う. この章では,スタック (stack: 棚),キュー (queue: 待ち行列),リスト (list) を説明する. p.213

3 データ構造とは 代表的なデータ構造として,以下のものが挙げられる: 表 (table: テーブル) 棚 (stack: スタック)
表については,ふつうの配列で扱うことができるので,ここでは扱わない. 良いデータ構造とは何かの基準は一概には言えないが,データの追加・削除・検索が効率よく行えること,および,複雑な構造が簡潔に表現できることなどがある. 表 (table: テーブル) 棚 (stack: スタック) 待ち行列 (queue: キュー) リスト (list) 木 (tree: ツリー) グラフ (graph) p.214~215

4 これから学ぶデータ構造 テーブル (ただの配列) スタック (後入れ先だし) キュー (先入れ先出し) a[0] a[1] a[2]

5 これから学ぶデータ構造 リスト (一本鎖) ツリー (分岐を含む) 次のデータへのポインタ 終端は NULL データ データ データ データ

6 これから学ぶデータ構造 グラフ・ネットワーク (ループも含む) グラフ (接続関係だけ) ネットワーク (区間に属性) 油縄子 油縄子
3分 6分 油縄子 油縄子 秦病院 Bulldog 秦病院 Bulldog 4分 7分 5分 カワチ 茨大 カワチ 茨大 10分 カスミ カスミ グラフ (接続関係だけ) ネットワーク (区間に属性)

7 スタック スタック (stack) は,一時的データ置き場 (buffer) として利用される代表的なデータ構造の一つである. データを棚に上から順に積んでいき,取り出すときも上から取り出す.データを放り込む操作を プッシュ (push),取り出す操作を ポップ (pop) という. このように,取り出せるのは最後に 入れたデータからである. このような方式を LIFO (Last In First Out: 後入れ先出し) と呼ぶ. スタックの実装は,1次元配列を 用いて行われる. push pop Data-2 Data-1 Data-0 スタック (棚) p.216~218 (前半)

8 スタック スタックは,データを格納する配列 と「現在いくつのデータが入っているか」を示す スタックポインタ から実現される.
配列を stack[],スタックポインタを sp とするとき,次のようになる. sp==4 での push は「あふれ」 sp==0 での pop は「空っぽ」 これらは エラー処理 プッシュ Data-B Data-C Data-D Data-A ポップ stack[3] stack[2] stack[1] stack[0] sp= 4 1 2 3

9 スタック (ソースコード) p.217. プリプロセッサでスタックのサイズ MaxSize を定義.
スタックの実体である配列 stack[] はグローバル変数として定義.スタックポインタ sp もグローバル変数として定義し,0 に初期化している (ほんとうは,グローバル変数は極力使わないことが望ましい). プッシュ関数 int push(int n); とポップ関数 int pop(int *n); では,int型の返値を返す. メイン関数では,while文でループさせてユーザからの文字入力を繰り返し受けている. int getchar(void) は,キーボードから文字を読み込み,int型に変換して返す関数.

10 スタック (ソースコード) while文のかっこ内でキーボードから1文字を読み込む.
読み込んだ文字が 'i' あるいは 'I' であるときは,ユーザからデータを入力して,それをスタックにプッシュする. スタックが既にいっぱいの場合, それ以上プッシュできない.このとき,関数 push() は -1 を返す. 読み込んだ文字が 'o' あるいは 'O' であるときは,スタックからデータを1つポップする. スタックが空の場合は取り出せないので関数 pop() は -1 を返す. プッシュもポップも,正常に実行した場合は返値は 0 となる.

11 スタック (ソースコード) さて, ポップを行ったとき,データが正しく取り出せれば 0 を,スタックが空のときは -1 を返す.このために「返値」という情報チャンネルは使われてしまう. では, 2つめの情報「ポップにより取り出したデータ」をどうやって呼出側に返せばよいだろうか? データを返すために,ここでは ポインタ渡し (参照渡し) という方法が使われている (通常は 値渡し). ポップしたデータを入れる変数は,main関数のローカル変数 n であり,main関数で pop() を呼び出すとき,引数としてその n へのポインタ (n のアドレス) を pop() に渡す. pop() 内では,もらったアドレスにデータを書き込む.

12 スタック (ソースコード) それぞれの関数を見ていこう.
push() では,最初にサイズあふれ (sp >= MaxSize) が起きていないかチェックしている. 起きていない場合は,配列の sp の位置に,値渡しで受け取ったデータ n を書き込み,sp を1つ増やす.正常終了なので,この場合は返値として 0 を返す. サイズあふれが起きていた場合,何もせずに -1 を返す. 値渡しでデータを受け取る際,main関数のローカル変数 n がコピーされ,これが関数に渡されている.従って,関数 push() の中で n に変更を加えたとしても,main関数内の n の値には影響しない.

13 スタック (ソースコード) 一方,pop() では,main関数内のローカル変数 n のポインタを受け取る.
関数 pop() 内においては,n はそのポインタ (アドレス) を示す変数であり, そのアドレスに書かれた情報を読み書きするときは *n としてアクセスする. 関数では,まずスタックが空 (sp==0) でないかチェックし,空でないときは sp を1だけ減らしてから 配列の sp番要素を *n に書き込み,正常終了を示す返値 0 を返す. 空の場合は何もせずに異常終了の返値 -1 を返す.

14 スタックからのポップ 関数 pop()の引数はポインタ渡しによる n.これは main()関数内のローカル変数 n への参照 である.
グローバル変数 関 数 sp main() int int pop(int *n) int *n 2 3 pop(&n) stack c n n 12 ? ’o’ ? ? -51 ? 25 -51

15 キュー スタックでは,データを入れたのとは逆の順番でデータを取り出していた (LIFO方式).このやり方は,まさに再帰呼出で紹介したような用途に向く.一方,例えば役所の窓口に人がどんどん来る場合を考えよう. LIFO方式では,最後に列に加わった人から処理を行うが,これは言うまでもなく不公平である.列に並んだ人々を,最初に並んだ人から順番に捌いていく必要がある.これを FIFO (First In First Out: 先入れ先出し) という. このモデルを キュー (queue: 待ち行列) という. 例)PCがインターネットで通信するとき,ネットからは続々とデータが流入するが,処理が追いつかない場合がある.このとき,データを一時的にキューに置いておき,手が空き次第,先に来たデータから順に処理する. p.222~224

16 キュー 実装は,スタックと同様,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

17 キュー (ソースコード) p.223. プリプロセッサで配列サイズ MaxSize を100としている.よって,容量は99となる.
キューの実体である配列 queue[],および先頭・末端位置 head,tail はグローバル変数として定義. キューにデータを入れる関数は int queuein(int n), データを取り出す関数は int queueout(int *n). main関数では,スタックの場合と同様,キーボードから1文字ずつの入力を繰り返し受け付け,それが 'i' または 'I' のときはデータを入れ,'o' または 'O' の時はデータを出している.

18 キュー (ソースコード) キューがいっぱいの時に更にデータを入れようとすると queuein() が -1 を返す,キューが空の時に取り出しをしようとすると queueout() が -1 を返す点はスタックと同じ.また,queueout() において,取り出したデータを返すために参照渡しを使っている点も同じ. キューにデータを入れる関数 queuein() においては,キューがいっぱいであるかどうかを,tail の次の位置が head と同じかどうかによって調べている. 「次の位置」を求めるために,(tail+1)%MaxSize という計算をしている.MaxSize が 100 とするとき,もしも tail が配列最後の位置 99 にあったとすれば,次の位置は先頭 0 であり,それはこの計算で求まる.

19 キュー (ソースコード) 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.

20 リストとは リスト (list) とは,データを鎖のようにつなぎ合わせて管理するデータ構造である.
各データは 次のデータを指すポインタ を持っている.このポインタで次々とデータをつないでいく (リンクする) ことによって,任意の数のデータを扱える. 一方,データを配列で扱う場合は,あらかじめ決められた配列サイズを超えるデータは格納できない. リストでは,データを新たに格納するたびに,そのデータのための記憶領域を新規に確保していく ので,サイズの制限がない (もちろんコンピュータ上のメモリサイズ以上は格納できないが). といっても理解しづらいので,絵的に理解しよう. p.228~229

21 リストとは リスト構造においてつながれる各データ要素 (ノード:node) は,レコード によって構成される.
ノードはデータを入れる データ部 の他に,次のノードのアドレスを指し示す ポインタ部 を含む. 最後尾のポインタ部は NULL としておく. プログラムからリスト構造にアクセスするため,リストの先頭へのポインタ head を変数として記録しておく. head ノード A B C データ部 ポインタ部

22 リストとは リストのノードはレコードであるから,C言語においては構造体を用いて実装することができる.
このように,自分自身と同じ構造体へのポインタを持つ構造体を 自己参照構造体 (self-referential structure) という. 教科書 p.228 のコードに 誤植.        tfieled ではなく tfield 型です. struct tfield { char name[20]; char tel[20]; struct tfield *pointer; }; struct tfield { char name[20]; char tel[20]; struct tfield *pointer; }; データ部 ポインタ部

23 リストとは 前述の通り,リスト構造では,必要に応じてノードを追加・削除することができる.
このためにはノードのためのメモリ領域を 動的に確保・ 解放 しなくてはならない. この目的で使われるのが メモリの動的確保 という方法である. C言語ではメモリの動的確保のための関数が2つ用意されている.これらの間には微妙な違いがあるがほとんど同じ.ここでは malloc() を使う. void *malloc(size_t size); void *calloc(size_t nmemb, size_t size);

24 リストとは 関数 malloc() は,引数 size で与えられたバイト数のメモリ領域をそのプログラムのために確保し, 確保された領域の先頭アドレスを返値として返す. 例えば,int型の配列 (要素数10) の領域を確保して配列 a[] を作るには,以下のようにする. ただし,sizeof(型名) は,その型のサイズ (バイト数) を表す. void *malloc(size_t size); int *a; a = (int *)malloc(sizeof(int)*10);

25 リストとは メモリ動的確保関数 malloc() を使って,ノードの動的確保を以下の関数で記述できる. この関数を用いて,
とすると,tfield 型のデータ領域を新たに確保し, それへのポインタが p に代入される. struct tfield *talloc(void) { return (struct tfield *)malloc(sizeof(struct tfield)); } tfield型ポインタへの型キャスト tfield型のサイズ 初期化前では,何が入っているかは分からない p struct tfield *p; p = talloc(); ?

26 リストとは ノード内部のデータの参照方法は以下の通り.
下記のように,ノードへのポインタ p によってノードが指定されている場合,名前欄は p->name,電話番号欄は p->tel,ポインタ欄は p->pointer により参照される.(*p).name とかと同じ意味. 一方, ノードがポインタでなく実体で記述されている 場合,つまり struct tfield node; として指定されている場合は,それぞれの欄は node.name,node.tel,node.pointer として参照される. 何を言っているか分からない人は十分復習すること. p p->name p->tel p->pointer inoue

27 入力とは逆順なリストの作成 教科書 p.230 の例題 34 は,ユーザが名前と電話番号を次々に入力したとき,そのデータが 入力されたのと逆順 で並ぶような リストを作成する手順である. リストの先頭ノードを示すポインタを head とする. 初期状態は空のリストであり,このとき head は NULL. データ追加手順は以下の通り. (1) ノード 1つを新規生成し,p で指し示す:p = talloc(); (2) p が指すノードにデータを書き込む (3) p が指すノードのポインタ部に head をコピー p->pointer = head; (4) head から p が指すノードを指す: head = p; p copy head Sato copy

28 入力とは逆順なリストの作成 次以降も同じ手順を繰り返すことで,入力とは逆順のリストを生成できる.
(1) ノード 1つを新規生成し,p で指し示す:p = talloc(); (2) p が指すノードにデータを書き込む (3) p が指すノードのポインタ部に head をコピー: p->pointer = head; (4) head から p が指すノードを指し示す:head = p; 手順 (3) と (4) の順序を取り違えるとまずいことになる. p p head Sato Ota head Ota Sato ?

29 入力とは逆順なリストの作成 これを繰り返すと,入力されたのとは逆の順番のリスト構造ができあがる. head B A head C A B

30 入力とは逆順なリストの作成 (ソース) p.231.
動的メモリ確保関数 malloc() を使うため stdlib.h をインクルードする. main関数では,ローカル変数として,リストの先頭を示すポインタ head,新規ノードのポインタ p を宣言,head を NULL で初期化している. 次の while文では,まず talloc() により新規ノードを作成してそのアドレスを p に代入し,ユーザから入力された2つの文字列を p->name,p->tel に代入. もしユーザから入力終了信号を受けた場合は, ループを抜ける.そうでないときは, (1) 新ノードのポインタ部に旧リストの先頭アドレス (head) を代入,(2) head の指し示す先を新ノードに変更.

31 入力とは逆順なリストの作成 (ソース) while文を抜けた時点で,ユーザから入力されたデータ群を逆順に保存したリスト構造ができている.
その次の処理は,リスト構造の内容を表示する. 最初にポインタ変数 p に head をコピー.これにより,p は先頭ノードのアドレスを示す. 次の while文は p が NULL でない限り繰り返す. まず,p が指し示すノードの内容を表示. 次に,p = p->pointer とすることで,ポインタ変数 p が,今表示したノードの次のノードのアドレスを指し示すようにする.つまり,ここで p は「視点」の役割. 末尾のノードのポインタ部は NULL なので,これにより p に NULL が入って while文を抜けることになる.  次ページで説明

32 リストの内容表示について 前のページの,リストの内容表示の部分を説明する. 今回は,変数 p は「視点」として使う. 作ったリスト構造 名3
p=head; while(p!=NULL){ printf("%15s%15s\n",p->name,p->tel); p=p->pointer; } ループを抜けて終了 p がもともと指し示していたノードのポイン タ部の情報を p にコピーすると言うことは… p  視点 p はもう一つ次のノードを指し示す! 作ったリスト構造 名3 電3 表示 名2 電2 表示 名1 電1 表示 名3 電3 名2 電2 名1 電1 head ※ 講義スライドでは,いちいち変数 p から矢印を描くのは煩雑なので,   p が示しているノードの上に単に「p」と表記することが多いです.

33 ノードへのポインタの表示方法 今後,先ほどのポインタ変数 p のように,様々なノードを指し示すポインタについては,上の図のようにいちいち矢印で指し示さず,下の図のようにノードに “p” と示すだけにする.p が指し示すノードを「ノード p」などと書く. p=p->pointer; p head Iida Ota Sato p p head Iida Ota Sato

34 入力順のリストの作成 ここまでの話は,入力順と逆順のリストの作成.では, 入力順のリストはどうすれば作れるか?
 途中まで作ったリストの最後尾のさらにあとに新規   ノードを追加していけばよい. 仕上げとして,最後に作ったノードのポインタ部を NULL にする. head A B head A B C D

35 入力順のリストの作成 面倒な問題:ノード作成手順が「最初のノード」と「次以降のノード」で異なる. 最初のノードへのポインタ:head から
次以降のノードへのポインタ:既存リスト最後尾から 最後尾ノードへのポインタを old とすると… head ノード作成: head=talloc(); A データ記入: head->name, head->tel へ head A B old p コードが異なる C ノード作成: p=talloc(); old->pointer=p; データ記入: p->name, p->tel へ

36 入力順のリストの作成 (ソースコード) 教科書 p.232~233 のソースコード (Dr34_1) 前半部 ムダ 
head=talloc(); scanf("%s %s",head->name,head->tel); old=head; while (p=talloc(),scanf("%s %s",p->name,p->tel)!=EOF){ old->pointer=p; old=p; } old->pointer=NULL; 前処理 ( 最後尾ノードをポインタ old でポイントしておく) EOF 後処理 ムダ head old p old p old p A B C

37 ダミー・ノード 先ほどのソース・コードのデータ入力部は…
データ入力処理が scanf() 一つだけならいいが,より複雑 な処理の場合,これは避けたい. これを避けるための一つの方法が ダミー・ノード である.  先頭ノードはデータを格納せず,ダミーとする. head=talloc(); scanf("%s %s",head->name,head->tel); old=head; while (p=talloc(),scanf("%s %s",p->name,p->tel)!=EOF){ old->pointer=p; old=p; } old->pointer=NULL;  前処理での入力処理  ループ内での入力処理

38 ダミー・ノード (ソースコード) 教科書 p.234~235 のソースコード (Dr34_2) 前半部 head=talloc();
scanf("%s %s",head->name,head->tel); old=head; while (p=talloc(),scanf("%s %s",p->name,p->tel)!=EOF){ old->pointer=p; old=p; } old->pointer=NULL;  入力処理はこれに統一 head old p old p old p old A B C ダミー・ノード 実際のデータ

39 ダミー・ノード (ソースコード) 教科書 p.234~235 のソースコード (Dr34_2) 後半部 (リスト内容の表示) 
p=head->pointer; while (p!=NULL){ printf("%15s%15s\n",p->name,p->tel); p=p->pointer; } head->pointer ( 視点は第2ノードからスタートする) p: NULL head p p p A A B B C C ダミー・ノード 実際のデータ

40 リストへの挿入 リスト構造は,データ列の途中の任意の位置においてデータを挿入・削除するのに適している.
配列の場合,挿入・削除に伴って他のデータの移動が必要となるが,リストではつなぎ換えだけでよい. リストの場合 A C D B ? 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

41 リストへの挿入 教科書の例では,既存のリストの中から,特定のデータ (キーデータ) を逐次探索で探し出し,そのノードの次の位置にデータを挿入する. 例えば B のデータの次に C を挿入する場合: 視点 p を head で初期化.p の位置のデータが B なら発見.そうでないなら視点を右へずらす (p=p->pointer;). 発見したら p->pointer のところにノードを挿入. ノード挿入手順: A B D head p p - 新規ノード n を作成 - そのポインタ部を次の ノードへ n=talloc(); scanf("%s",n->name); n n->pointer=p->pointer; - p のポインタ部を新しい ノードへ p->pointer=n; ? C

42 リストへの挿入 (ソース) p . リストの先頭位置を示すポインタ head はグローバル変数として宣言 (struct tfield の型定義の部分). リストの生成は関数 genlist()にまとめられている.ここでは逆順リストの生成を行っている. リスト内容表示は関数 displist()にまとめられている. ノードの挿入を行っているのは関数 link()である.引数は char 型へのポインタ key であり,キーデータの文字列に対応している.(key というポインタ (アドレス) は,char 型の配列の位置を表しており, その配列に文字コード列が入っている. ) key 'I' 'n' 'o' 'u' key の実体 (配列) は main関数のローカル変数である! 73 110 111 117 key[0] key[1] key[2] key[3]

43 リストへの挿入 (ソース) 関数 link() の中で行っている処理を見てみよう.
ローカル変数としてノードへのポインタ p と n がある. 最初にノードを一つ作成し,それへのポインタを n に代入.その中に入れるデータをキーボードから入力している. 次に p を視点として,head で初期化. while (p!=NULL) のループを回し,いま視点を置いているノードの name欄が key と一致するかどうかを strcmp() によって調べる. 合致した場合は,視点位置 p の次にノード n を挿入.まず,n のポインタを p の次 (p->pointer) に向ける.次に,p のポインタを n に向ける. 合致しない場合は視点を次へ送る (p=p->pointer).

44 リストへの挿入 (ソース) キーデータが存在しなかった場合, 最後のノードで合致しないことが分かった後,p=p->pointer; の処理によって,p には NULL が入る. これにより while文の条件 (p が NULL でない限り続けろ) が満たされなくなり,while文を抜ける. すると, 関数の最後でエラーメッセージを表示して終了する.つまり,新しいノード n は作りっぱなしで放置されてしまう. (見つかったノード p がリストの最後尾であった場合も,新規ノード n のポインタを n->pointer=p->pointer とするので,n->pointer はきちんと NULL になる.)

45 リストへの挿入 (キーデータ不在時) キーデータが見つからないときはリストの先頭へデータを追加するように書き直したコードが p . 単に,先程の whileループを抜けたところでエラーメッセージを出すだけではなく, そこに先頭への挿入手順を書き加えればよい. 先頭への挿入手順 (復習):(1) 新しいノード n のポインタ部を,既存リストの先頭位置 (それまでの head) に向ける.(2) head を新しいノード n に向ける. head Keisatsu 110 Kyuukyuu 119 n

46 リストからの削除 既に存在するリストからキーデータを逐次探索で探し出し, そのノードをリストから削除する関数 del()を作る.
ダミー・ノードを用いない場合は,先頭データを削除する場合 と 先頭以外の場合 とで手順が異なるため,場合分けが必要である. 視点2つ,p と old を用意し,双方 head で初期化. while(p!=NULL) のループで,視点 p を回す. キーデータが先頭 (A) だったとき (キーデータ発見の時点で p==head) head を p->pointer に向ける. head p,old B C A ほんとは不要になったノードの消去を! p.243~247

47 リストからの削除 キーデータが先頭ではなかったとき (例えば C)
 キーデータと一致するノード (p) の一つ手前のノード (old) のポインタを,p の次のノード位置 (p->pointer) に向けかえる. p, old は head で初期化していた.最初の位置 (A) では発見しなかった場合,old = p としてから p を一つ進める (p = p->pointer). 発見したら old のポインタを p->pointer へ向ける. キーデータが C の時: old->pointer = p->pointer; p, old p , old p B D A C

48 リストからの削除 発見したノード (p) が最後尾だとした場合も今のアルゴリズムできちんと処理可能. ノードが複数の場合 (検索キー:C)
old p , old p A B C old->pointer = p->pointer; = NULL ノードが一つだけしかなかった場合 (検索キー:A) p, old A head = p->pointer; = NULL

49 リストからの削除 (ソース) p.243-244.削除の関数は del(). void del(char *key) {
struct tfield *p,*old; p=old=head; while (p!=NULL){ if (strcmp(key,p->name)==0){ if (p==head) head=p->pointer; else old->pointer=p->pointer; return; } old=p; p=p->pointer; printf("キーデータが見つかりません\n"); キー文字列へのポインタを受け取っている p:視点,old:視点の一つ前のノード 初期化 視点位置 p での照合 抜ける 場合分けしてポインタ向けかえ 視点を右にずらす(old は一個遅れて追従) エラーメッセージ

50 リストからの削除 (ダミー・ノード版) 先程の例では,削除されるべきノードが head に直結している場合と,次以降である場合とで,「head を変更する」場合と「old->pointer を変更する」場合の場合分けが必要となる. 前の議論と同様, この場合もダミー・ノードが有用. これにより head 直後に対する場合分けがなくなるので,「一つ手前の視点」である old を使う必要がなくなる.  視点位置を p とするとその次を見る.「p の次のノードの名前欄」は p->pointer->name,「次のノードのポインタ欄」は p->pointer->pointer で参照できる. ソースは p.246.p を head で初期化.p->pointer が NULL でない限り続ける while ループを回す.

51 リストからの削除 (ダミー・ノード版) ループ内部では,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) につなぐ

52 メモリ領域の開放 教科書のやり方では,削除されたノードへのポインタを向け変えてすましている.つまり,削除されたノードのメモリ領域は,どこからも参照されないムダ領域として残る. ノードはもともと,malloc() によって動的に確保されたもの.つまり, プログラムのためにコンピュータから分けてもらった領域である. 教科書のやり方では,ノードを削除してもそのノードのためのメモリ領域はプログラムのために確保され続けるため,大量にノードの作成・削除を繰り返すと,無駄に確保され続ける無用のメモリ領域 (ゴミ) が大量に生まれてしまう. コンピュータを「ゴミ屋敷」のようにしないためには,きちんとゴミを処分しなくてはならない.即ち,不要になったメモリ領域を開放し,コンピュータに返してやる.

53 メモリ領域の開放 malloc() を使って動的に確保したメモリを解放してシステム (コンピュータ) に返還する関数:free()
例えば,ポインタ p で参照されるノードを解放するには,free(p); とすればよい.これだけで,スライド上に大きな赤のバッテンで描いた消去処理が実現できる. 例えば教科書 p.244 のコードでは,以下を追加すればよい. if (strcmp(key,p->name)==0) if (p==head) head=p->pointer; else old->pointer=p->pointer; return; } 赤ペンなどで 書き加えてください { free(p);} { free(p);} お行儀の良いプログラミングを!

54 いろいろなリスト構造 ここまでで扱ってきたリスト構造:head から順にノードを後ろへたどっていき,最後は NULL で終わる.このようなリストを 線形リスト (linear list) と呼ぶ. リストにはこれ以外の形式もある. 循環リスト (circular list):線形リストの最後が NULL ではなく,先頭ノードへのポインタとなっている.従って,データの終端はない. 線形リスト A B C D 循環リスト A B C D p.248~253

55 双方向リスト また,線形リストでは常に前から後ろへ進むことしかできない.「次のノードへのポインタ (順ポインタ)」はあるが,「手前のノードへのポインタ (逆ポインタ)」はない. 順ポインタとともに,逆ポインタも付け加えたリストがあり,これを 双方向リスト (doubly-linked list) と呼ぶ. ノードの構造体にはこれら2つのポインタ部, すなわち 順方向のポインタ right,逆方向のポインタ left が入る. 循環型でない場合は,両端の外側ポインタは NULL とし,両端ノードへのポインタ head と tail を用いる. 順ポインタ head tail A B C 逆ポインタ left right

56 双方向リストの作成 教科書の例では,末端である tail を起点にして「逆順リストの作成」を行い,左端を head からつなぐことにより,逆順リストの生成手順を使って入力順リストを作っている. ソースは p . p p p head tail ? A ? B ? 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;

57 循環・双方向リスト 双方向リストの両端のノード同士を相互に接続すれば, 循環・双方向リストとなる (tail は不要となる).
ダミー・ノードが以下の2点で重要な役割を果たす. (1) 先頭ノードに対する特別な処理の排除 (2) データ探索における番兵の役割 リストの作成時には,まず空きリストから初めて新しいノードを後ろへつなげていく. head tail ? A B ダミー・ノード head ? ? ダミー・ノード

58 循環・双方向リスト 既存の循環・双方向リストに新たなノードを加える手順は以下のようになる (順番に注意).
(1) p (新ノード) の right を先頭ノード (head) へ. (2) p の left を最後尾ノード (head->left) へ. (3) 最後尾ノード (head->left) の right を p へ. (4) 先頭ノード (head) の left を p へ. 教科書に誤植:p.252 一つめの A は,正しくは D . 初期状態 p p head ? ? A ? B この順番を間違うと何が起こるか考えてみること

59 自己再編成探索 (試験範囲外) 線形リストとして記録されたデータに対して逐次探索を行うことを考える.
逐次探索ではデータの先頭から1つ1つ調べていくので,後ろにあるものほど探索に時間がかかる. ここで,一つの傾向を利用して探索効率を高めることを考える:自己再編成探索 (self re-organizing search) 傾向:一度使われたデータは再度使われる可能性が高い そこで,データが探索されるたびに,探索されたデータを前の方に移動する. 例)PC上で漢字変換をした場合,直前に変換した漢字が今回の第1候補となる「学習機能」. データの挿入・削除を頻繁に行う  リストが適切 p.266~269

60 自己再編成探索 (探索データを先頭へ) まず逐次探索を行う.視点 p と視点の手前 old を用意して head で初期化.
p の位置を照合.一致しないときは old を p のところへ引っ張ってきてから p を進める (p=p->pointer). 一致した場合,まず手前ノード (old) を p の次のノード (p->pointer) へつなげる. 次に,p のポインタを先頭ノード (head) へ向け,head を p に向ける. 探索データが C の場合 head p, old p , old p A B C D

61 自己再編成探索 (探索データを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 の場合 見つかったのがAまたはDのときは? q head old2 , old1 p , old2 p ? A B C D


Download ppt "茨城大学 工学部 知能システム工学科 井上 康介 E2棟801号室"

Similar presentations


Ads by Google