情報工学概論 (アルゴリズムとデータ構造) 第2回 実習用環境 • C言語 – 学習用C言語開発環境 http://9cguide.appspot.com/ • Fortran 90 – Self-extracting Windows x86 http://www.g95.org/downloads.shtml http://ftp.g95.org/g95-MinGW.exe 2 アルゴリズムの設計 • 挿入ソート: 逐次添加法 (incremental approach) • 分割統治法 (divide-and-conquer) に基づく方法 →マージソート – 実行時間の解析が容易であることが多い 3 分割統治法 • 問題の再帰的な (recursive) 構造を利用 分割:問題をいくつかの小さな部分問題に分割 統治:各部分問題を再帰的に解く 統合:それらの解を組合わせて元の問題の解を構成 – マージソートでは 分割: n 要素の列を n/2 要素の2つの部分列に分割 統治:マージソートを用いて2つの部分列をソート 統合:2つのソートされた部分列を統合して答を得る 4 マージソート 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] を統合 } } 5 1 2 2 3 4 5 6 6 マージ 2 4 5 6 1 5 4 マージ 5 6 1 6 4 3 2 マージ マージ 2 3 マージ マージ 2 2 6 1 6 マージ 3 2 6 6 マージ • 一時的な配列 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]; // 元の配列に書き戻す } 7 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"); } 8 Rubyの場合 def merge(a, b) x = [] i=0 j=0 while i < a.length do if j == b.length then x << a[i] i=i+1 elsif a[i] < b[j] then x << a[i] i=i+1 else x << b[j] j=j+1 end end x.concat(b[j..-1]) return x end def merge_sort(a) n = a.length if n == 1 then return [a[0]] else return merge(merge_sort(a[0..n/2-1]), merge_sort(a[n/2..-1])) end end a = [27,17,3,16,13,10,1,5,7,12,4,8,9,0] b = merge_sort(a) pb 9 Fortran 90の場合 program main integer a(14) integer b(size(a)) a = (/ 27,17,3,16,13,10,1,5,7,12,4,8,9,0 /) print *, "a = ", a print *, "merge sort" call mergesort(a, lbound(a,1), ubound(a,1), b) print *, "a = ", a contains recursive subroutine mergesort(a, p, r, b) integer a(:), b(:) integer p, r integer q if (p < r) then q = (p+r)/2 call mergesort(a, p, q, b) call mergesort(a, q+1, r, b) call merge(a, p, q, r, b) end if end subroutine mergesort subroutine merge(a, p, q, r, b) integer a(:), b(:) integer p, r, q, i, j, k integer t k = p; i = p; j = q+1 do if (.not. (k <= r)) exit if (j > r) then t = a(i); i = i+1 else if (i > q) then t = a(j); j = j+1 else if (a(i) <= a(j)) then t = a(i); i = i+1 else t = a(j); j = j+1 end if b(k) = t; k = k+1 end do do i = p, r a(i) = b(i) end do end subroutine merge end program main 10 分割統治アルゴリズムの解析 • 全体の実行時間は問題のサイズに関する漸化式 (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): 問題の分割, 統合にかかる時間 11 マージソートの解析 • 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) となる 12 アルゴリズムの重要性 • コンピュータが速くても, 実行時間のオーダが 大きいアルゴリズムは役に立たない • スーパーコンピュータで挿入ソートを実行 – 1秒間に1億命令実行 – 2n2 命令必要 • パーソナルコンピュータでマージソートを実行 – 1秒間に100万命令実行 – 50 n lg n 命令必要 13 • 100万個の数の配列のソート • ス-パーコンピュータで挿入ソート 2 (106 ) 2 命令 20,000秒 5.56時間 8 10 命令 / 秒 • パーソナルコンピュータでマージソート 50 106 lg 106 命令 1,000秒 16.67分 6 10 命令 / 秒 • オーダの低いアルゴリズムの開発が重要 14 今までのソーティングアルゴリズム 1. 挿入ソート: (n2) 時間だが,入力サイズが 小さいときには高速.in-place 2. マージソート: (n lg n) 時間だが,実行には 一時的な配列が必要 15 新しいソーティングアルゴリズム 1. ヒープソート: (n lg n) 時間, in-place. 2. クイックソート: 最悪 (n2) 時間だが平均実行 時間は(n lg n).実際上は高速.in-place. 16 クイックソート • n 個の数に対して最悪実行時間(n2)のソーティ ングアルゴリズム • 平均実行時間は(n log n) • 記法に隠された定数も小さい • in-place (一時的な配列が必要ない) 17 クイックソートの記述 • 分割統治法に基づく • 部分配列 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. 部分問題の統合 • 18 A[p..r] はすでにソートされているので何もしない クイックソートのコード 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); } 19 def quick_sort(a) Rubyの場合 n = a.length 注:これはin-placeではない if n <= 1 then (一時的に余分な記憶領域が必要) return a else pivot = a[0] less = a.select{|x| x < pivot} equal = a.select{|x| x == pivot} greater = a.select{|x| x > pivot} return quick_sort(less) + equal + quick_sort(greater) end end a = [27,17,3,16,13,10,1,5,7,12,4,8,9,0] b = quick_sort(a) pb 20 配列の分割 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; } } } 21 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] を交換 正しい分割位置になる 22 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 以上 23 Fortran 90の場合 program main integer a(14) a = (/ 27,17,3,16,13,10,1,5,7,12,4,8,9,0 /) print *, "a = ", a print *, "quick sort" call quicksort(a, lbound(a,1), ubound(a,1)) print *, "a = ", a contains recursive subroutine quicksort(a, p, r) integer a(:) integer p, r, q if (p < r) then q = partition(a,p,r) call quicksort(a,p,q) call quicksort(a,q+1,r) end if end subroutine quicksort function partition(a, p, r) integer a(:) integer p, r, q, i, j integer x, tmp x = a(p) i = p-1; j = r+1 do do j = j-1 if (.not. (a(j) > x)) exit end do do i = i+1 if (.not. (a(i) < x)) exit end do if (i < j) then tmp = a(i); a(i) = a(j); a(j) = tmp else partition = j exit end if end do end function end program main 24 PARTITIONの正当性 • PARTITIONの返り値を q とする • A[p..r] の分割後に満たされるべき条件 – A[p..q] はある pivot x 以下 – A[q+1..r] は x 以上 –pq<r • q = r となると,QUICKSORTは停止しないため, どんな A に対しても q < r となることを保障する 必要がある 25 • 初期状態は 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 26 • 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 27 8.2 クイックソートの性能 • クイックソートの実行時間は分割が平均化さ れているかどうかに依存 • これはpivotの選択に依存 • 分割が平均化されていれば実行時間は漸近 的にマージソートと同じ ((n log n)) • 最悪時は (n2) 28 最悪の分割 • 最悪の場合は,PARTITIONによって領域が 1 要素と n-1 要素に分けられる場合 • 分割には (n) 時間かかる • 実行時間に対する漸化式は – T(n) = T(n1) + (n), T(1) = (1) • T(n) = (n2) • 最悪実行時間は挿入ソートと同じ • 入力が完全にソートされているときに起こる (挿入ソートなら O(n) 時間) 29 n n n 1 1 n 1 n2 1 1 n n n2 n 3 3 2 2 1 再帰木 1 Total : n 2 30 最良の分割 • クイックソートが最も速く動作するのは, PARTITIONによってサイズ n/2 の2つの領域 に分割されるとき. • T(n) = 2T(n/2) + (n) • T(n) = (n lg n) • ちょうど半分に分割される場合が最速 31 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 32 平衡分割 • PARTITIONで常に 9 対 1 の比で分割されると 仮定する • T(n) = T(9n/10) + T(n/10) + (n) • 再帰木の各レベルのコストは O(n) • 再帰の深さは log 109 n lg n • 漸近的には中央で分割するのと同じ • 分割の比が 99 対 1 でも同じ (定数比なら良い) 33
© Copyright 2024 ExpyDoc