2分探索と2分木 ・ 2分探索 ・ ヒープ ・ 2分木 データを検索 ・ データの利用において、検索は基本のひとつ ・ 「データの中にこれはありますか?」という質問(こういう質 問をクエリーという)にすばやく答える方法を考えよう -1つの方法は、バケツやハッシュを使うもの ・ データのため方ではなく、検索方法のほうからアプローチ してみよう 辞書を引く ・ 例えば、辞書をひく、という操作を考えよう ・ 何かひきたい言葉があるときに、それを辞書の中からどう やって見つけ出すか?? 適当にひらき、そこのページに言葉があるか調べ、な ければそこの前か後ろを調べる 言葉が含まれるページの候補を絞り込んでいる ・ この方法を真似よう 2分探索(2分検索) ・ 簡単のため、データは数値の集まりとする ・ まず、データを小さい順にソートして並べておく。 s をデータ の先頭の添え字、 t をデータの最後の添え字とする ・ q はありますか、と質問が来たら、まず、真ん中を見る q と一致するなら、ありましたよ、と場所を返す 一致しなければ、q と真ん中を比べて、探索範囲を絞る t s 1 3 7 8 9 11 13 17 18 19 探索範囲の絞込み ・ q より真ん中が大きい q があるとすれば、真ん中より前 t を、真ん中より1つ前に設定、再帰的に検索 ・ q より真ん中が小さい q があるとすれば、真ん中より後ろ s を、真ん中より1つ後ろに設定、再帰的に検索 ・ s より t のほうが小さくなったら終了 ・ 探索範囲は、(およそ)半分半分と減っていく t s 1 3 7 8 9 s t 11 13 17 18 19 2分探索の計算時間 ・ 問題毎回、探索範囲が半分以下になる log2 n 回後には、探索範囲の大きさが1になり、終了する 計算時間は O(log2 n) 、情報理論的に最適 ・ メモリは O(n)。しかも、in place。最適 ・ つまりは、非常にすばらしい 練習問題 ・ 以下の数列で、8,17,19 を2分探索で検索せよ (s,t がどのよ うに変化するか、記録すること) 1 3 7 8 9 11 13 17 18 19 配列データの弱み ・ 配列データを保持する場合、挿入や削除をするのは大変 O(n) 時間かかる ・ リストを使うと挿入削除は速いが、「真ん中を見つける」のに 時間がかかる (先の例題のように、いくつか真ん中を保持する手もあるが、そ うすると真ん中を保持する時間がかかる) ・ 一般に、挿入も削除も検索も更新も速い、というのは、それ ほど自明なことではない ・ しかし、手はある まずは最小値から ・ 挿入削除が速くて、かつどのセルを見つけるのも速い、は欲 張りなので、まずは、「最小値を見つけるのが速い」としよう 問題設定: ・ データ(数値)を保持すること ・ データの挿入&削除(あるいは単に最小値の削除)は短時 間でできること ・ データの中の最小値が(いつでも)短時間で取り出せること ・ 一般に、このような機能を持つデータ構造を ヒープ という 最強を決める ・ 学校で一番足の速い人を決めよう ・ こういう場合、全員一度にレースをすることはできないので、 まず、各クラスで一番を決め、各クラス1番の中でさらに一番 を決める ・ クラスの中も、さらに小さいグループに分けて決める ・ 高校野球は、最強を決めるのに、一回 2チームしか評価でき ないので、トーナメントをする 最小を決める ・ 数値データでも同じようにトーナメントをしたとしよう (最小を決めるトーナメント) ・ さて、数値が1つ、値が変わった場合、最小が変わるかもしれ ないが、どうチェックしたらいいだろう ・ 数値が小さくなった場合は簡単。最小と、変わった数値を比 べればいい。つまり、数値の挿入&数値の変化は小さくしか ならない、ならば最小値だけ覚えておけば十分 ・ 最小値が大きくなった場合 (あるいは削除した場合)、最小値を 計算しなおさなければならない??? 再計算は一部でいい ・最小値が大きくなった場合、どこを計算しなおさなければいけ ないだろうか? 実は、全部ではない ・ 数値が変わったものから上に行くルート上の対戦カードだけ、 チェックしなおせばいい ・ 逆に見れば、自分の下に変化した 数値がある対戦カードだけ、 チェックしなおせばいい 再計算の計算時間 ・再計算の手間はどれくらい? トーナメント表の高さ (トーナメント表のことを ヒープ木 とよぶこともある) ・ 頂上から1つずつレベルを下げていくと、チームの数は倍倍 で増えていく ・ 頂つまり、一番下の数値のところにいくまでに log2n +1 回か かる ・ データ更新の計算時間は O(log n ) 挿入と削除 ・ ヒープに新しく要素を追加するときは、ヒープ木の一番右側に 追加する ・ ヒープから要素を削除するときは、削除する要素にヒープ木 の一番右側の要素を代入してヒープ木を更新したあと、右端 の要素を削除し、ヒープ木の更新をする ・ どちらも計算時間は O(log n ) ヒープを実現するには ・ さて、ヒープを実際にメモリの中に構築するにはどうすればい いでしょうか? リストのように、セルとポインタを作ればいいかな? ・ ヒープ木の隣接関係がつかめるよう、上と左右の下のセルに ポインタを張る ・ 実は配列で、ポインタなしで作れる 配列でヒープ構造を ・ ヒープ木を上から1レベルずつ、各レベルを左から右にたどり、 対戦カードに順番に 0 から番号をつける 一番下のレベルが n のとき、最後は 2n-2 になる 配列の大きさは 2n-1 ・ 自分の上・左下・右下が、計算で出せる 0 2 1 3 4 5 7 8 9 10 11 12 6 隣のセルの添え字 ・ ヒープの対戦カード(以後セルといいます)i の 上 ( 親 という) (i-1)/2 (切捨て) 左下 ( 左の子 という) i*2+1 右下 ( 右の子 という) i*2+2 ・ i がn-1 より大きいなら、子はいない 0 2 1 3 4 5 7 8 9 10 11 12 6 ヒープの構造体 ・ ヒープの構造体は、配列と、配列の大きさと、ヒープの大きさ ・ i 番目のセルの値を a に変更するルーチン typedef struct { void AHEAP_chg ( AHEAP *H, int i, int a ){ int *h; // array for values int j; int end; // size of array H->h[i] = a; int num; // current size of heap while ( i>0 ){ } AHEAP; j = i - 1 + (i%2)*2; // j := sibling of i if ( H->h[j] < a ) a = H->h[j]; i = (i-1) / 2; // i := parent of i if ( H->h[i] == a ) break; // no need to update H->h[i] = a; } } 挿入と削除 ・ 新しいセルを挿入する際には、num を1つ大きくして、最後にセ ルの a に変更 void AHEAP_ins ( AHEAP *H, int a ){ H->num++; H->h[H->num*2-3] = H->h[(H->num*2-2)/2] AHEAP_chg ( H, H->num*2-2, a); } void AHEAP_del ( AHEAP *H, int i ){ AHEAP_chg ( H, i, H->h[H->num*2-2]); AHEAP_chg ( H, (H->num*2-2)/2, H->h[H->num*2-3]); H->num--; } 0 2 1 3 4 5 7 8 9 10 11 12 6 最小値を持つセルの検索 ・ 一番上(セル i )からスタートして、最小値を持つ子どもの方に 降りていく int AHEAP_findmin ( AHEAP *H, int i ){ if ( H->num <= 0 ) return (-1); while ( i < H->num-1 ){ if ( H->h[i*2+1] == H->h[i] ) i = i*2+1; else i = i*2+2; } return ( i ); } 0 2 1 3 4 5 7 8 9 10 11 12 6 閾値以下の値を全て見つける ・ 一番左にある、閾値以下の値を持つものを見つける int AHEAP_findlow_leftmost ( AHEAP *H, int a , int i ){ if ( H->num <= 0 ) return (-1); if ( H->h[0] > a ) return (-1); while ( i < H->num-1 ){ if ( H->h[i*2+1] <= a ) i = i*2+1; else i = i*2+2; } return ( i ); } ・ (セル i )の直右にある、閾値以下の値を持つものを見つける int AHEAP_findlow_nxt ( AHEAP *H, int i ){ for ( ; i>0 ; i=(i-1)/2 ){ if ( i%2 == 1 && H->h[i+1] <= a ) return ( AHEAP_findlow_leftmost ( H, a, i+1 ); 1 } 3 4 return ( -1 ); } 0 2 5 7 8 9 10 11 12 6 ヒープの利用例 ・ 数列をソートする(小さい順に並べ替える) -数列の数値を全てヒープに挿入する -小さい順に1つずつ取り出す ・ グラフ構造からクラスタリングをする 0 2 1 3 4 5 7 8 9 10 11 12 6 ヒープの利用例:ハフマン木の構築 ・ (単語などが) n 個あり、それぞれ頻度がある ・ 頻度が最小なものと、その次のものを列の数値を全てヒープに 挿入する -頻度が小さい順に2つ取り出す -両者を合併し、1つになるまで同様に繰り返す ・ 右の子、左の子に01を割当ててコード化すると、各文字を01の コードに割当てられる。 35 最適な圧縮形式になっている 15 20 11 7 A9 B6 C5 D4 E3 F8 練習問題:ヒープ ・ 以下の数値でヒープを作り、そこに 7, 2, 13 を順に挿入せよ 4, 6, 8, 9, 11, 15, 17 メモリ効率を上げる ・ このヒープは、値を n 個覚えるのに、メモリを 2n-1 使う 2倍程度使っている ・ もう少し効率良くならないか 中間セルにも値が蓄えられているようにすればいい 0 2 1 3 4 5 7 8 9 10 11 12 6 教科書にあるヒープ ・ 普通、教科書に出てくるヒープは、このように全てのセルに固 有の値を蓄えるタイプ ・ そして、「親は子どもより値が小さい」という性質を満たすよう に、データを更新する 頂上の頂点が最小値になる 0 2 1 3 4 5 7 8 9 10 11 12 6 ヒープの更新 - 値の変更は、小さくなったら、親と入れ替え、大きくなったら、 子と入れ替え、を再帰的に - 挿入は、一番右下にセルを追加 - 削除は、一番右下をコピーし、値を更新し、サイズを1つ減少 ・ だいたい、先ほどのヒープと同じやり方でできる 0 2 7 9 8 3 10 11 9 10 4 4 7 数値の変更のコード ・ ヒープの構造体は、前回と同じ ・ i 番目のセルの値を a に変更するルーチン void HEAP_chg ( AHEAP *H, int i, int a ){ int aa = H->h[i]; H->h[i] = a; if ( aa > a ) HEAP_chg_up ( H, i ); if ( aa < a ) HEAP_chg_down ( H, i ); } typedef struct { int *h; // array for values int end; // size of array int num; // current size of heap } HEAP; ヒープの更新 (上り) ・ ヒープを更新する際には、値を小さくしたときは上に、値を大き くしたときは下に、親子の入れ替えをしながら進む void HEAP_up ( AHEAP *H, int i ){ int a; while ( i>0 ){ if ( H->h[(i-1)/2]<= H->h[i] ) break; a = H->h[(i-1)/2]; H->h[(i-1)/2] = H->h[i]; H->h[i] = a; i = (i-1)/2 } } typedef struct { int *h; // array for values int end; // size of array int num; // current size of heap } HEAP; ・ セルの値を変更すると、場所が入れ替わるので、各値の場所 を覚えておきたいときには不向き ヒープの更新 (下り) ・ 値を大きくしたときは、子どもより親のほうが小さくなるかもしれない ・ その際には、子どもと親を入れ替えるのだが、その際に、値の小さいほう の子ども、と入れ替える必要がある void HEAP_down ( AHEAP *H, int i ){ typedef struct { int ii, a; while ( i<H->num/2 ){ int *h; // array for values ii = i*2+1; int end; // size of array if (i*2+1 < H->num && H->h[ii]>H->h[ii+1]) ii = ii+1; int num; // current size of heap if ( H->h[ii] >= H->h[i] ) break; } HEAP; a = H->h[ii]; H->h[ii] = H->h[i]; H->h[i] = a; i = ii; } } 閾値以下の値を全て見つける ・ 再帰呼び出しで、比較的簡単に int HEAP_findlow ( AHEAP *H, int a , int i ){ if ( i>=H->num ) return; if ( H->h[i] > a ) return; printf (“%d\n”, H->h[i] HEAP_findlow ( H, a, i*2+1) HEAP_findlow ( H, a, i*2+2) } 0 2 7 9 8 3 10 11 9 10 4 4 7 練習問題:ヒープ (2) ・ 以下の数値で、教科書ヒープを作り、そこに 7, 2, 13 を順に挿 入せよ 4, 6, 8, 9, 11, 15, 17 ~余談~: 実際のヒープの速度 ・ ヒープを検索/更新する時間は O(log n) ・ しかし、実際にプログラミングしてみると、100万要素くらいあっ ても、普通にメモリアクセスするのに比べて 4-5倍程度しか時 間がかからない (log2 100万 ≒ 20 ) ・ なぜだろうか ~余談~: 実際のヒープの速度 (2) ・ ヒープを検索/更新するときは、葉から根までを更新する ・ 1回アクセスすると、アクセスした部分はキャッシュに入り、次 回短時間でアクセスできる ・ 何回も繰り返すと、根に近い部分は全部キャッシュに入ってし まう。時間がかかるのは、下の部分だけ ・ キャッシュに入らないのが、4-5レベル ということ このあたりで木に関する用語の定義を ・ (グラフ理論では)頂点(あるいは点、ノード)、が 枝 で結ばれた構造をグ ラフという(枝は、2つの頂点を結ぶ) ・ グラフの中で、わっかになった構造を持たないものを木という ・ 頂上の頂点(根という)が指定されている木は根付き木という(アルゴリズ ムやプログラミングでは、これを木という) ・ 根付き木の頂点 x に対して、 - x に隣接し、 より根に近い頂点を(上の頂点)を親という - それ以外の隣接する頂点を子という - 頂点 x と根を結ぶ線上にある頂点を x の先祖という - x が先祖である頂点を x の子孫という - x の子孫からなる木を x の部分木という ・ 子どもを持たない頂点を 葉 という ・ 子どもを持つ頂点を 内点 という ・ 根から最も遠い葉までの距離を木の高さ(深さ)という ・ 子どもの数が2以下である木を2分木という ・ 子どもの数が0か2である木を完全2分木という (ヒープは完全2分木) 任意の値を見つけたい ・ ヒープはシンプルで良いのだが、やはり、任意の値を短時間 で見つけられるようにしたい ・ ヒープのように、トーナメント表のような構造を使うのはいい が、挿入の位置が固定されているので、順番を保持すること ができない(削除もそう) ・ 順番を保持できるようにする ためには、任意の場所に挿入 できないといけない 0 2 1 3 4 5 7 8 9 10 11 12 6 順番がそろっていると ・ 数値が順番どおりに入っていると、2分探索を、頂上から下 にたどることで実現できる ・ 各ノードには、その子孫の最大の値を書くようにする 見つけたい数値がどちらにあるか、子どもを見ればわかる ・ では、順番を守ったまま、 挿入&削除をすることにしよう ・ 木の形はゆがんでもいいことにしよう ・ 前の数値があるノード(葉という) の下に子を2つ作り、挿入 ・ 葉を消して隣の葉の数値を 親にコピー ゆがみをゆるすと ・ 検索の時間は、見つける葉までの深さ、ない場合は、隣合う 葉の深いほうの葉の深さに比例 ・ 木の形がきれい(バランスが取れている)なうちは、検索が速 いが、やたら深いところが出てくると、遅くなる 同じところに挿入が続いた場合 ・ 検索の高速性を実現する ためには、何か工夫をしないと ゆがみを矯正 ・ 検索の時間、最適なものは O(log n ) ・ では、定数 c に対して、c log n を超えないように調整しよう ・ 木に深いところがあるならば、必ずどこかに、浅い場所がある 浅い場所を深くして、深い場所を浅くできればいい その際、順番を変えないように ・浅いところを下げ、 深いところを上げ、 親を付け替えて、 バランスをとろう バランスをとる ・ 左右の子どもの部分木の高さが2以上異なる頂点が親子で続い ているところを、構造を変えて高さをそろえる (このような局所的な変更をオレンジ色の頂点に対する ローテート という) ・ ローテートすると高さがそろう(高さの差が2減る) 2以上 木の高さは抑えられるか? ・ 「左右の子どもの部分木の高さが2以上異なる頂点が親子で続 いているところ」がないようにする(ある限りローテートする) ・ 木の高さ k に関して、何かいえるか??? - 深さ k-1 の頂点(葉)が 1つある (根、あるいは根の子どもで分岐) - 深さ k-2 の頂点が 2つある (深さ 2,3 の頂点で分岐) - 深さ k-h の頂点が 2h-1 個ある (深さ2h,2h+1の頂点で分岐) 木の頂点数は、少なくとも 2k/3 ・ 葉の数が n なら、高さは 3log2 n = O(log n) このようにバランスの取れた木構造を、バランス木 とよぶ 検索の時間 ・ 「ある数値があるか」を調べるには木を根から葉までたどる必 要がある ・ 計算時間は、葉までの距離に比例。最悪でも木の高さ ・ 葉の数が n なら、高さは 3log2 n = O(log n) つまり、検索の時間は O(log n) このようにバランスの取れた木構造を、バランス木 とよぶ ローテートの影響 ・ 頂点 x をローテートをした結果、ローテートする必要がなかった のに、ローテートする必要が出てくる頂点があるか? - x の子孫は大丈夫。部分木の高さに変化がない - x の子孫でも先祖でもない頂点もそう - x の先祖には影響がある ・ つまり、条件を満たした木の構造に局所的な変化があったら、 条件を守るようにするためには、その先祖に対してローテート しなければいけない 頂点の挿入と削除 ・ 木から頂点を削除/木に頂点を挿入すると、一部の頂点がロー テートの条件を満たさなくなる そのような頂点は、挿入/削除した場所の先祖 ・ 高さは高々1しか変わらないので、一回ローテートすれば条件を 満たすようになる ・ 先祖に対して、山登り的にローテートを、不要になるまですれば よい(一回不要になったら、それより上は必ず不要である) ・ 木の高さは O(log n)、ローテートは 定数時間でできるので、挿入/削除 してバランスをとる作業は O(log n) でできる 他の基準によるローテート ・ 2つ下のレベルの部分木の大きさが、元の部分木の大きさの 半分以上であるときに、ローテートする ・ ローテートすると大きさが(1以上)減る 2つ下がると大きさが半分以下になるまでローテートする 20 30 50% 30 20 木の高さ ・ 2つ下がると大きさが半分以下、なので、 2log2 n 回以上下が れない 葉の数が n なら、高さは 4log2 n = O(log n) 20 30 50% 頂点の挿入と削除 ・ このローテートは、先祖に対しても影響しない (ローテートしても子孫数に変化はない) ・ 挿入/削除した頂点の先祖に対して、山登り的にローテートをする ただし、不要になるまですればよい、というわけではない ・ 木の高さは O(log n)、ローテートは 定数時間でできるので、挿入/削除 してバランスをとる作業は O(log n) でできる 2分木の構造体 ・ ヒープのようにポインタなしで実現するのは難しいので、リスト のように隣を指すポインタを用意しよう ・ バランスを取るため、高さやサイズのデータも保持しよう ・ リストのように、配列で実現しても良い typedef struct { BTREE *p; // -> parent BTREE *l; // -> left child BTREE *r; // -> rigth child int height; // height of subtree int height; // height of subtree int value; // (max) value } BTREE; 2分木の利用例 ・ 辞書のデータ、IDの管理 ・ 文書データのキーワード検索 練習問題: 2分木 ・ 以下の2分木をローテートせよ (高さが2以上違う場合、1つ の子どもが子孫に葉を50%より真に大きい割合持つ場合) たくさんの子ども ・ 2分木の内点は、必ず子どもを2人持っていた ・ 2つである意味は? - 更新の速度が最速 - 検索と更新が同じ速さ - 子どもの処理が簡単 ・ 子供2人以上をあり、にするとどうなるだろう ・ 2-3木という、子どもの数が2か3である木がある - 特徴は、どの葉も深さが同じということ - ただし、子どもに関する処理が面倒 (子どもが2人の場合、3人の場合で、 それぞれ最小がどこか、とか) ・ もっと子どもを増やしたら? B木 ・ 子どもの数が B 以下の木データ構造をB木という ・ わざわざ子どもを増やすのは、それなりの動機がある ・ ハードディスクやテープのような、アクセスに時間がかかるが、 そこからある程度まとまったデータを取るのには時間がかから ない、というような記憶装置を使う場合、計算時間のボトルネッ クは、検索/更新する際に、何個のノードを見たか、という点に なる ・ それならば、各ノードの子どもの数を 増やせばよい B木の更新 ・ B木を、「全てのノードがB個の子どもを持つ」とすると、効率が 良いが、このようにきっちり条件を守るのは大変 ・ しかし、どのノードも子どもが2-3人、となると、それはそれで効 率が悪い B/2 以上 B 以下にする 親子、あるいは兄弟で、子どもの数が B/2 以下であるようなも のがあれば、それらを合併して1つにする ローテートすれば、木の高さが O(logB/2 n) になる まとめ ・ ・ ・ ・ 2分探索: 半分に分割して、log n 回で見つける ヒープ: トーナメント表の構造を保持して、最小値を更新 2分木: 木構造のバランスとって、高さを抑える B木: アクセスするノード数を最小化
© Copyright 2024 ExpyDoc