情報工学概論 (アルゴリズムとデータ構造) 第6回 番兵 (Sentinel) • プログラムを単純にするために用いるダミー オブジェクト void LIST_DELETE(dllist *L, lobj *x) { if (PREV(x) != NIL) NEXT(PREV(x)) = NEXT(x); else HEAD(L) = NEXT(x); if (NEXT(x) != NIL) PREV(NEXT(x)) = PREV(x); } void LIST_DELETE2(dllist2 *L, lobj *x) { NEXT(PREV(x)) = NEXT(x); PREV(NEXT(x)) = PREV(x); } 2 双方向リストの構造体 • リストの要素 (以前と同じ) typedef struct lobj { struct lobj *prev; // 前の要素へのポインタ struct lobj *next; // 後の要素へのポインタ data key; // キー } lobj; • 双方向リスト typedef struct { lobj *nil; // NILを表す要素のポインタ } dllist2; 3 データ構造の変更点 • リスト L は NIL を表現するオブジェクトを持つ • 双方向リストをNILを含めた循環リストに変更 • 先頭要素は HEAD(L) = NEXT(NIL(L)) nil(L) 9 16 4 NIL 4 連結リストの探索 • LIST_SEARCH(L, k): リスト L に属する,キー k を持つ最初の要素のポインタを返す • キー k を持つ要素が存在しなければ NIL を返す lobj *LIST_SEARCH(dllist2 *L, data k) • (n) 時間 { lobj *x; x = HEAD(L); while (x != NIL(L) && KEY(x) != k) { x = NEXT(x); } return x; nil(L) } 9 16 4 5 連結リストへの挿入 • LIST_INSERT(L, x): x を L の先頭に挿入 – x のキーは既にセットされているとする • O(1) 時間 x nil(L) void LIST_INSERT(dllist2 *L, lobj *x) { NEXT(x) = HEAD(L); PREV(HEAD(L)) = x; HEAD(L) = x; PREV(x) = NIL(L); } 9 9 16 4 16 4 6 リストによるスタック/キューの実現 • スタックの場合 – 先頭に追加,先頭から削除 – 一方向リストで実現可能 • キューの場合 – – – – 末尾に追加,先頭から削除 削除は簡単 追加のためには,末尾へのポインタが必要 一方向リストで実現可能 • 全て O(1) 時間 7 11.3 ポインタとオブジェクトの実現 • オブジェクトは構造体やポインタを用いて表 現される • Fortran 77のような言語で実現するにはどう すればいいか 8 オブジェクトの多重配列表現 • • • • オブジェクトのフィールドごとに配列を用いる ポインタの代わりに,配列の添字を用いる 0 は NIL ポインタを表す 変数 L はリストの先頭を表す L 1 2 3 next 3 0 2 5 key 4 1 16 9 prev 5 2 7 0 7 4 5 6 7 8 9 オブジェクトの割り付けと開放 • 動的集合を表現する場合,新しいオブジェクト を格納するメモリ領域が必要 • C言語では malloc や free を用いるが,これは オーバーヘッドが大きい • オブジェクトのサイズがすべて等しい場合に限 定して,割り付けと開放を実装する 10 • オブジェクトは多重配列で表現されているとする • 配列の長さを m,現在表現されている動的集合の サイズを n とする • mn 個のオブジェクトは未使用 (free) • 未使用オブジェクトを “未使用リスト” (free list) と 呼ばれる1方向リストとして管理する L 1 7 2 3 4 5 6 7 8 8 3 0 key 4 1 16 9 prev 5 2 7 0 next 0 1 2 4 5 free 6 11 • 未使用リストでは,配列 next のみ用いる • 未使用リストの先頭は大域変数 free_list が保持 • 各オブジェクトはリスト L か未使用リストのどちらか に属する L 1 7 2 3 4 5 6 7 8 8 free_list 3 0 key 4 1 16 9 prev 5 2 7 0 next 0 1 2 4 5 6 12 オブジェクトの割り付け • • • • 未使用リストをスタックとみなす 割り付けるオブジェクトはリストの先頭 開放したらリストの先頭に追加 int ALLOCATE_OBJECT(void) O(1) 時間 { int x; if (free_list == NIL) { printf("ERROR メモリ不足\n"); exit(1); } else { x = free_list; free_list = NEXT(x); return x; } } 13 オブジェクトの開放 • オブジェクト x を開放し,未使用リストの先頭に 追加 • O(1) 時間 • オブジェクトに next フィールドがない場合も,あ るフィールドを next として用いてリストを表現 void FREE_OBJECT(int x) { NEXT(x) = free_list; free_list = x; } 14 メモリの初期化 • まず,すべてのオブジェクトを未使用リスト に入れておく void INIT_MEMORY(int size) { int i; prev = (int *)malloc((size+1)*sizeof(int)); next = (int *)malloc((size+1)*sizeof(int)); key = (data *)malloc((size+1)*sizeof(data)); if (prev == NULL || next == NULL || key == NULL) { printf("ERROR メモリ不足\n"); exit(1); } for (i = 1; i < size; i++) { NEXT(i) = i+1; } NEXT(size) = NIL; free_list = 1; nil = NIL; } 15 多重配列表現の欠点 • 多くの操作ではオブジェクト中の複数のフィールド を読み書きする – key, next, prev • 多重配列表現では,CPUキャッシュが利きにくい – 1つのオブジェクトがメモリ中で分断されているため 16 オブジェクトの単一配列表現 • 計算機のメモリは大きな配列とみなせる – 0 番地から M1 番地 • オブジェクトはメモリのある連続した場所を占める • ポインタはオブジェクトの占める場所の先頭番地を 指す • 各フィールドは,ポインタにオフセットを加えたもの で指される L 13 A key next prev 4 7 13 1 2 3 4 5 6 3 0 1 9 1 0 7 8 9 10 11 12 13 14 15 17 二分探索 • アルゴリズムとデータ構造で重要な概念 • 全順序集合の探索を高速化する • 集合 S の要素を L, E, G に分ける – L = {x | x S, x < p} (p より小さい要素) – E = {x | x S, x = p} (p と等しい要素) – G = {x | x S, x > p} (p より大きい要素) • k を探索するとき – p = k ならば探索終了 (見つかった) – p < k ならば G を二分探索 – p > k ならば L を二分探索 18 • • • • • サイズ n の集合の二分探索の時間を T(n) とする L, G のサイズを n1, n2 とする (n1+n2 n) T(n) = O(1) + max{T(n1)+T(n2)} n1 ½ n, n2 ½ n なら T(n) = O(1) + T(½ n) T(n) = O(log n) となる 19 既ソート配列を用いた辞書 • • • • 集合の要素を配列に格納するデータ構造 探索は二分探索を用いることができる 挿入はソート順を保つようにする必要がある 削除は未ソートの場合と同じ • 集合の要素は全て異なるとする 20 既ソート配列での二分探索 • • • • • • • • E は配列の中央の要素 (p = S[n/2]) L は中央より左側の要素 (S[0..n/21]) G は中央より右側の要素 (S[n/2+1..n1]) 集合が配列のl 番目からh 番目で表されているとき m = (l+h) / 2 とする S[m]=k ならば探索終了 (k が存在した) S[m]<k ならば k はL, E には存在しない⇒G を探索 S[m]>k ならば k はG, E には存在しない⇒L を探索 l <p L m p E h >p G 21 int search(dic_sortedarray *S, int k) { int i; int high, low, mid; low = 0; // 探索の範囲は最初は配列全体 [0..n-1] high = S->n-1; i = 0; while (low <= high) { mid = (low + high) / 2; if (S->key[mid] == k) { return mid; } else if (S->key[mid] < k) { low = mid + 1; i = mid + 1; } else { high = mid - 1; i = mid; } } return -(i+1); // 見つからなかったときに挿入する場所を返す } 22 • l > h になったら探索終了 (見つからなかった) • その直前では l = h = m • S[m] = p < k だったとき m p >k k はここに入る • S[m] = p > k だったとき <k m p k はここに入る 23 要素の挿入 • • • • • k が既に存在するなら挿入しない k を探索し,挿入場所 i を求める i から配列の最後までの要素を右に1つずらす 空いたところに k を入れる O(n) 時間 void insert(dic_sortedarray *S,int k) {int i,j; i = search(S, k); if (i >= 0) return; if (S->n+1 > S->MAX) { printf("ERROR 配列のオーバーフロー\n"); exit(1);} i = -i-1; for (j = S->n; j > i; j--) {S->key[j] = S->key[j-1];} S->key[i] = k; S->n = S->n + 1; 24 } 既ソート配列を用いた辞書のまとめ • 探索: O(log n) 時間 • 挿入: O(n) 時間 • 削除: O(n) 時間 • 初めに指定したサイズのメモリを常に使用する • それより多い数の要素は格納できない 25 9.1 ソーティングの下界 • ソーティングの入力: 〈a1, a2, ..., an〉 • 比較ソートでは要素間の比較のみを用いて ソートを行う • 2つの要素 ai, aj が与えられたとき,それらの相 対的な順序を決定するためにテストを行う – ai aj, ai aj, ai aj, ai aj, ai aj のみ • これ以外の方法では要素のテストはできない 26 仮定 • すべての入力要素は異なると仮定する – ai aj という比較は行わないと仮定できる • ai aj, ai aj, ai aj, ai aj は全て等価 • 全ての比較は ai aj という形と仮定できる 27 決定木モデル • 比較ソートは決定木 (decision tree) とみなせる • 決定木はソーティングアルゴリズムで実行される 比較を表現している • アルゴリズム中における制御,データの移動など の要因は無視する 28 5,4,3 2,4,1 2,4,1 a1 : a2 入力:数の列 各ノードでは ai aj の比較を行う 5,4,3 a2 : a3 a1 : a3 2,4,1 a1 : a3 a1, a2, a3 a2, a1, a3 a1, a3, a2 a3, a1, a2 1,2,4 葉は入力の置換に対応 5,4,3 a2 : a3 a2, a3, a1 a3, a2, a1 3,4,5 29 決定木の高さと比較回数 • 決定木はソートアルゴリズム A から決まる • 入力数列を与えると決定木の対応する葉が決まる • 根から葉までのパスの長さ =Aを実行する際の比較回数 • 根から葉までのパス長の最大値 =実行されるソートアルゴリズムの最悪比較回数 • 比較ソートでの最悪の比較回数は決定木の高さに 対応 30 最悪時の下界 • 決定木の高さの下界=任意の比較ソートアルゴ リズムの実行時間の下界 定理 9.1 n 要素をソートするどんな決定木の高さも (n lg n) 証明 n 要素をソートする高さ h の決定木を考える. ソートを正しく行うには,n 要素の n! 通りの置換全 てが葉に現れなければならない. 高さ h の2分木の葉の数は高々 2h. よって n! 2h ではソートできない. つまり h lg(n!) 31 h lg( n!) Stirling n n! e n の公式よりn! 2n e n 1 1 n n n n h lg e n lg n n lg e (n lg n) 32
© Copyright 2025 ExpyDoc