算法数理工学 第1回 定兼 邦彦 1 目的・成績評価・参考書 • アルゴリズム理論による設計は,大規模なデータを高速で処理 する際に特に有効 • 部品 (データ構造) を組み合わせてプログラムを作るだけでなく, 部品の中身を理解する • • • • 成績評価は試験とレポート メール: [email protected] ホームページ: http://researchmap.jp/sada/ 居室: 工学部6号館341 • 参考書:アルゴリズムイントロダクション(近代科学社)など プログラミング問題集 AIZU ONLINE JUDGE 問題セットの Introduction to Algorithms and Data Structures http://judge.u-aizu.ac.jp/onlinejudge/finder.jsp?course=ALDS1 2 アルゴリズムの概念 ~ オーダーと計算量 ~ ・ 算法 = アルゴリズム (algorithm) ・ アルゴリズムの概念 ・ オーダーの定義と計算量 3 アルゴリズム • アルゴリズムとは – 入力 (input): ある値(の集合) – 出力(output): ある値(の集合) – 明確に定義された計算手続き • 明確に定義された計算問題を解くための道具 • 問題の記述とは – 望むべき入出力関係を指定すること 4 ソーティング問題の形式的定義 • 入力: n 個の数の列 〈a1, a2, ..., an〉 • 出力: a’1 a’2 … a’n であるような入力 列の置換 〈a’1, a’2, ..., a’n〉 • 入力例 (具体例, instance) – 入力 〈31, 41, 59, 26, 41,58〉 – 出力 〈26, 31, 41, 41, 58, 59〉 5 ソーティング • 計算機科学における基本的な操作 • 多くのアルゴリズムが開発されている • 入力例によって優劣が異なる – ソートすべきデータの数 – どの程度までデータがすでにソートされているか – 用いる記憶装置の種類 (主記憶, ディスク, テープ) 6 アルゴリズムの正しさ • アルゴリズムが正しい (correct) ⇒全ての具体例に対して正しい出力とともに停止する – 与えられた計算問題を解く (solve) という. • 正しくないアルゴリズム – ある具体例に対して望ましい答えを出力せずに停止 – ある具体例に対して全く停止しない • 確率的アルゴリズムも存在 – モンテカルロ法 (高い確率で正しい答えを出す) • 円周率の計算,素数判定 – ラスベガス法 (高い確率で早く停止する) • ランダムクイックソート 7 挿入ソート • 入力: 長さ n の配列 A[0..n-1] • 出力: ソートされた配列 A[0..n-1] void INSERTION-SORT(data *A, int n) { data key; int i, j; for (j=1; j < n; j++) { key = A[j]; // A[j] をソート列 A[0..j-1] に挿入する i = j – 1; while (i >= 0 && A[i] > key) { A[i+1] = A[i]; i = i -1; } A[i+1] = key; } } 5 2 4 6 1 3 2 5 4 6 1 3 2 4 5 6 1 3 2 4 5 6 1 3 1 2 4 5 6 3 1 2 3 4 5 6 入力 出力 8 C言語の場合 #include <stdio.h> #include <stdlib.h> typedef int data; void INSERTION_SORT(data *A, int n) { data key; int i, j; for (j=1; j < n; j++) { key = A[j]; // A[j] をソート列 A[0..j-1] に挿入する i = j - 1; while (i >= 0 && A[i] > key) { A[i+1] = A[i]; i = i - 1; } A[i+1] = key; } } int main(int argc, char *argv[]) { data A[14] = {27,17,3,16,13,10,1,5,7,12,4,8,9,0}; int i,n; n = 14; INSERTION_SORT(A,n); for (i=0;i<n;i++) printf("%d ",A[i]); printf("\n"); } 9 アルゴリズムの解析 • アルゴリズムの実行に必要な資源を予測したい – メモリ量 – 通信バンド幅, 論理ゲート – 計算時間 • 解析を行うにはモデルを仮定する必要がある • RAM (random access machine) モデル – 命令は1つずつ実行 – どのメモリ番地も一定の時間で読み書き可 10 挿入ソートの解析 • INSERTION-SORTの手続きに要する時間は入 力によって変わる. • 一般に, プログラムの実行時間は入力のサイズ の関数で表す. • 入力サイズの定義 – ソーティング, 離散フーリエ変換など: データ数 – 整数の積の計算など: 入力のビット数 – グラフの問題: グラフの頂点と辺の数 11 実行時間の定義 • 実行される基本的な演算の回数 • プログラムの第 i 行の実行に ci 時間かかるとす る (ci は定数) • 注: サブルーチン呼び出しは定数時間ではない 12 void INSERTION-SORT(data *A, int n) { data key; int i, j; for (j=2; j <= n; j++) { key = A[j]; // A[j] をソート列 A[1..j-1] に挿入する i = j – 1; コスト 回数 c1 c2 n n-1 c4 n-1 n while (i > 0 && A[i] > key) { c5 t j 2 n A[i+1] = A[i]; c6 (t j 1) (t j 1) j 2 n i = i -1; c7 j 2 } A[i+1] = key; c8 j n-1 } } tj : while ループが j の値に対して実行される回数 13 INSERTION-SORTの実行時間 T (n) c1n c2 (n 1) c4 (n 1) n n n j 2 j 2 j 2 c5 t j c6 (t j 1) c7 (t j 1) c8 (n 1) tj の値は入力によって変化する. 最良の場合 = 配列が全てソートされている場合 tj = 1 T (n) c1n c2 (n 1) c4 (n 1) c5 (n 1) c8 (n 1) (c1 c2 c4 c5 c8 )n (c2 c4 c5 c8 ) an b n の線形関数 (linear function) 14 最悪の場合 • 配列が逆順にソートされている場合 – tj = j n(n 1) T (n) c1n c2 (n 1) c4 (n 1) c5 1 2 n(n 1) n(n 1) c6 1 c7 1 c8 (n 1) 2 2 an bn c 2 n の2次関数 (quadratic function) 15 時間計算量 (Time Complexity) • アルゴリズムの実行時間は入力に依存する • アルゴリズムの解析では通常最悪時の実行時 間を考える – 最悪時の実行時間は任意の入力に対する実行時 間の上界 – 最悪時の実行時間を保証する – 「最悪時」は頻繁に起きる (データベース検索など) – 平均時も最悪時と同じくらい悪い (挿入ソートで数 をランダムに挿入) 16 増加のオーダ • 実行時間の解析を容易にするための抽象化 – – – – – 各行の実行時間 (コスト) を定数 ci とする 最悪の実行時間を an2+bn+c と表す 実行時間の増加率をみるには主要項 an2 で十分 定数係数も無視 (n2) と表す • 挿入ソートは (n2) という最悪実行時間を持つ • あるアルゴリズムが他より効率がよい ⇔最悪実行時間の増加率が低い 17 関数のオーダ • アルゴリズムの効率を実行時間のオーダで 特徴付け, 相対的な比較を行う • 入力サイズ n が大きいときの挙動を知りたい • アルゴリズムの漸近的 (asymptotic) な効率を 調べる 18 漸近記号 -記法 • ある関数 g(n) に対し, (g(n)) は次のような集 合と定義する (g(n)) = {f(n): 全ての n n0に対して 0c1 g(n) f(n) c2 g(n) となるような 正の定数c1 , c2 , n0 が存在} ( g (n)) { f (n) : c1 0c2 0n0 0n n0 0 c1 g (n) f (n) c2 g (n)} • f(n) = (g(n)) は f(n) (g(n)) を意味する • 2n2+3n+1 = 2n2 + (n) という表現も用いる 19 1 2 n 3n (n 2 )を示す 2 1 2 全ての n n0に対して c1n n 3n c2 n 2となればいい 2 1 2 2 2 n で割ると c1n n 3n c2 n 2 2 1 c2 なら n 1に対して右辺の不等号 が成立 2 1 c1 なら n 7に対して左辺の不等号 が成立 14 1 1 よって c1 , c2 , n0 7とすれば成立 14 2 2 20 6n (n )を背理法で示す 3 2 全ての n n0に対して 6n 3 c2 n 2であるような 定数c2 , n0が存在したとする 任意の大きな nに対して 6n c2となり矛盾 f (n) an 2 bn c (n 2 ) d 任意の d次多項式p(n) ai n iに対し (ad 0) i 0 p ( n ) ( n d ) 21 O-記法 • ある関数 g(n) に対し, O(g(n)) は次のような集 合と定義する O(g(n)) = {f(n): 全ての n n0に対して 0 f(n) c g(n) となるような 正の定数c, n0 が存在} O( g (n)) { f (n) : c 0n0 0n n0 0 f (n) cg (n)} • f(n) = (g(n)) ならば f(n) = O(g(n)) • n = O(n2) も正しい表現 22 • 挿入ソートの実行時間はO(n2)…正解 • 挿入ソートの実行時間は(n2)…間違い – ソートされた入力に対しては(n) • 実行時間がO(n2)であるという場合, 最悪実行 時間について言っている – 実行時間は入力データに依存するため – 最悪実行時間はデータ数 n のみに依存 23 -記法 • ある関数 g(n) に対し, (g(n)) は次のような 集合と定義する (g(n)) = {f(n): 全ての n n0に対して 0 c g(n) f(n) となるような 正の定数c, n0 が存在} O( g (n)) { f (n) : c 0n0 0n n0 0 cg (n) f (n)} • f (n), g(n) に対して f(n) = (g(n)) であるため の必要十分条件は f(n) = O(g(n)) かつ f(n) = (g(n)) 24 • -記法は下界を表す • 挿入ソートの最良の実行時間は(n)とは – このアルゴリズムではどのような入力に対して も n に比例した時間が必ず必要という意味 • アルゴリズムの実行時間が(n2)とは – どのような入力に対しても少なくとも n2 に比例 する時間がかかるという意味 25 o-記法 (リトルオー) • ある関数 g(n) に対し, o(g(n)) は次のような集 合と定義する o(g(n)) = {f(n): 任意の定数 c > 0 に対し ある定数 n0 が存在し, 全ての n n0に対して 0 f(n) c g(n)} o( g (n)) { f (n) : c 0n0 0n n0 0 f (n) cg (n)} f ( n) lim 0が成り立つ n g ( n) 26 • -記法 (g(n)) = {f(n): 任意の定数 c > 0 に対し ある定数 n0 が存在し, 全ての n n0に対して 0 c g(n) f(n)} • n2/2 = (n) • n2/2 (n2) f (n) ( g (n)) g (n) o( f (n)) f ( n) lim が成り立つ n g ( n) 27 アルゴリズムの設計 • 挿入ソート: 逐次添加法 (incremental approach) • 分割統治法 (divide-and-conquer) に基づく方法 →マージソート – 実行時間の解析が容易であることが多い 28 分割統治法 • 問題の再帰的な (recursive) 構造を利用 分割:問題をいくつかの小さな部分問題に分割 統治:各部分問題を再帰的に解く 統合:それらの解を組合わせて元の問題の解を構成 – マージソートでは 分割: n 要素の列を n/2 要素の2つの部分列に分割 統治:マージソートを用いて2つの部分列をソート 統合:2つのソートされた部分列を統合して答を得る 29 マージソート void MERGE_SORT(data *A, int p, int r, data *B) { int q; // A[p..r] をソート if (p < r) { // p==r ならソートする必要なし q = (p+r)/2; MERGE_SORT(A, p, q, B); // A[p..q] を再帰的にソート MERGE_SORT(A, q+1, r, B); // A[q+1..r] を再帰的にソート MERGE(A, p, q, r, B); // ソートされた部分列 A[p..q] と A[q+1..r] を統合 } } 30 1 2 2 3 4 5 6 6 2 3 マージ 2 4 5 6 1 マージ マージ 2 5 4 マージ 5 6 1 4 3 2 マージ マージ 2 6 6 1 6 マージ 3 2 6 31 マージ • 一時的な配列 B[0,n-1] を用いる void MERGE(data *A, int p, int q, int r, data *B) { // ソートされた部分列 A[p..q] と A[q+1..r] を統合 int i,j,k; data t; k = p; i = p; j = q+1; while (k <= r) { if (j > r) t = A[i++]; // 前半のみにデータがある else if (i > q) t = A[j++]; // 後半のみにデータがある else if (A[i] <= A[j]) t = A[i++]; // 前半のほうが小さい else t = A[j++]; // 後半のほうが小さい B[k++] = t; // 一時的な配列に保存 } for (i=p; i<=r; i++) A[i] = B[i]; // 元の配列に書き戻す } 32 void MERGE_SORT(data *A, int p, int r, data *B) // A[p..r] をソート { int q; if (p < r) { // p==r ならソートする必要なし q = (p+r)/2; MERGE_SORT(A, p, q, B); // A[p..q] を再帰的にソート MERGE_SORT(A, q+1, r, B); // A[q+1..r] を再帰的にソート MERGE(A, p, q, r, B); // ソートされた部分列 A[p..q] と A[q+1..r] を統合 } } int main(int argc, char *argv[]) { data A[14] = {27,17,3,16,13,10,1,5,7,12,4,8,9,0}; data B[14]; int i,n; n = 14; MERGE_SORT(A,0,n-1,B); for (i=0;i<n;i++) printf("%d ",A[i]); printf("\n"); } 33 分割統治アルゴリズムの解析 • 全体の実行時間は問題のサイズに関する漸化式 (recurrence) で記述できることが多い • サイズ n の問題に関する実行時間を T(n) とする • n c (ある定数) ならば定数時間((1)) • 問題を a 個の部分問題に分割し, それぞれが元の サイズの 1/b 倍になったとすると (1) n cのとき T ( n) aT (n / b) D(n) C (n) それ以外 D(n), C(n): 問題の分割, 統合にかかる時間 34 マージソートの解析 • n は2のべき乗と仮定する • n = 1のとき T(n) = (1) • n > 1のとき – 分割: D(n) = (1) – 統治:再帰的にサイズn/2の部分問題を解く 2T(n/2) – 統合:MERGEは C(n) = (n) (1) n 1のとき T ( n) 2T (n / 2) (n) n 1のとき T(n)= (n lg n) となる 35 アルゴリズムの重要性 • コンピュータが速くても, 実行時間のオーダ が大きいアルゴリズムは役に立たない • スーパーコンピュータで挿入ソートを実行 – 1秒間に1億命令実行 – 2n2 命令必要 • パーソナルコンピュータでマージソートを実 行 – 1秒間に100万命令実行 – 50 n lg n 命令必要 36 • 100万個の数の配列のソート • ス-パーコンピュータで挿入ソート 2 (106 ) 2 命令 20,000秒 5.56時間 8 10 命令 / 秒 • パーソナルコンピュータでマージソート 50 106 lg 106 命令 1,000秒 16.67分 6 10 命令 / 秒 • オーダの低いアルゴリズムの開発が重要 37 問題の計算量 • アルゴリズムの計算量ではなく,問題の計算量 (計算複雑度)という概念がある • ある問題の計算量が O(f(n)) とは – その問題を解くあるアルゴリズム A が存在し, 任意の入力に対する計算時間が O(f(n)) である. – 例:ソーティングの計算量は O(n log n) (マージソートはどんな入力でも O(n log n) 時間だから) 38 • ある問題の計算量が Ω(f(n)) とは – その問題に対するどんなアルゴリズムにも,計算時間 が Ω(f(n)) となる入力が存在する – 例:比較ソートの計算量は Ω(n log n) 証明は後日行う • ある問題の計算量が (f(n)) とは – O(f(n)) かつ Ω(f(n)) – 例:比較ソートの計算量は (n log n) • 計算量の議論をする場合は,計算モデルを明確に する必要がある 39 クイックソート • n 個の数に対して最悪実行時間(n2)のソーティ ングアルゴリズム • 平均実行時間は(n log n) • 記法に隠された定数も小さい • in-place (一時的な配列が必要ない) 40 クイックソートの記述 • 分割統治法に基づく • 部分配列 A[p..r] のソーティング 1. 部分問題への分割: – – – 配列 A[p..r] を2つの部分配列 A[p..q] と A[q+1..r] に 再配置する. A[p..q] のどの要素も A[q+1..r] の要素以下にする. 添字 q はこの分割手続きの中で計算する. 2. 部分問題の解決 (統治): • 部分配列 A[p..q] と A[q+1..r] を再帰的にソート 3. 部分問題の統合 • A[p..r] はすでにソートされているので何もしない 41 クイックソートのコード void QUICKSORT(data *A, int p, int r) { int q; if (p < r) { q = PARTITION(A,p,r); QUICKSORT(A,p,q); QUICKSORT(A,q+1,r); } } main() { QUICKSORT(A,0,n-1); } 42 配列の分割 int PARTITION(data *A, int p, int r) // O(n) 時間 { int q, i, j; data x, tmp; x = A[p]; i = p-1; j = r+1; while (1) { do {j = j-1;} while (A[j] > x); do {i = i+1;} while (A[i] < x); if (i < j) { tmp = A[i]; A[i] = A[j]; A[j] = tmp; } else { return j; } } } 43 A[p..r] 5 3 2 6 4 1 3 7 i j 5 3 2 6 4 1 3 7 i A[i] x A[j] x となる最初の i, j 7 は正しい分割位置にある j 3 3 2 6 4 1 5 7 i 初期状態: i と j は配列の範囲外 x = A[p] = 5 によって分割 x: 枢軸 (pivot) と呼ぶ j A[i] と A[j] を交換 正しい分割位置になる 44 3 3 2 6 4 1 5 7 i j 3 3 2 1 4 6 5 7 i A[i] x A[j] x となる最初の i, j A[i] と A[j] を交換 j 3 3 2 1 4 6 5 7 j i i j となったので q = j を返す A[p..q] は x 以下 A[q+1..r] は x 以上 45 PARTITIONの正当性 • PARTITIONの返り値を q とする • A[p..r] の分割後に満たされるべき条件 – A[p..q] はある pivot x 以下 – A[q+1..r] は x 以上 –pq<r • q = r となると,QUICKSORTは停止しないため, どんな A に対しても q < r となることを保障する 必要がある 46 • 初期状態は i < j • do {j = j-1;} while (A[j] > x); do {i = i+1;} while (A[i] < x); の終了後 – – – – p i, j r A[j+1..r] は x 以上 A[p..i-1] は x 以下 A[i] x A[j] • i < j のとき,A[i] と A[j] を交換すると – A[j..r] は x 以上 – A[p..i] は x 以下 • i j のとき q = j 47 • PARTITIONの終了時に q = j = r とする – while (1) のループを実行する毎に j は1以上減る – 終了時に j = r ならばこのループは1度しか実行されて いない – しかし1回目のループでは x = A[p] なので i = p • PARTITIONは p < r の場合のみ呼ばれるので, 1回目のループでは p = i < j = r • つまり1回目のループでは終了しない • よってPARTITION終了時に q = r とはならない. つまり q < r 48 クイックソートの性能 • クイックソートの実行時間は分割が平均化 されているかどうかに依存 • これはpivotの選択に依存 • 分割が平均化されていれば実行時間は漸 近的にマージソートと同じ ((n log n)) • 最悪時は (n2) 49 最悪の分割 • 最悪の場合は,PARTITIONによって領域が 1 要素と n-1 要素に分けられる場合 • 分割には (n) 時間かかる • 実行時間に対する漸化式は – T(n) = T(n1) + (n), T(1) = (1) • T(n) = (n2) • 最悪実行時間は挿入ソートと同じ • 入力が完全にソートされているときに起こる (挿入ソートなら O(n) 時間) 50 n n n 1 1 n 1 n2 1 1 n n n2 n 3 3 2 2 1 再帰木 1 Total : n 2 51 最良の分割 • クイックソートが最も速く動作するのは, PARTITIONによってサイズ n/2 の2つの領域 に分割されるとき. • T(n) = 2T(n/2) + (n) • T(n) = (n lg n) • ちょうど半分に分割される場合が最速 52 n n n 2 n 2 n 4 n 4 n 4 n n 4 n lg n n n n n n n n n 8 8 8 8 8 8 8 8 n n Total : nlg n 53 平衡分割 • PARTITIONで常に 9 対 1 の比で分割されると 仮定する • T(n) = T(9n/10) + T(n/10) + (n) • 再帰木の各レベルのコストは O(n) • 再帰の深さは log 109 n lg n • 漸近的には中央で分割するのと同じ • 分割の比が 99 対 1 でも同じ (定数比なら良い) 54 クイックソートの 確率的アルゴリズム • クイックソートの平均的な場合の実行時間を解析 する場合,入力の頻度を仮定する必要がある. • 通常は,すべての順列が等確率で現れると仮定 • しかし実際にはこの仮定は必ずしも期待できない • この仮定が成り立たなくてもうまく動作するクイック ソートの確率的アルゴリズムを示す 55 確率的 (randomized) アルゴリズム • 動作が入力だけでなく乱数発生器 (randomnumber generator)に依存するアルゴリズム • 関数 RANDOM(a,b): a 以上 b 以下の整数を 等確率で返すとする. • プログラミング言語は擬似乱数発生器 (pseudorandom-number generator) を備える • 擬似乱数: 統計的にはランダムに見えるが, 決定的に作られる数(の列) 56 乱数発生法 • {0, 1, …, p1} の整数を一様ランダムに生成 – C言語 int x; x = rand() % p; // 整数乱数を p で割った余り 57 乱数の種の設定 • 現在時刻を乱数の種にする • C言語 srand((int)clock()); srand((int)time(NULL)); 58 確率的アルゴリズム1 • クイックソートを行う前に入力配列の要素をラン ダムに並び替える • 実行時間は入力順序に依存しなくなる • アルゴリズムがうまく動作しないのは,乱数発生 器によって運の悪い順列を作る場合のみ • 最悪の実行時間は改善されない ((n2)) • 最悪の場合はほとんど起きない 59 確率的アルゴリズム2 • 配列を A[p..r] を分割する前に, A[p] と A[p..r] からランダムに選んだ要素を交換 • pivotが rp+1 個の要素から等確率で選ば れることを保障する • 分割が平均的にはバランスのとれたもの になることが期待できる 60 int RANDOMIZED_PARTITION(data *A, int p, int r) { int i; data tmp; i = RANDOM(p,r); tmp = A[i]; A[i] = A[p]; A[p] = tmp; // 値の交換 return PARTITION(A,p,r); } void RANDOMIZED_QUICKSORT(data *A, int p, int r) { int q; if (p < r) { q = RANDOMIZED_PARTITION(A,p,r); RANDOMIZED_QUICKSORT(A,p,q); RANDOMIZED_QUICKSORT(A,q+1,r); } } 61 最悪の場合の解析 • T(n): サイズ n の入力に対するQUICKSORT の最悪実行時間 T (n) max T (q) T (n q) (n) 1 q n 1 • T(n) = O(n2) を示す • m < n に対し T(m) cm2 と仮定 62 T (n) max T (q) T (n q ) (n) 1 q n 1 c max q (n q) (n) 2 2 1 q n 1 c 1 (n 1) (n) 2 2 cn 2c(n 1) (n) 2 cn 2 c は 2c(n1) が (n) の項よりも大きくなるように 十分大きくとる よって T(n) = O(n2) が示された 63 平均的な場合の解析 • T(n): サイズ n の入力に対するRANDOMIZED QUICKSORTの平均実行時間 • T(n) に関する漸化式を解く • 入力データはすべて異なる数とする 64 分割の解析 • RANDOMIZED_PARTITIONでPARTITIONが呼 ばれるとき,A[p] の要素は A[p..r] のランダムな 要素と置き換えられている. • 観察:PARTITIONによって返される q の値は A[p..r] の中での x = A[p] のランクのみに依存 • x のランク rank(x) = 集合中の x 以下の要素数 • 要素数 n = rp+1 • rank(x) = i となる確率は 1/n 65 • rank(x) = 1 のとき,PARTITIONでのループは i = j = p で終了 • このとき q = j = p つまり分割の小さい方のサイ ズは 1.この事象が起きる確率は 1/n • rank(x) 2 のとき,x = A[p] より小さい値が少 なくとも1つ存在 • PARTITIONでの最初のループ実行後は i = p,j > p • A[i] と A[j] を入れ替えるため,x = A[p] は右の 分割に入る 66 • PARTITIONが停止したとき,左の分割には rank(x)1 個の要素があり,それらは x より小さい • rank(x) 2 のとき,左の分割のサイズが i である 確率は 1/n (i = 1,2,...,n-1) • rank(x) が1の場合と2以上の場合を合わせると, • 左の分割のサイズ rp+1 が – 1 になる確率: 2/n – i になる確率: 1/n (i = 2,3,...,n-1) 67 平均時に対する漸化式 • T(n): n 要素の入力配列をソートするのに必要 な平均時間 • T(1) = (1) • 長さ n の配列に対してソートする場合 – 配列の分割: (n) 時間 – 統治: 長さ q と nq の部分配列を再帰的にソート n 1 1 T (n) T (1) T (n 1) (T (q) T (n q)) (n) n q 1 68 T(1)=(1), T(n1) = O(n2) より 1 1 T (1) T (n 1) (1) O(n 2 ) n n O(n) よって T(n) は次のように書ける 1 n 1 T (n) (T (q ) T (n q )) (n) n q 1 2 n 1 T ( k ) ( n ) n k 1 69 m < n に対し T(m) am lg m + b (a>0, b>0) と仮定 2 n 1 T ( n ) T ( k ) ( n ) n k 1 2 n 1 (ak lg k b) (n) n k 1 2a n 1 2b k lg k (n 1) (n) n k 1 n n 1 1 2 1 2 k lg k n lg n n を用いる 2 8 k 1 a n が (n)+b 以上になるように a を選ぶと 4 70 2a 1 2 1 2 2b T ( n) n lg n n (n 1) (n) n 2 8 n a an lg n n 2b (n) 4 a an lg n b (n) b n 4 an lg n b T(n) においても成り立つ よってクイックソートの平均実行時間は O(n lg n) 71 n 1 1 2 1 2 k lg k n lg n n の証明 2 8 k 1 n 1 n / 2 1 n 1 k 1 k 1 k n / 2 k lg k k lg k k lg k n / 2 1 k 1 n 1 n 1 n / 2 1 n k lg k lg n (lg n 1) k lg n k 2 k n / 2 k 1 k n / 2 n 1 n / 2 1 k 1 k 1 lg n k k 1 1n n n(n 1) lg n 1 2 22 2 1 2 1 2 n lg n n 2 8 72
© Copyright 2024 ExpyDoc