1 / 44

基礎的なデータ構造

基礎的なデータ構造. ・ スタックとキュー ・ リスト ・ バケツとハッシュ. データを記憶する. ・ コンピュータの基礎的な操作の1つは、データ(情報)を記憶すること ・ しかし、どのように記憶するかで、利用効率が変わる  記憶させる手間、検索する手間... - 来たデータを本に 1 ページ目から順に書き込んでいく - 棚を作って整理する ・ 使用用途によって、記憶の仕方(データの構造)を工夫する必要がある. データを記憶する記憶の仕方を、データ構造という. コンピュータの記憶の仕方. ・ コンピュータの記憶領域は、メモリ。メモリ 1 単位に値が1つ入る

grazia
Download Presentation

基礎的なデータ構造

An Image/Link below is provided (as is) to download presentation Download Policy: Content on the Website is provided to you AS IS for your information and personal use and may not be sold / licensed / shared on other websites without getting consent from its author. Content is provided to you AS IS for your information and personal use only. Download presentation by click this link. While downloading, if for some reason you are not able to download a presentation, the publisher may have deleted the file from their server. During download, if you can't get a presentation, the file might be deleted by the publisher.

E N D

Presentation Transcript


  1. 基礎的なデータ構造 ・ スタックとキュー ・ リスト ・ バケツとハッシュ

  2. データを記憶する ・コンピュータの基礎的な操作の1つは、データ(情報)を記憶すること ・しかし、どのように記憶するかで、利用効率が変わる 記憶させる手間、検索する手間... -来たデータを本に1ページ目から順に書き込んでいく -棚を作って整理する ・使用用途によって、記憶の仕方(データの構造)を工夫する必要がある データを記憶する記憶の仕方を、データ構造という

  3. コンピュータの記憶の仕方 ・コンピュータの記憶領域は、メモリ。メモリ1単位に値が1つ入る -来たデータを本に1ページ目から順に書き込んでいく   メモリを配列で確保して、来たデータを、添え字の小さいところから順に書き込んでいく。スタック、キューなど -棚を作って整理する  リスト、ヒープ、2分木、バケツ、ハッシュなど どのようなデータ構造があるのか、順に見ていこう

  4. 配列による記憶 ・問題です。配列にデータを記憶させたいとします。さて、ここに、データ(数字だと思っていいでしょう)が1つずつやってきます。さて、どうやって記憶しますか?  最後に記録したところの次に書き足せばいいんじゃない?  その通り! ・ ただし、メモリには「書き込まれていない」という状態がないので、どこが最後かわからない ・ たとえあっても、メモリ全体を見渡せるわけではないので、「書き込まれたところの最後」を見つけるのは大変 ・ なので、「書き込んだ最後の場所」を記憶する変数を使う

  5. スタック ・ 例を見てみましょう 値 値 配列 カウンタ 0 1 2 ・ このように、「配列」と「最後に書いた場所のカウンタ」からなるデータの構造を スタック という (カウンタのことを、スタックポインタ といいます)

  6. 値の消去 ・今度は、値を記憶するだけでなく、読み出し、および、読み出したものを消去する方法も考えましょう - どれでもいいから1個読み出して消したい   最後に記録したところを読み出して、カウンタを1減らす - ●番目の値を消したい   ●番目に最後の値を移して、カウンタを1減らす - ▲という値を持つものを消したい   スキャンして見つけないといけない... カウンタ 5 配列 値 値 値 値 値

  7. スタックの関数 ・スタックを実現するには、以下のような関数を作ればよい ・使うときは、スタックと記憶する値をセットで渡して呼び出す int STACK_push ( STACK *S, int a){ if ( S->t == S->max ) return (1); // overflow error S->h[S->t] = a; S->t ++; return (0); } int STACK_pop ( STACK *S, int *a){ if ( S->t == 0 ) return (1); // underflow error S->t --; *a = S->h[S->t]; return (0); } typedef struct { int *h;// array for data int end; // size of array int t; // counter } STACK t end h[] 値 値 値 値 値

  8. スタックの利用例 ・文字列を読み込んで、逆向きにする ABCDEFGH ・ワープロの undo 機能 end h[]

  9. ~余談~:あふれないスタック? ・スタックは、値をたくさん書き込むと、配列からあふれてしまう。そりゃそうだ。 ・しかし、あらかじめ書き込みの上限がわからないこともある。 (例えば、数字がたくさんあるファイルを読んで、スタックにためる、といった場合。最初にスキャンすればわかるけど…) ・あふれたら、新しく大きな配列を取り直して、内容をコピーする、というようにすれば、あふれても対処できる ・しかし、サイズを1つずつ大きくしたのでは、毎回あふれてコピーすることになり、無駄が多い

  10. ~余談~:あふれないスタック? (2) ・配列を取り直す際には、例えば、現在のサイズの2倍の大きさの配列を取り直すようにする 一度あふれたあとは、同時に使われているメモリの大きさは、常に記憶している要素の数の3倍で抑えられる ・コピーにかかる手間の総量も、現在の配列の大きさの2倍で抑えられる  計算量の意味で、損失はない

  11. ~余談~:あふれないスタック? (3) ・コードを書くと、このようになる void STACK_push ( STACK *S, int a){ if ( S->t == S->max ){ // overflow error int i, *h = malloc (sizeof(int)*max*2 ); // using realloc is easy for ( i=0 ; i<S->t ; i++ ) h[i] = S->h[i]; free ( S->h ); S->h = h; } S->h[S->t] = a; S->t ++; }

  12. F I L O ・ どれでもいいから1個読み出して消したい   例えば、ユーザーが指定した点に★マークを書いて、消しゴムボタンを押したときに全部消す、というようなときに使う(場所を記憶する) - 最後に書き込んだものが、最初に読み出されることになる - このような、データの読み出され方を FILO(First In Last Out)という カウンタ 5 値 値 値 値 値 配列

  13. キュー、FIFO ・ 読み出し&消去をするときに、「最初に書き込んだものが最初に出てくる」ようにしたい (FIFO、 First In First Outという)   例えば、ユーザーが指定した点に★マークを書いて、消しゴムボタンを押したときに、最初のほうから消していく、というようなときに使う(場所を記憶する)   窓口サービスなどは、皆このタイプ ・ このようなデータ構造をキューという - 最後に書き込んだものが、最後に読み出されることになる カウンタ 5 値 値 値 値 値 配列

  14. キューのカウンタ ・ キューを実現するには、「どこが最初に書き込まれたか」を覚えておく必要がある   つまり、書き込む場所と、読み込む場所を覚えておくため、2つカウンタが必要   読み出す場所を、先頭という意味で head、 書き込む場所を、最後と言う意味で tailとよぶ head 2 tail 7 値 値 値 値 値 配列

  15. あふれたら ・ スタックは、配列の大きさ以上の書き込みをするとあふれる ・ キューは、記憶している数が、配列の大きさ以上でなくてもあふれる  あふれたときは、tailを配列の先頭に戻す headも、最後まで来たら先頭に戻す ・ 本当にあふれるのは、taillが headを追い越すとき head 5 tail 10 値 値 値 値 値 配列 0

  16. 値 値 値 値 値 値 値 値 値 追い抜きの調整 ・tailが head を追い抜くときは、実は調整が必要 ・ 配列の数と同じだけ書き込むと、 tailは headにおいつく   つまり、両者は等しくなる  これは、何も書き込んでいないときと同じ  両者を区別できない! ・ 解決策は  - 両者を区別するためのフラグを用意する  - (配列の大きさ-1)までしか書き込まない head 5 tail 5 配列

  17. キューの関数 ・スタックを実現するには、以下のような関数を作ればよい ・使うときは、スタックと記憶する値をセットで渡して呼び出す int QUEUE_ins ( QUEUE *Q, int a){ if (( Q->t +1 ) % Q->end == Q->s ) return (1); // overflow error Q->h[Q->t] = a; Q->t = ( Q->t +1 ) % Q->end; return (0); } int QUEUE_ext ( QUEUE *S, int *a){ if ( Q->s == Q->t ) return (1); // underflow error *a = Q->h[Q->s]; Q->s = ( Q->s +1 ) % Q->end; return (0); } typedef struct { int *h;// array for data int end; // size of array int s; // counter for head int t; // counter for tail } QUEUE t s end h[] 値 値 値

  18. キューの利用例 ・1つずつ数を入力し、ときどき5個ずつ書き出す ・マウスカーソルの軌跡を描画する (マウスの座標を連続的にキューに蓄え、1秒間に30コマとか、ある程度時間が経過したものから消す) end h[]

  19. リスト: 順番を守って挿入削除 ・ 配列は簡単で便利だが、「順番を維持して挿入/削除」をするときに弱みが出る ・ 他の操作の便利さを犠牲にしてもいいから、この点を何とかできないだろうか???   ランダムアクセス性(k番目の要素へのアクセスが一手でできる)を犠牲にしてもよいとする  - 窓口サービスで、割り込みを許す、あるいは途中のキャンセルを許す場合  - 文章の編集(段落単位とか)をする場合。途中に絵を挿入するたびに、全部移動させたくない

  20. アイディア:鎖と同じ構造を ・ 鎖は挿入削除が簡単。でも、k 番目を見つけるのは面倒   この構造をまねよう ・ 鎖の構造は、隣との隣接関係が確保されていて、場所は自由。場所が自由なのはともかくとして、隣接関係を記録することにしよう ・ 記憶する場所それぞれ(セルとよぶ)に、お隣さん(両隣)の情報を記録。つまり、各セルは3つの値からなる  - 記憶するもの  - 前のセル(場所か名前、つまりポインタか添え字)  - 後ろのセル(場所か名前、つまりポインタか添え字) 1 5 7 3

  21. 挿入削除の戦略 ・ 鎖をつけかえるときは何をするか?   入れるところ、抜くものの周り、の隣接構造を切る   要素に対して、「隣は何か」を変える ・ 要素をある場所に挿入するときは、挿入する場所の前後の要素の隣接関係を切り、それを入れる ・ 要素を削除するときは、その要素の前後の要素を直結する ・ 両方とも順番を保ったまま、定数手でできる 1 5 7 3

  22. ポインタを利用した構造体 ・ このような構造体を定義して、新しく記憶するものが来るたびに、新しくメモリを確保するようにする   配列の大きさのような、上限の制約がなくなる 注意: Cで書くときは、LISTの定義 にLISTを使うので、下記のように する必要あり ・ リストの最初のセル(head という) と最後のセル(tailという)を覚えて おく必要がある (LISTで覚えてもいい) typedef struct { LIST *prv; // pointer LIST *nxt; // pointer int h; // value } LIST typedef struct _LIST_ { struct _LIST_ *prv; // pointer struct _LIST_ *nxt; // pointer int h; // value } LIST

  23. コード(初期化) ・初期化では、LIST 構造を1つ用意して、前と次を自分にして、空のリストを表現 int LIST_init ( LIST *L ){ L->prv = L; L->nxt = L; } ・セルを挿入すると、●の次と前がそれぞれリストの headと tailになる ● ● 1 5 7 3

  24. 挿入 ・挿入は、入れる前のセル(あるいは後ろのセル)を指定して行う。前と後ろのポインタを付け替える int LIST_ins ( LIST *l, LIST *p ){ p->nxt->prv = l; l->nxt = p->nxt; p->nxt = l; l->prv = p; } ・つぎ変えの順番を変えると、正常につぎかえられなくなる ● 1 5 7 3

  25. 削除 ・削除は、入れる前のセル(あるいは後ろのセル)を指定して行う。前と後ろのポインタを付け替える int LIST_del ( LIST *l ){ l->nxt->prv = l->prv; l->prv->nxt = l->nxt; } ・l のポインタはいじらなくて良い (消したときの情報が残っているので、復帰するときに使える) ● 1 5 7 3

  26. 普通の教科書では ・ 一般に、教科書では、リストの両端は NULL のポインタを指すことにする   次/前を指すポインタが NULL なら、そこは端である、と判定する ・ 理論的にきれいだが、プログラミングのときは厄介   隣がNULLのとき、例外処理をしなければいけない  NULLの前に挿入、ということはできないので、挿入を2通り(○○の前に入れる、○○の後に入れる、を用意して、入れる場所によって使い分けなければいけない リスト 1 5 7 3

  27. リストをたどるループ ・リストをたどりたいときは、●から始めて、後ろへ1つずつポインタをたどり、●まで戻ったら終了する LIST *p; int e; for ( p=●->nxt ; p!=● ; p=p->nxt ){ e = p->h; … } ・逆向きのときは prv を使用 ● 1 5 7 3

  28. 復元 ・直前に除去したセルを復元するには、前と後ろのポインタが指すセルの間に、自分を挿入すればよい int LIST_recov ( LIST *l ){ LIST_ins ( l, l->prv); } ・消した順番の逆順に復元 するのであれば、いくつものセルを復元することができる (消したものは、片方向リストにすればよい) ● 1 5 7 3 1 5 7 3

  29. リストの利用例 ・1から nの数字をランダムなセルの次に順番に挿入して、ランダムな列を作る (同じ数字が2度出てこないので、乱数列とは違う) ・時系列にそって、現れたり消えたりするデータを保持する (ある値を持つセルがうまく見つけられるように工夫がいる)

  30. リストの利用例2 ・値を2つもつデータが n個ある。 (x1,y1) ,…, (xn,yn)とする ・yが大きいものから k/nだけデータを集めたときの、 xの値が k‘/m番目である要素が、各 k,k’について知りたい ・普通にやると O( n2 log n)時間 ・ リストを使ってうまくやると O( n( m+log n))時間

  31. リストの利用例2 (2) ・データを x個でソートして、リストにする ・0/m番目から m/m番目を見つける ・次に y でソートした列も作り、 y が小さい順にリストから抜いていく ・0/m番目から m/m番目を更新する。必要に応じて、左右に1つ動かせばよい ・ 感覚的には、O( n2 log n) O( n( m+log n))時間 で n倍速くなる感じ

  32. 片方向リスト ・ 削除がなければ、リストは後ろへのポインタを覚えるだけでできる ・ 例えば、スライドの作成? ・k個のソートされた数列の合併 ● 1 5 7 3

  33. 配列を利用したリスト ・ ポインタを使うとわかりづらい、あるいは、1つずつセルの管理をするのが面倒な場合、配列を使ったリストが使える  利点 添え字がポインタの代わりになるので、     各セルに変数を割当てたい、というよう     なときに面倒がない  欠点 配列なので、大きさを変えるのが面倒 ・ 実用では、意外と、 大きさを変えないですむことが多い ・ リスト全体の構造体を   作ることになる typedef struct { int *prv; // index to previous int *nxt; // index to next int *h; // value } ALIST

  34. 配列リストの例 ・h, prv, nxt の各配列の i 番目の要素が、セル i のh, nxt, prv ・0番目なり、最後なりを特別なセルにして、リストをたどるときは必ずそこからたどるようにする  (そうでないと、どれが「生きているところ」かわからない) ・最初の空リストは、最後のセルの前後を最後のセルにして表現 0 1 2 3 4 (●) h 値 値 値 値 typedef struct { int *prv; // index to previous int *nxt; // index to next int *h; // value } ALIST prv 2 4 0 3 1 nxt 0 1 3 4 2

  35. バケツ ・ キューにしてもリストにしても、順番どおりに記憶することはできるが、参照がしづらい   記憶した数字の中で、1桁のものを全部出してください ・ 少し構造を持たせて、検索が楽になるようにしたい ・ まず、値による分類から始めよう

  36. バケツのアイディア ・ 数字を記憶する際に、それぞれの値ごとに分類して記憶することにする ・ 例えば、0から 99 までの値を持つ数字がいくつも入力され、それを10の位の値で分類する ・10の位、0 から 9に対して、1つずつリストなり配列なりを持たせればよい

  37. バケツの利用例 ・ 例えば、10の位で数値をソートするときに使える ・ 例えば、疎な行列の転置をするときに使える A: 1,9,5 B: 1,2 C: 1,6,7 D: 4,5

  38. 応用:Radix(基数) ソート ・ バケツの、桁ごとのソートを繰り返すと、数値のソートができる ・ 各数値の下の位から順番にバケツソートをしていく。バケツ内の順番は変えないこと ・ バケツが2ついる。ただし、1つにすることもできる。その際は、バケツを一回スキャンして全部をつなげる必要がある。

  39. ハッシュ ・ バケツの分類精度を上げようとすると、バケツの数を増やさなくてはいけない   大きなメモリが必要。ソートするときのように、全てのバケツをスキャンする、という操作にも時間がかかる ・ 正確に分類できなくてもいいから、分類そこそこ、メモリもそこそこ、検索もそこそこ、という間を取れないだろうか?

  40. ハッシュのアイディア ・ 例えば文字列をしまうデータ構造を考える。 ・ 「この文字列Sはデータの中にありますか?」という質問に高速で答えるためにはバケツは便利だが、非常に多くのバケツが必要 ・ しかし、文字列の数自体は少ないだろう ・ そこで、例えば、頭2文字だけでバケツを作ったらどうだろう?  3文字目以降が異なっても、頭2文字が同じなら同じバケツに入る。バケツを共有してメモリを節約している そのため、バケツが空なら、Sはない、と簡単に答えられるが、バケツが空でないときは、バケツの中身を全部調べて、Sがあるかないかチェックしなければならない

  41. 文字列のバケツ ・ 「バケツは数値しか入らないんじゃない?」 ・ その通りなので、文字列は他の場所にしまい、バケツにはそのインデックスを入れる 1: ABCABC 2: ABBBBB 3: CCCBBB ・ 「頭2文字」は数値に変換する。例えばABCからなる文字列の場合は、A=0,B=1,C=2 と思って、3進数2桁の数と解釈 AB 1 、CC 8 となる

  42. データの偏り ・ 文字列をしまう場合、文字列の頭2文字がある程度均質であればいいが、偏っていたらどうしよう?   あるバケツにデータ集中して、のこりは空ばかり、となる ・ ならば、頭2文字でなく、文字列から整数への、均一な写像関数を持ってきたらどうだろうか?  (これをハッシュ関数、データに対する写像の値をハッシュキー、ハッシュ値とよぶ)   実用を考えれば、似たものが異なる値を持つほうがいい ・ 例えば、x1,x2,x3 ,…に対して、 (x1)1+(x2)2+(x3)3 や、 ((x1+1)x2+1)x3… などの、バケツの大きさ(ハッシュの大きさ)の剰余をとったものを使う

  43. ハッシュの大きさ ・ さて、ハッシュ関数のおかげで偏りなくデータをしまえそうだが、ハッシュの大きさはどうしたらいいのだろうか?   基本的に、バケツにデータがたくさん入っていると検索に時間がかかるので、なるべく異なるデータが同じところに入らないようにしたほうがいい   もともと、データをとっておくところは必要なので、それと同じ位はメモリを使ってもいいんじゃないか? ・ ならば、例えば、データの数の定数倍にする。計算量の意味でも損失なし ・ データが増えてきたら、スタックと同じように、サイズを2倍に変更して作り直せばよい

  44. まとめ ・スタックとキュー: 配列とカウンタをセットにして、逐次的にくるデータに対応 ・リスト: 隣接構造を記憶することで、順番を保ちながらの挿入削除を効率化 ・バケツ: 値による分類で検索を容易に ・ハッシュ: ハッシュキーを使ったバケツで、分類精度とメモリ効率の良さを両立させる

More Related