Upload
others
View
3
Download
0
Embed Size (px)
Citation preview
1
アルゴリズム及び実習⑧
馬 青
2
比較による探索に限界が
• 2分探索法は線形探索法より効率よいが、それでも計算量がO(log2n)である。
• 線形探索も2分探索もすべてデータの値の
比較に基づくものであった。結局、比較だけをもとにする探索法ではO(log2n)が計算量の限界である。
3
格段に高速化できる方法はある!
• 例えば、データのとりうる値が1から100の整数に限られているとしよう。この場合、もっとも高速な探索法は1~100の整数を
添え字とする配列を用意するやり方である。探索は、与えられた値を添え字として、この配列の要素を調べるだけで済む。
• 探索1回あたりの計算量はO(1)である。
4
例1
元配列aa[0]:50a[1]:20a[2]:1::a[98]:10a[99]:100
新しい配列tt[0]:t[1]:1t[2]:2t[3]:3::t[99]:99t[100]:100
データ99を探索するなら、t[99]だけを見ればよい。
5
しかし、この方法は
• データの値が限られた範囲の整数である場合にしか使えないので一般的でない。また、整数であってもデータの値が連続していないとき、配列のサイズが無駄に大きい。
• この方法を少し手直しすれば、一般的な探索法とすることができる。これがハッシュ法である。
6
例1(修正後)
新しい配列tt[0]:100t[1]:4t[2]:10t[3]:54::t[99]:1t[100]:78
データ10を探索するなら、10のハッシュ値(ここで2とする)を計算し、t[2]を見ればよい。
関数計算
元配列aa[0]:50a[1]:20a[2]:1::a[98]:10a[99]:100
7
ハッシュ法
• 例1では、データの値そのものを配列の添え字とした。これに対し、例1(修正後)のハッシュ法では、データの値を引数として、ある関数の値を計算する。そして、この関数値を添え字として新しく作成された配列を参照する。
• ここで使う関数をハッシュ関数、ハッシュ関数で求めた値をハッシュ値、新しい配列をハッシュ表と呼ぶ。また、ハッシュ値を求めることをハッシュング(hashing)という。
• ハッシュ関数の計算も配列の参照も、手間はnに無関係である。つまり、この方法によれば、探索はO(1)の計算量で実
現できる。
8
例2(1/3)
整数の探索問題
55436016
9
例2(2/3)
ハッシュ関数として
hash(x)=x mod mを用いる。但し、mはハッシュ表のサイズ、つまり、ハッシュ表用の配列の添え字は0からm-1の範囲内である。ここでm=11とすれば、
hash(55)=55%11=0 55→t[0]hash(43)=10 43→t[10]hash(60)=5 60→t[5]hash(1)=1 1 →t[1]hash(6)=6 6 →t[6]
mod:余りを求めるという意味C言語では%またはfmodである
10
例2(3/3)配列t[](ハッシュ表)が以下のようになる
t[0]: 55t[1]: 1⋮t[5]: 60t[6]: 6⋮t[10]: 43
例えばx=43を探索する場合、
hash(43)=10なので、直接t[10]を参照すればよい 。
つまり、 x=t[10]かどうかをチェックすればよい。
11
例3(1/3)
文字列の探索問題
TANAKANAKAYAMATAKAJIMASUZUKIYAMAMOTO
12
例3(2/3)ハッシュ関数として
を用いる。
但し、kは文字列の長さで、iはi番目の文字、cは予め設定した定数で、ord(s[i])は文字s[i]の整数値、
mはハッシュ表のサイズである。 つまり、ck-1-iで文字の位置情報を与える。したがって、“ee”のような文字列の場合、2つの‘e’のハッシュ値が異なる。
misordki
ikcshash mod ])[(10
1)( ×∑−
=−−=
A-Z: 65-90
a-z: 97-122
0-9: 48-57
13
例3(3/3)
例 え ば 、 c=28=256, m=101 と す れ ば 、 文 字 列“SUZUKI”の場合、
hash(“SUZUKI”)=(2565*83+2564*85+2563*90+2562*85+2561*75+2560*73)%101=25なので、t[25]に“SUZUKI”を与える。
(Cであれば、strcpy(t[25], “SUZUKI”),tは2次元配列)
一般的にhash(s)=vならstrcpy(t[v], s)
このように、配列(ハッシュ表)を作ることができる。そして、“SUZUKI”を探索したければ直接t[25]を参照すればよい。
14
ハッシュ関数
ハッシュ法においてはよいハッシュ関数を用いるのが重要である。特にハッシュ関数として特定されたものはなく、用途に合わせて用いられる。ただ、上の例にも示しているように、データが整数の場合は、それをmで割った余りをハッシュ関数(つまり、h(x)=x mod m)とするのが大低の場合よい(あるいはh(x)=(a*x+b) mod m)。但し、一般的にa, b, mは素数を取るのがよい。
データが文字列の場合は、例3のように、まず何らかの方法で整数値を得て、それをmで割った余りを用いれ
ばよい。例3に示しているハッシュ関数はよく使われる。また、1文字を8ビットで表現する計算機なら、c=256(28)とする場合が多い。
素数って何?
15
素数とは, 互いに素とは
• 素数とは、1以外の数で1と自分自身しか約数がない整数のことをいう。例えば、2, 3, 5, 7…などが素数である。
• 互いに素とは、1と-1以外に共通の約数を持たない2つの整数のことをいう。
16
Question
• なぜ余剰演算が必要?
• なぜmは素数がよい?
17
演習問題1
• 任意の与えられた値について、それが素数かを判定するプログラムを作成しなさい。
• 実行例:
./a.outinput a number: 10素数でない。
./a.outinput a number: 5素数
18
演習問題2
• 任意の与えられた値について、それ以下のすべての素数を求めるプログラムを作成しなさい。
• 実行例:
input a number: 102357
19
ハッシュ表のサイズmを求めるアルゴリズム
入力:元配列に格納されているデータの個数nと値α(=m/n)。ただし、α>1
出力:ハッシュ表のサイズm(値αn以上のもっとも小さい素数)
補助: i, j, fg
?
20
衝突
普通、データのとりうる値の種類(処理対象とするデータの数ではない!)に比べて、配列の添え字として使える値の範囲が小さい。そのため、異なるデータが同一ハッシュ値を持つことがある。例えばハッシュ関数hash(x)=x mod 11を用いた場合、11を割り切れる番号(22, 33, ...)がみんな0のハッシュ値を持つ。このような事態を「衝突」と呼ぶ。この「衝突」を処理するのがハッシュ法で
の重要な処理である。
21
衝突の処理
処理法としては次の二つの方法がよく使われる。
(1)同じハッシュ値を持つデータをリストでつなぐ方法(チェイン法)
(2)衝突が起きた場合、別のハッシュ関数を使って再度ハッシュする方法(オープンアドレス法、ある
いは開番地法) 。この操作を再ハッシュ(rehasing)という。
参考
22
チェイン法
ハッシュ表の各要素には、このリストの先頭を指すポインターを入れる。ハッシュ関数を計算して特定のリストを選んでから、リストの上で探索を行う。
ハッシュ表 同じハッシュ値を持つデータのリスト
この方法の欠点は、同じハッシュ値を持つキーがたくさんあって(つまり特定のチェインが非常に長い)、ハッシュ法の意味がなくなる。
参考
23
オープンアドレス法(開番地法)
• 一般的には、m個のハッシュ関数h1, h2, ...,hmを用意し、衝突が起きないまでそれらの関数を用いて順に調べる方法である。
• このうち、もっとも簡単な方法として、線形走査法がある。
24
線形走査法
これは最初の場所の隣、その隣、さらにその隣と、配列番号を1つずつ増やしながら調べていく方法である。つまり、
h1(x)=h(x)h2(x)=h(x)+1h3(x)=h(x)+2⋮
とした場合に相当する。
25
線形走査法について
• 簡単で確実な方法である。すべての場所を探せるので、確実である。
• ただ、特に表がいっぱいになり始めると「クラスター」と呼ばれる現象が生じやすくなり、再ハッシュの回数が増える可能性がある。
• クラスター現象:
あるデータのハッシュ値がhだとすると、ほかのデータのハッシュ値がh近辺の可能性が高く、その近辺にクラス
ター、つまりデータのかたまりができやすい。このような現象をクラスター現象という。クラスターができてしまうと、線形走査法では効率が落ちる。
参考
26
例4:線形走査法(1/2)
以下のデータについて考える(65,66,75,76,77)
ハッシュ表のサイズを11とし、ハッシュ関数を次のように定義する。
h(x) = x mod 11h1(x)=h(x)h2(x)=[h(x)+1] mod 11h3(x)=[h(x)+2] mod 11⋮
27
例4:線形走査法(2/2)各データに対してハッシュ値をh1(x)を用いて計算すると、
h1(65)=10h1(66)=0h1(75)=9h1(76)=10 衝突が起きるのでハッシュ値を
再度計算h2(76)=0再度計算h3(76)=1
h1(77)=0 衝突が起きるのでハッシュ値を
再度計算h2(77)=1 再度計算h3(77)=2
28
2重ハッシュ法、2次ハッシュ法
2つのハッシュ関数hとgを用意し、以下のように
h1(x)=h(x)h2(x)=[h(x)+g(x)] mod mh3(x)=[h(x)+2g(x)] mod m
求める方法を2重ハッシュ法(double hashing)という。
また、 hi(x)=[x+(i-1)2] mod m で計算する2次ハッシュ法(quadratic hashing)と呼ばれる方法もある。
これらの方法の狙いは h1, h2, …などを互いに独立
なものにし、クラスター(データのかたまり)をできにくくすることにある。
29
g(x)は注意して選ぶ!• g(x)は注意して選ばないと、プログラムがまったく正しく働かない。
• g(x)は以下の点を考慮して選ぶ
– g(x)の値が0とならないこと
– g(x)の値とmが互いに素であること(たとえばm=2×(g(x)の値)の場合、再ハッシュ値が
短い範囲で繰り返される)。したがって、mが素数でないといけない。
– g(x)がh(x)と異なっていなければならない。
• 上記ことを考えて、実際はg(x)=q-(x mod q)のようなもので十分のようである。ここのqはmより小さな整数である。
参考
30
例5(1/2)以下のデータについて考える
(65,66,75,76,77)
ハッシュ表のサイズを11とし、ハッシュ関数を次のように定義する。
h1(x)=h(x)h2(x)=(h(x)+g(x)) mod 11h3(x)=(h(x)+2g(x)) mod 11⋮
ただし、h(x)=x mod mg(x)=8-(x mod 8)
2重ハッシュ法
31
例5(2/2)
各データに対してハッシュ値をh1(x)を用いて計算すると、
h1(65)=10h1(66)=0h1(75)=9h1(76)=10 衝突が起きるのでハッシュ値を
再度計算h2(76)=[10+(8-76%8)]%11=3h1(77)=0 衝突が起きるのでハッシュ値を
再度計算 h2(77)=[0+(8-77%8)]%11=3再度計算→ h3(77)=[0+2*(8-77%8)]%11=6
32
演習問題例5において、2次ハッシュ法を用いるとする。つまり、hi(x)=[x+(i-1)2] mod mをハッシュ関数として用いる。ただし、 m=11問1:h1(x),h2(x),h3(x)の式を具体的に示しなさい。問2:h1でハッシュ値を求めると、
x=65, h1=10x=66, h1=0x=75, h1=9x=76, h1=10x=77, h1=0
が得られる。76と65が衝突しているのがわかる。76の新しいハッシュ値を求めなさい。問3:そのとき、衝突は解消したか。理由をつけてのべなさい。問4:解消していないと答える人はさらに新しいハッシュ値を求めなさい。
33
ハッシュ表作成アルゴリズム入力:n個のデータが格納されている配列a, ハッシュ配列tのサイ
ズm, ハッシュ回数の上限NHASH_LIM出力:ハッシュ表配列t, 最大ハッシュ回数nhash_max補助:1 nhash_maxと配列tについて初期化2 i=0からn-1について以下を実行
2.1 ハッシュ関数でa[i]のハッシュ値vを計算し、nhash=1とおく2.2 nhash<NHASH_LIM、かつ、t[v] にすでに(初期値以外の)
値が入っていれば、以下を繰り返す2.2.1 他のハッシュ関数でハッシュ値を再度計算する2.2.2 nhashの値を更新する2.2.3 必要ならnhash_maxも更新する
2.3 nhash>= NHASH_LIMであれば、エラーメッセージを出力し、処理を終了
2.4 ハッシュ表配列にデータを格納する
34
ハッシュ探索アルゴリズム
入力:探索キーx, ハッシュ表のサイズm, ハッシュ表配列t, 最大ハッシュ回数nhash_max
出力:キーxがハッシュ表の中の位置p(ない場合-1)補助:
1 xのハッシュ値vを計算し, nhashの値を更新する
2 nhash<nhash_maxかつx≠t[v]かつt[v] にすでに(初期値以外の)値が入っていれば、以下を繰り返す
2.2.1 xのハッシュ値vを再度計算
2.2.2 nhashの値を更新する
3 ?であればp=vとおく
そうでなければp=-1とおく
35
平均ハッシュ回数の実験結果
• 5種類10万個の(0~RAND_MAXまでの)乱数を使用
• ハッシュ表作成時の平均ハッシュ回数
α(m/n) 2.0 1.7 1.4 1.25 1.1 1.05
線形 1.5 1.7 2.3 2.98 5.84 11.36
2重 1.4 1.5 1.8 2.2 3.3 5.06
2次 1.4 1.6 1.9 2.18 2.9 3.52
αの値が小さくなるにつれ、各手法に性能差が大きくなる。
36
第8回実習課題
1.任意の与えられた値について、それ以下のすべての素数を求めるプログラム(ex08-prime-table.c)を作成しなさい。実行例:./a.outinput a number: 102357
37
第8回実習課題2.ハッシュ表のサイズを求めるアルゴリズムを実現する関数int
CalM(int n, double α)を作成し、その動作を確認できるプログラム(ex08-calm.c)を作成しなさい。ただし、求めたサイズを関数の戻り値とする。
注意:例えば整数変数にnα=6x1.2を与えると、値が7となってしまう。このような問題はceil関数を用いるとよいだろう。この関数の意味・使い方などは各自で調べよう。
3.ハッシュ表作成アルゴリズムを実現する関数intMakeHashTable(int n, int m, int a[], int t[])を作成し、その動作を確認できるプログラム(ex08-makehashtb.c)を作成しなさい。ただし、
(1)aは元データ配列,tはハッシュ表配列,nは元データの個数,mはハッシュ表のサイズである。
(2)元データはゼロ以上の整数でweb上のファイルex08-a.datから読み込む(n=100000)。
38
第8回実習課題
(3)ハッシュ表のサイズmを125003とする(任意に指定した
αとデータ個数nに対し、課題2の関数を利用して求めることを発展課題とする)。
(4)再ハッシュには線形走査法を用いる(2次ハッシュ法、2重ハッシュ法を用いることを発展課題とする。
(5)最大ハッシュ回数nhash_maxを関数の戻り値とする。
(6)main()プログラムにおいてm, nhash_max, ハッシュ表(配列t)を結果ファイルex08-t.datに出力すること。
(7)m, 平均ハッシュ回数, nhash_maxを計算機画面上に出力すること。
39
第8回実習課題
4.ハッシュ探索アルゴリズムを実現する関数intHashSearch(int m, int nhash_max, int t[], int x)を作成し、その動作を確認するプログラム(ex08-hsearch.c)を作成しなさい。ただし、
(1)xは探索データである。データの位置情報を関数の戻り値とする(見つからないとき-1)。(2)m, nhash_maxとハッシュ表(配列t)は課題3で得られたファイルex08-t.datから読み込むこと
(3)ファイルの読み込みはファイルポインターを使うこと。
発展課題
40
課題3のヒント(1/2)• これまでファイルへの出力は./a.out>ex08-t.datというようにやってきたが、画面へも同時に出力したいとき(今回はm, 平均ハッシュ回数など)は難
しい。これは、プログラムの中を以下のように書くことで解決できる。
FILE *fpopt;fpopt=fopen(“ex08-t.dat”, “w”);fprintf(fpopt, “...”, ...);fclose(fpopt);
このような場合、実行は“>ex08-t.dat”の部分が不要。
41
課題3のヒント(2/2)• 今回の課題では、ファイルからのデータ入力は./a.out<ex08-a.datというようなやり方でもOKだ
が、前のページのファイル出力の方法に合わせてプログラムの中を以下のように書くこともできる。
FILE *fpipt;fpipt=fopen(“ex08-a.dat”, “r”);fscanf(fpipt, “...”, ...);fclose(fpipt);
このような場合、実行は“<ex08-a.dat”の部分が不要。
42
課題3の実行例$ ./makehashtablem: 125003avg. nhash: 3.0nhash_max:176$ more ex08-t.dat ←ファイルの中身を見る125003 ←m176 ←nhash_max1896920525 ←t[0]832394972 ←t[1]1627789068 ←t[2]-1 ←t[3] ←初期値1027774670 ←t[4]1968672252 ←t[5]-1⋮
43
課題4の実行例
$ ./hsearchInput the search key: 1027774670nhash: 1Search result: 4Input the search key: 100000000nhash: 2Search result: -1Input the search key: Ctrl-D