12月15日

予定 (I)
10/6: 第1回:イントロダクション
10/13: 第2回:アルゴリズムの解析
10/20: 第3回:関数の増加
10/27: 第4回:漸化式,確率論的解析
11/10:第5回:乱択アルゴリズム
11/17:第6回:ヒープソート
11/24: 第7回:クイックソート
12/1: 第一回小テスト(範囲は第5回:乱択アル
ゴリズムまで)
337
データ構造と
アルゴリズムⅠ
第9回
予定 (II)
前回の小テスト
12/8: 第8回:線形時間ソーティング
12/15:第9回:基本データ構造
12/22:第二回小テスト(範囲は第6回から
第9回まで)
1/5:第10回:ハッシュ表
1/12: 金曜日の講義日
1/19:第11回:2分探索木
1/26: 休講
2/9: 定期試験
問題
解答
338
339
集合を扱うデータ構造
データ構造
• データをどのような形式で格納し,どのように
利用するかを与える
• 現実のプログラミングでは,アルゴリズムの
選択よりもデータ構造の選択が重要であるこ
とが多い
⇒ データ構造が決まれば用いるべきアルゴリズム
も決まる
340
•
•
•
•
集合:数学,計算機科学において基本的
動的集合:要素が追加/削除/変更される
集合に対して行う操作によってデータ構造を変える
行いたい操作によって最適なデータ構造が決まる
行う操作
データ構造
挿入,削除,
存在判定
挿入
最小要素の取出し
辞書
プライオリティー
キュー
341
1
動的集合に関する操作
動的集合の基本
1. 集合に関する情報を返す質問 (query)
•
• 各要素はオブジェクトとして表現される
• オブジェクトはキーと付属データからなる
• 集合の操作で扱うフィールドがあってもよい
•
– 他のオブジェクトへのポインタなど
•
• キーは全順序を持つとする場合もある
•
•
342
動的集合に関する操作
10.1 スタックとキュー
2. 集合を変える修正操作 (modifying operation)
•
•
•
•
SEARCH(k): key[x] = k である S の要素 x へのポイ
ンタを返す.存在しなければ NIL.
MINIMUM(): 全順序集合 T において,最小のキーを
持つ要素を返す
MAXIMUM(): 全順序集合 T において,最大のキーを
持つ要素を返す
SUCCESSOR(x):全順序集合 T においてキーが x
のキーの次に大きな要素を返す.x が最大なら NIL.
PREDECESSOR(x): キーが x のキーの次に小さな
343
要素を返す.x が最小なら NIL.
INSERT(x): 集合 S に要素 x を加える.
DELETE(x): x へのポインタが与えられたとき,S
から x を取り除く.
SUCCESSOR,PREDECESSORは同じキー
が複数ある集合にも拡張される
集合操作を実行するのにかかる時間は集合の
サイズで測る
• 動的集合,挿入と削除をサポートする
• スタック (stack) では,DELETEでは最後に
挿入された要素が取り除かれる
– 後入れ先出し (last-in, first-out; LIFO) という
• キュー (queue) では,最初に挿入された要素
が取り除かれる
– 先入れ先出し (first-in, first-out; FIFO) という
344
配列によるスタックの実装
スタック (Stack)
• INSERT, DELETEの代わりにPUSH, POPと
呼ぶ
– PUSH(x): スタックに要素 x を加える.
– POP(): スタックから最後に PUSH された要素を
削除し, その要素を返す
PUSH(6)
PUSH(9)
PUSH(15)
PUSH(2)
POP()
15
692
9
2
6
15
345
• 最大 MAX 要素を格納できるスタックを実装
• スタックを表すオブジェクト
– top: 最後に挿入された要素の格納場所(= 要素数)
– S: 要素を格納するサイズMAXの配列(へのポインタ)
• 要素は S[1..top] に格納される class STACK {
– S[1]: スタックの底
– S[top]: スタックの最上部
– top  MAX
346
public:
int top;
int MAX
data *S;
};
347
2
実装例
• PUSH(x)
その他の関数
• POP()
– topを1増やし,x を配
列に入れる
– topがMAXを超えたら
エラー
– O(1) 時間
• STACK_EMPTY()
– スタックが空ならエラー
– サイズを1減らし,最上部
の要素を返す
– O(1) 時間
void PUSH(data x)
{
if (top == MAX) {
cout << "オーバーフロー";
exit(1);
}
top = top + 1;
S[top] = x;
}
data POP()
{
if (STACK_EMPTY()) {
cout << "アンダーフロー";
exit(1);
}
top = top - 1;
return S[top + 1];
348
}
• 初期化
– スタックが空なら1を返す
– O(1) 時間
int STACK_EMPTY()
{
if (top == 0) return 1;
else return 0;
}
stack (int size) {
TOP=0;
MAX=size;
S = new data[size];}
349
課題
答え
• n要素の配列A[1,…,n]を用いて,二本のス
タックを実装したい
• 二つのスタックの要素の合計がnを超えない
限り,どちらもオーバフローしないようにする
にはどうしたらよいか?
• 一つのスタックは最初から詰めて,もう一つ
のスタックは後ろから逆に詰める
• topが同じになったらオーバーフロー
350
配列によるキューの実装
キュー (Queue)
• INSERT, DELETEの代わりにENQUEUE,
DEQUEUEと呼ぶ
• 人の並んだ列と同じ
– ENQUEUEでは列の最後に追加
– DEQUEUEでは列の先頭を取り出す
DEQUEUE()
ENQUEUE(x)
Q
15
6
2
351
• 最大 MAX1 要素を格納できるキュー
class QUEUE
• キューを表すオブジェクト
– Q: 要素を格納する配列
– head: キューの先頭の位置
– tail: 次に追加される位置
1
2
3
4
5
6
7
8
15 6
9
352
head
{
public:
int head;
int tail;
data *Q;
};
9 10 11 12
9
8
4
tail
353
3
実装例
環状バッファ (Ring Buffer)
• 配列 Q の右端と左端はつながって輪になっている
と考える
• Q[MAX] の右は Q[1] だとみなす
• 要素は Q[head..MAX] Q[1..tail1] に格納される
• DEQUEUEの際に全要素を左にずらす必要がな
い
1
2
3
4
5
3
6
7
8
9 10 11 12
15 6
tail
9
8
4 17
head
354
10.4 根付き木の表現
• ENQUEUE(x)
• DEQUEUE()
– x をQ[tail]に入れる
– tailを1増やす
– O(1) 時間
– Q[head]を取り出す
– headを1増やす
– O(1) 時間
void ENQUEUE(data x)
{
Q[tail] = x;
if (tail == MAX)
tail = 1;
else tail = tail + 1;
}
data DEQUEUE()
{
data x;
x = Q[head];
if (head == MAX)
head = 1;
else head = head + 1;
注: オーバーフロー, アンダーフロー return x;
}
処理は省略してある
355
2分木
• (二分)根付き木をオブジェクトを用いて表現する
• 各節点はフィールド p, left, right, keyを持つ
– p: 親へのポインタ,NILなら根
– left: 左の子へのポインタ,NILなら子を持たない
– right: 右の子へのポインタ,NILなら子を持たない
root(T)
• root(T): T の根を指す.NILなら木は空
p
root(T)
left right
356
357
左-子, 右-兄弟表現
k分木
(Left-Child Right-Sibling Representation)
• 子供を最大 k 個持つ木の表現
– left, rightの代わりに child1, child2,..., childk とする
– 子供の数が一定でないと記憶領域を無駄にする
– p(x): 親へのポインタ
– left-child(x): x の子で最も左にあるものへのポインタ
– right-sibling(x): x の右の兄弟へのポインタ
…
child1
child2
• k 分木を2分木で表現する方法
• 任意の n 節点の根付き木を O(n) 領域で表現可
能
childk
358
• x が子を持たないとき left-child(x) = NIL
• x がその親の右端の子のときright-sibling(x) =
359
NIL
4
セント・ペテルスブルグの逆説
root(T)
360
二分木で表すと?
セント・ペテルスブルグのパラドックス
1/2
• なぜ期待値が無限大のクジに,10円払うのも
いやなのか?
1/4
2円
1/8
4円
…
8円 1/16
16円
1/2k
2k 円
• 以下のようなクジを考える.
– コインを投げる.
– 表が出たら終わり.2円もらえる.
– 裏が出たらならもう一回コインを投げる.
– 表が出たら終わり,4円もらえる.
– 裏が出たならもう一回コインを投げる.
– 表が出たら終わり,8円もらえる.
– 以下繰り返し,k回目に初めて表が出たら 2k円
もらえる
• このクジに参加するのに,いくらまでなら払っても
361
良いか?
– リスクに対する態度:人間はリスクを避けたいと
思う --- 期待値が小さくても,確実な方を好む
• 一億円確実にもらえるのと,コインを投げて表なら二
億円,裏なら0円のクジの価値は同じ?
…
362
リスクに対する態度
• 自動車保険
– 保険に入らない場合:ほとんどの場合にコスト0, 非
常に低い確率pで,事故を起こして1億円払うという
クジを引く
– 保険に入る場合:確実に5万円損をする代わりに,
上のクジを引かなくてよい
– p×一億円 < 5万円
– 期待値だけを考えるなら,保険に入らない方が良い
– 通常,人はリスク回避的,しかし,保険に入る人が
宝くじを買うこともある (100円払って,期待利得が
50円未満のものを買う)
364
– 金額が倍になっても,うれしさは倍にならない:二
万円は一万円の倍うれしいが,二兆円は一兆円
の倍ほどはうれしくない
– このクジは現実には成立し得ない(地球全体の
富の総額を超える賞金を約束している)
363
木探索アルゴリズム
• ルート(根)ノードから,ある条件を満たすノー
ドへの経路を見つける
• 例:迷路の探索,宣教師と人喰い人種,8puzzle, カーナビの経路探索, VLSIのレイア
ウト, 移動ロボットの経路プランニング
3
2
1 8 4
7 6 5
1 2 3
4 5 6
7 8
365
5
幅優先探索
一般的な木探索アルゴリズム
OPENをルートノードのみからなるリストに設定
loop do
if OPEN が空のリスト
then return no-solution
else OPEN中の最初のノード n をOPENから取り除く
if nがゴールノード
then return ルートからnへの経路
else n を展開して,子ノードをすべて,
適当な基準でOPENに加える
end if
end if
end do
• ルートノードがまず展開され,つぎにルートノードに
よって生成されたすべてのノードが展開され,そして
それらの後続ノードが展開される,というように続く.
• 一般に,探索木における深さdのすべてのノードが,
深さd+1のノードの前に展開される.
• 一般アルゴリズムで,OPENをキューを用いて実装
し,展開した子ノードをキューの最後に加える
366
幅優先探索の性質
深さ優先探索
– 幅優先探索で得られる解は最適
• 木が無限でも,有限の距離の解があるなら完全
– どの状態もb個の新しい状態に展開されるものとする(分岐
度=b).この問題に対する解が,経路長dを持つと仮定する.
– 最悪の場合,レベルdの最後のノードが解
– 展開されるノード数は, 1+b+b2+b3+…+bd=O(bd)
– b=10,
1000ノード/秒
100バイト/ノード
とする.
8 10^8 31 時間
10 10^10 128 日
12 10^12 35 年
14 10^14 3500 年
367
11 ギガバイト
1 テラバイト
111 テラバイト
11,111 テラバイト
• 木の最も深いレベルのノードの一つをいつも
展開する.
• 探索が行き止まりになる (子ノードがない) と
きに限り,探索は後戻りし,より浅いレベルの
ノードを展開する.
• 一般アルゴリズムで,OPENをスタックを用い
て実装し,子ノードをスタックの先頭に加える
368
深さ優先探索
369
深さ優先探索の性質
• メモリの使用量が少ない
– ある部分木に関する探索が終了して初めて,
異なる部分木の探索を行う
• 最適解を得ることは保証できない
• 木が無限の場合,探索が終了しないことが
ある
• 木の深さが限定される場合に相性が良い
370
371
6
最良優先探索(近視眼的)
最良優先探索(A*探索)
• OPENを常にソートして,最良のものを優先し
て展開する
• 目的までの距離の推定値が最小のものを優先
• 推定値の求め方(迷路の場合):障害物が存在
しないと仮定した場合の距離を求める
• この推定値は,必ず楽観的な値になっており,
真の値より小さい(か等しい)
• 最適解(最短距離のルート)を求めることは保
証できない
• OPENを常にソートして,最良のものを優先し
て展開する
• 最良のもの=最短経路の候補となる可能性
が高いもの
• ルートからの距離+目的までの距離の推定
値が小さいものを優先
• 推定値が楽観的な場合,最適解を得ることが
保証される
372
373
リストの種類
10. 2 連結リスト
リストの利点:
• 何個の要素が入るか あらかじめ予想できな
い場合でも使える
• 最悪の場合を予想して,大きめに領域を取っ
ておく必要がない
• 再帰的な構造をしているので,再帰的なプロ
グラムと相性が良い
• 一方向 (singly linked) と双方向 (doubly linked)
– 一方向のとき,各要素は prev を持たない
• 既ソート (sorted) と未ソート
– 既ソート: リストの線形順序はキーの線形順序に対応
– 未ソート: 任意の順序
• 循環 (circular list) と非循環
– 循環: リストの先頭要素の prev はリストの末尾を指し,
末尾の next はリストの先頭を指す
• 以下では未ソート双方向循環リストを扱う
374
連結リスト (Linked Lists)
• オブジェクトをある線形順序に並べて格納するデータ構造
• 連結リストでの線形順序は,オブジェクトが含むポインタで決定さ
れる
• 双方向連結リスト (doubly linked list) L の要素
– キーフィールド KEY
– ポインタフィールド PREV, NEXT
– (付属データ)
• リストの最初の要素はキーを持たない特別なオブジェクト(nil_obj)
NIL
•
•
•
•
•
双方向リストはnil_objを含めた循環リストで表現される
リストL は NIL というメンバ変数を持つ
NILはnil_objを指す
先頭要素は HEAD = NEXT(NIL)
NEXT(x): リスト中の x の直後の要素のポインタ
– NEXT(x) = nil_obj のとき,x は最後の要素
• PREV(x): x の直前の要素のポインタ
– PREV(x) = nil_obj のとき,x はリストの最初の要素
NIL
9
nil_obj
375
16
4
9
376
nil_obj
16
4
377
7
双方向リストの構造体
省略記法
• リストの要素
class lobj {
public
lobj *PREV;
// 前の要素へのポインタ
lobj *NEXT;
// 後の要素へのポインタ
data KEY; }; // キー
#define KEY(x) (x->KEY)
#define NEXT(x) (x->NEXT)
#define PREV(x) (x->PREV)
#define HEAD (NIL->NEXT)
• 双方向リスト
class dlist {
public
lobj *NIL;
};
// nil_objへのポインタ
378
379
空リスト
連結リストからの削除
void LIST_DELETE(lobj *x)
{NEXT(PREV(x)) = NEXT(x);
PREV(NEXT(x)) = PREV(x);}
• 以下で表現される
NIL
NIL
nil_obj
9
16
4
nil_obj
380
連結リストの探索 (再帰バージョン)
連結リストへの挿入
• LIST_INSERT(x): x を リストの先頭に挿入
– x のキーは既にセットされているとする
void LIST_INSERT(lobj *x)
• O(1) 時間
{NEXT(x) = HEAD;
PREV(HEAD) = x;
x
HEAD = x;
PREV(x) = NIL;}
9
NIL
9
381
16
4
16
4
• R_LIST_SEARCH(k, i): リスト に属する,要素i以
降でキー k を持つ最初の要素のポインタを返す
• R_LIST_SEARCH(k, HEAD) とすれば最初から
探索
NIL
382
lobj *R_LIST_SEARCH(data k, lobj* i)
{
if (i != NIL && KEY(i) != k)
return R_LIST_SEARCH(k, NEXT);
else return i;
}
9
16
4
383
8
最小値の探索 (再帰バージョン)
• R_MINIMUM(i): リストに属する,要素i以降の最小
値を返す
• R_MINIMUM(HEAD) とすれば全体の最小値を返
す
NIL
data R_MINIMUM(lobj* i)
{
if (NEXT(i) != NIL)
return KEY(i)とR_MINIMUM(NEXT(i))の最小値;
else return KEY(i);
}
9
16
4
384
9