ハッシュテーブルがどのように機能するかについての説明を探しています。
たとえば、私はそれがキーを受け取り、ハッシュを計算し(方法の説明を探しています)、ある種のモジュロを実行して、値が格納されている配列のどこにあるかを計算しますが、ここで私の知識が停止します。
誰かがプロセスを明確にできますか?
編集:ハッシュコードの計算方法については特に質問していませんが、ハッシュテーブルの動作の概要については質問します。
ハッシュテーブルがどのように機能するかについての説明を探しています。
たとえば、私はそれがキーを受け取り、ハッシュを計算し(方法の説明を探しています)、ある種のモジュロを実行して、値が格納されている配列のどこにあるかを計算しますが、ここで私の知識が停止します。
誰かがプロセスを明確にできますか?
編集:ハッシュコードの計算方法については特に質問していませんが、ハッシュテーブルの動作の概要については質問します。
回答:
ここでは素人の言葉で説明します。
ライブラリを本でいっぱいにしたいだけでなく、必要なときに簡単に見つけられるようにしたいとします。
したがって、本を読みたい人が本のタイトルと起動する正確なタイトルを知っている場合は、それで十分です。タイトルがあれば、その人は図書館員の助けを借りて、本を簡単かつ迅速に見つけることができるはずです。
それで、どうすればそれができますか?まあ、もちろん、各本を置いた場所のある種のリストを保持することはできますが、ライブラリを検索するのと同じ問題が発生するので、リストを検索する必要があります。当然のことながら、リストは小さくて検索しやすくなりますが、ライブラリ(またはリスト)の一方の端からもう一方の端まで順番に検索する必要はありません。
あなたは本のタイトルであなたにすぐにあなたに正しい場所を与えることができる何かが欲しいので、あなたがしなければならないことはただ正しい棚に歩いて行き、本を手に取るだけです。
しかし、それはどのように行うことができますか?ええと、ライブラリをいっぱいにするときは少し先見の明があり、ライブラリをいっぱいにするときは多くの作業が必要です。
ライブラリを片方からもう片方までいっぱいにするのではなく、賢い小さな方法を考案します。あなたは本のタイトルをとり、小さなコンピュータプログラムを実行します。それはその棚の棚番号とスロット番号を吐き出します。ここに本を置きます。
このプログラムの優れた点は、後で本を読むために人が戻ってきたときに、もう一度プログラムにタイトルを入力し、最初に与えられたものと同じ棚番号とスロット番号を取得できることです。これは本がどこにあるか
他の人がすでに述べたように、プログラムはハッシュアルゴリズムまたはハッシュ計算と呼ばれ、通常は、そこに入力されたデータ(この場合は本のタイトル)を取得して、そこから数値を計算します。
簡単にするために、各文字と記号を数値に変換し、それらをすべて合計するとします。実際にはもっと複雑ですが、とりあえずはそのままにしておきましょう。
このようなアルゴリズムの優れた点は、同じ入力を何度もフィードすると、毎回同じ数が吐き出されることです。
OK、それが基本的にハッシュテーブルの仕組みです。
技術的なものが続きます。
最初に、数のサイズがあります。通常、このようなハッシュアルゴリズムの出力は、いくつかの大きな数値の範囲内にあり、通常はテーブル内のスペースよりもはるかに大きくなります。たとえば、図書館に100万冊の本を収める余地があるとします。ハッシュ計算の出力は、0から10億の範囲になる可能性があり、はるかに高くなります。
どうしようか?モジュラス計算と呼ばれるものを使用します。基本的には、必要な数(つまり、10億の数)まで数えたが、はるかに小さい範囲内にとどまりたい場合、その小さい範囲の制限に達するたびに、 0、しかし、あなたはあなたが来た大きなシーケンスのどこまで追跡する必要があります。
ハッシュアルゴリズムの出力が0〜20の範囲にあり、特定のタイトルから値17を取得するとします。図書館のサイズが7冊しかない場合は、1、2、3、4、5、6と数え、7になると0から始めます。17回数える必要があるため、1 2、3、4、5、6、0、1、2、3、4、5、6、0、1、2、3、および最終的な数は3です。
もちろん、係数の計算はそのようには行われません。除算と剰余で行われます。17を7で割った余りは3です(7は14で2倍の17になり、17と14の差は3です)。
したがって、本をスロット番号3に入れます。
これは次の問題につながります。衝突。アルゴリズムは、本を正確に(または、可能であればハッシュテーブルに)埋めるように本を配置する方法がないため、常に、以前に使用された数を計算することになります。図書館という意味では、本を入れたい棚とスロット番号にたどり着くと、そこにはすでに本があります。
さまざまな衝突処理方法が存在します。たとえば、データをさらに別の計算で実行して、テーブル内の別の場所を取得する(ダブルハッシュ)、または単に指定されたスペースに近いスペースを見つける(つまり、前の本のすぐ隣にスロットがあると仮定する)線形プローブとしても知られていました)。これは、後でその本を見つけようとするときにやらなければならないことがあるということを意味しますが、それでも、単にライブラリの一方の端から始めるよりはましです。
最後に、ある時点で、図書館が許可するよりも多くの本を図書館に入れたい場合があります。つまり、より大きなライブラリを構築する必要があります。ライブラリ内の正確なスポットは、ライブラリの正確な現在のサイズを使用して計算されたため、ライブラリのサイズを変更すると、計算によってすべての書籍の新しいスポットを見つける必要が生じる可能性があります。変更されました。
この説明がバケットと関数よりも地球に少し通じていることを願っています:)
A{ptrA, valueA}, B{ptrB, valueB}, C{ptrC, valueC}
と、3つのバケットを持つハッシュテーブルがあるとし[ptr1, ptr2, ptr3]
ます。挿入時に衝突があるかどうかに関係なく、メモリ使用量は修正されます。衝突がない可能性があります:A{NULL, valueA} B{NULL, valueB} C{NULL, valueC}
and [&A, &B, &C]
、またはすべての衝突A{&B, valueA} B{&C, valueB}, C{NULL, valueC}
and [NULL, &A, NULL]
:NULLバケットは「無駄」ですか?ちょっと、そうではありません。同じ合計メモリ使用量。
使用法とLingo:
実際の例:
Hash&Co .は1803年に設立され、コンピュータ技術を何も欠いており、約30,000のクライアントの詳細情報(レコード)を保持するために、合計300のファイリングキャビネットがありました。各ファイルフォルダーは、クライアント番号(0〜29,999の一意の番号)で明確に識別されました。
その時のファイリング担当者は、作業スタッフのクライアントレコードをすばやくフェッチして保存する必要がありました。スタッフは、ハッシュ手法を使用してレコードを保存および取得する方が効率的であると判断していました。
クライアントの記録を提出するために、事務員はフォルダに書かれた一意のクライアント番号を使用します。このクライアント番号を使用して、ハッシュキーを300 だけ変調して、含まれているファイリングキャビネットを識別します。ファイリングキャビネットを開くと、クライアント番号順に並べられた多くのフォルダーが含まれていることがわかります。正しい場所を特定した後、彼らは単にそれを差し込みます。
クライアントのレコードを取得するために、ファイリング担当者には、紙片にクライアント番号が付与されます。この一意のクライアント番号(ハッシュキー)を使用して、どのファイリングキャビネットにクライアントフォルダーがあったかを判断するために、この番号を300ずつ変調します。ファイリングキャビネットを開くと、クライアント番号順に並べられた多くのフォルダーが含まれていることがわかりました。レコードを検索すると、クライアントフォルダーがすぐに見つかり、取得されます。
実際の例では、バケットはファイリングキャビネットで、レコードはファイルフォルダーです。
覚えておくべき重要なことは、コンピューター(およびそのアルゴリズム)は文字列よりも数値をより適切に処理するということです。したがって、インデックスを使用して大きな配列にアクセスする方が、順次アクセスするよりもはるかに高速です。
Simonが私が非常に重要であると信じていることを述べたように、ハッシュの部分は大きなスペース(任意の長さ、通常は文字列など)を変換し、それをインデックス付けのために小さなスペース(既知のサイズ、通常は数値)にマッピングすることです。これを覚えておくことは非常に重要です!
したがって、上記の例では、30,000人程度のクライアントがより小さなスペースにマッピングされています。
これの主なアイデアは、通常は時間がかかる実際の検索を高速化するために、データセット全体をセグメントに分割することです。上記の例では、300のファイリングキャビネットのそれぞれに(統計的に)約100のレコードが含まれます。100件のレコードを(順序に関係なく)検索すると、30,000件を処理するよりもはるかに高速になります。
既に実際にこれを実行しているものがあることに気づいたかもしれません。ただし、ハッシュ方法を考案してハッシュキーを生成する代わりに、ほとんどの場合、姓の最初の文字を使用します。したがって、26のファイリングキャビネットにそれぞれAからZまでの文字が含まれている場合、理論的にはデータをセグメント化し、ファイリングと取得のプロセスを強化しただけです。
お役に立てれば、
ジーチ!
100
レコードが含まれます(30kレコード/ 300キャビネット= 100)。編集する価値があるかもしれません。
TonyD
し、テキストフィールドに入力したSHA-1ハッシュを生成します。最終的には、次のような値が生成されますe5dc41578f88877b333c8b31634cf77e4911ed8c
。これは、160ビット(20バイト)の大きな16進数にすぎません。これを使用して、レコードの保存に使用するバケット(数量限定)を決定できます。
これは理論のかなり深い領域であることがわかりますが、基本的な概要は単純です。
基本的に、ハッシュ関数は1つのスペース(たとえば、任意の長さの文字列)から物事を取り出し、それらをインデックス付けに役立つスペース(符号なし整数など)にマップする関数にすぎません。
ハッシュするもののスペースが少ない場合は、それらを整数として解釈するだけで済む可能性があり、これで完了です(例:4バイト文字列)
ただし、通常ははるかに広いスペースがあります。キーとして許可するもののスペースが、インデックス付けに使用しているもののスペース(uint32のものなど)よりも大きい場合、それぞれに一意の値を設定することはできません。2つ以上のものが同じ結果にハッシュする場合、適切な方法で冗長性を処理する必要があります(これは通常、衝突と呼ばれ、それをどのように処理するか、またはしないかは、あなたが何であるかに少し依存します。ハッシュを使用する)。
これは、同じ結果が得られないようにしたいことを意味します。また、おそらくハッシュ関数を高速にしたいと思うでしょう。
これら2つのプロパティ(およびその他いくつか)のバランスをとることで、多くの人々が忙しくなりました!
実際には、通常、アプリケーションでうまく機能することがわかっている関数を見つけて使用できるはずです。
これをハッシュテーブルとして機能させるために、メモリの使用について気にしていないと想像してください。次に、インデックスセット(すべてのuint32など)が設定されている限り、配列を作成できます。テーブルに何かを追加するとき、それをキーとしてハッシュし、そのインデックスで配列を調べます。そこに何もなければ、そこにあなたの価値を置きます。すでに何かがある場合は、このエントリをそのアドレスにあるもののリストに、どのエントリが実際にどのキーに属しているかを見つけるのに十分な情報(元のキー、または巧妙なもの)とともに追加します。
したがって、長い間、ハッシュテーブル(配列)のすべてのエントリが空であるか、1つのエントリまたはエントリのリストが含まれています。取得は、配列にインデックスを付けて値を返すか、値のリストをウォークして正しい値を返すのと同じくらい簡単です。
もちろん、実際には通常これを行うことはできません。メモリを浪費しすぎます。したがって、スパース配列に基づいてすべてを実行します(唯一のエントリは実際に使用するものであり、その他はすべて暗黙的にnullです)。
これをよりよくするためのスキームやトリックはたくさんありますが、それが基本です。
int
、1000分の1 のスパース性と4kページのキー=タッチされたほとんどのページ)、およびOSの扱いすべて-0ページアドレス空間が十分にあるとき、効率的...、(ので、すべての未使用バケットページはバックアップメモリを必要としない)
たくさんの答えがありますが、どれもあまり視覚的ではなく、ハッシュテーブルは視覚化されたときに簡単に「クリック」できます。
ハッシュテーブルは、リンクリストの配列として実装されることがよくあります。人の名前を格納するテーブルを想像すると、数回挿入した後、次のようにメモリに配置される可能性があります。ここで、-で()
囲まれた数字は、テキスト/名前のハッシュ値です。
bucket# bucket content / linked list
[0] --> "sue"(780) --> null
[1] null
[2] --> "fred"(42) --> "bill"(9282) --> "jane"(42) --> null
[3] --> "mary"(73) --> null
[4] null
[5] --> "masayuki"(75) --> "sarwar"(105) --> null
[6] --> "margaret"(2626) --> null
[7] null
[8] --> "bob"(308) --> null
[9] null
いくつかのポイント:
[0]
、[1]
...)はバケットと呼ばれ、空のリンクされた値のリスト(この例では要素、この例では人の名前)を開始します"fred"
ハッシュ付き42
)はバケット[hash % number_of_buckets]
などからリンクされます42 % 10 == [2]
。%
あるモジュロ演算子バケットの数で割った余り-42 % 10 == [2]
、および9282 % 10 == [2]
)が、ハッシュ値が同じである(例えば、時にはため"fred"
と"jane"
の両方がハッシュで示さ42
上記)
テーブルサイズが大きくなると、上記のように実装されたハッシュテーブルはサイズを変更する傾向があります(つまり、バケットの大きな配列を作成し、そこから新しい/更新されたリンクリストを作成し、そこから古い配列を削除して)、バケットに対する値の比率を維持します(ロード) factor)0.5から1.0の範囲のどこか。
Hansは以下のコメントで他の負荷係数の実際の式を示していますが、指標値については、負荷係数1と暗号強度ハッシュ関数を使用すると、バケットの1 / e(〜36.8%)は空になる傾向があり、別の1 / e (〜36.8%)1つの要素、1 /(2e)または〜18.4%の2つの要素、1 /(3!e)約6.1%の3つの要素、1 /(4!e)または〜1.5%の4つの要素、1 / (5!e)〜.3%には5などがあります。-空でないバケットからの平均チェーン長は、テーブルに要素がいくつあっても(つまり、100の要素と100のバケットがあるか、または1億であるか)要素と1億個のバケット)。これが、ルックアップ/挿入/消去がO(1)一定時間操作であると言う理由です。
上記のようなハッシュテーブルの実装が与えられた場合、などの値タイプstruct Value { string name; int age; };
、およびname
フィールドのみを参照する(年齢を無視する)等値比較とハッシュ関数を作成すると想像できます。そして、何か素晴らしいことが起こります:テーブルにValue
レコードを保存でき{"sue", 63}
ます。 、その後、彼女の年齢を知らずに「sue」を検索し、保存された値を見つけて回復するか、彼女の年齢を更新します
-お誕生日おめでとうスー-興味深いことに、ハッシュ値は変更されないため、スーのレコードを別のレコードに移動する必要はありません。バケツ。
これを行うときは、ハッシュテーブルを連想コンテナ(別名map)として使用します。格納される値は、キー(名前)と、混乱を招く- 値(私の例では、ちょうど年齢)。マップとして使用されるハッシュテーブルの実装は、ハッシュマップと呼ばれます。
これは、「sue」のような個別の値を格納したこの回答の前の例とは対照的です。これは、独自のキーであると考えることができます。そのような使用法は、ハッシュセットと呼ばれます。
すべてのハッシュテーブルがリンクリストを使用しているわけではありません(別のチェーンと呼ばれます)。ただし、主な代替クローズドハッシュ(別名オープンアドレス指定)と同様に、最も一般的な目的は使用します。ハッシュ関数。
衝突を最小化する最悪のハッシュ関数の一般的な目的は、常に同じキーに対して同じハッシュ値を生成しながら、キーをハッシュテーブルバケットの周りにランダムに効果的に散布することです。キーのどこかで1ビットが変化しても、理想的には-ランダムに-結果のハッシュ値の約半分のビットが反転します。
これは通常、計算が複雑すぎて理解できないほど複雑にまとめられています。理解しやすい方法の1つを説明します。これは、最もスケーラブルまたはキャッシュフレンドリーではないが本質的にエレガントです(ワンタイムパッドを使用した暗号化のように!)。64ビットdouble
のs をハッシュしていたとすると、256個の乱数(以下のコード)の8つのdouble
テーブルを作成し、のメモリ表現の各8ビット/ 1バイトのスライスを使用して、別のテーブルにインデックスを付け、調べた乱数。このアプローチでは、ビット(2進数の意味で)がどこかで変化するdouble
と、別の乱数がテーブルの1つで検索され、完全に相関のない最終的な値になることが簡単にわかります。
// note caveats above: cache unfriendly (SLOW) but strong hashing...
size_t random[8][256] = { ...random data... };
const char* p = (const char*)&my_double;
size_t hash = random[0][p[0]] ^ random[1][p[1]] ^ ... ^ random[7][p[7]];
多くのライブラリのハッシュ関数は整数を変更せずに渡します(自明なハッシュ関数またはIDハッシュ関数と呼ばれます)。これは、上記の強力なハッシュからのもう1つの極端です。IDハッシュは非常に最悪の場合は衝突が発生しやすくなりますが、増加する傾向がある(おそらくギャップがある)整数キーのかなり一般的なケースでは、ランダムなハッシュの葉よりも空の数が少ないまま、連続したバケットにマップされます(約36.8前述の負荷係数1での%)。これにより、ランダムマッピングによって達成されるよりも、衝突が少なくなり、衝突する要素のリンクリストが長くなります。また、強力なハッシュを生成するのにかかる時間を節約でき、メモリ内の近くのバケットでキーが見つかるようにキーが検索されると、キャッシュヒットが改善されます。キーがうまくインクリメントされない場合、それらの配置が完全にランダム化されてバケットに完全にランダム化される強力なハッシュ関数を必要としないほど十分にランダムになることが期待されます。
皆さんはこれを完全に説明するのに非常に近いですが、いくつか欠落しています。ハッシュテーブルは単なる配列です。アレイ自体は、各スロットに何かが含まれます。少なくとも、ハッシュ値または値自体をこのスロットに格納します。これに加えて、このスロットで衝突したリンクされた値/チェーンされた値のリストを保存したり、オープンアドレス指定メソッドを使用したりすることもできます。このスロットから取り出したい他のデータへのポインタを格納することもできます。
ハッシュ値自体は通常、値を配置するスロットを示していないことに注意することが重要です。たとえば、ハッシュ値は負の整数値になる場合があります。明らかに、負の数は配列の場所を指すことはできません。さらに、ハッシュ値は多くの場合、利用可能なスロットよりも大きな数になる傾向があります。したがって、ハッシュテーブル自体が別の計算を実行して、値をどのスロットに入れるかを判断する必要があります。これは、次のような係数演算で行われます。
uint slotIndex = hashValue % hashTableSize;
この値は、値が入るスロットです。オープンアドレス指定では、スロットが既に別のハッシュ値や他のデータで埋められている場合、次のスロットを見つけるためにもう一度モジュラス演算が実行されます。
slotIndex = (remainder + 1) % hashTableSize;
スロットインデックスを決定するためのより高度な方法が他にもあると思いますが、これは私が見た一般的な方法です...パフォーマンスの高い他の方法に興味があります。
係数法では、たとえばサイズが1000のテーブルがある場合、1〜1000のハッシュ値はすべて対応するスロットに入ります。負の値、および1000より大きい値は、競合するスロット値になる可能性があります。これが発生する可能性は、ハッシュ方式と、ハッシュテーブルに追加したアイテムの総数の両方に依存します。一般に、ハッシュテーブルに追加される値の総数がそのサイズの約70%に等しくなるように、ハッシュテーブルのサイズを作成することをお勧めします。ハッシュ関数が均等に分散する場合、通常、バケット/スロットの衝突はほとんど発生しないか、まったく発生せず、ルックアップと書き込みの両方の操作で非常に高速に実行されます。追加する値の総数が事前にわからない場合は、何らかの手段を使用して適切な推定を行ってください。
これがお役に立てば幸いです。
PS-C#ではGetHashCode()
メソッドがかなり遅く、テストした多くの条件下で実際の値の衝突が発生します。実際の楽しみとして、独自のハッシュ関数を作成し、ハッシュする特定のデータと衝突しないようにし、GetHashCodeよりも高速に実行し、かなり均等に分散するようにしてください。私はこれを、intサイズのハッシュコード値の代わりにlongを使用して行いました。これは、0の衝突で、ハッシュテーブル内の最大3200万のハッシュ値全体で非常にうまく機能しました。残念ながら、コードは私の雇用者のものなので共有することはできません...しかし、特定のデータドメインで可能であることを明らかにできます。これを達成できる場合、ハッシュテーブルは非常に高速です。:)
remainder
は元のモジュロ計算の結果を参照し、次の使用可能なスロットを見つけるために1を追加します。
long
ハッシュ値の話はそれが達成したことを意味します)が、mod /%操作の後にハッシュテーブルでそれらが衝突しないことを保証することに注意してください(一般的な場合) )。
これは私の理解ではそれがどのように機能するかです:
次に例を示します。テーブル全体を一連のバケットとして描きます。英数字のハッシュコードを使用した実装があり、アルファベットの文字ごとに1つのバケットがあるとします。この実装は、ハッシュコードが特定の文字で始まる各項目を対応するバケットに入れます。
200個のオブジェクトがあり、そのうち15個だけが「B」の文字で始まるハッシュコードを持っているとします。ハッシュテーブルは、200個のオブジェクトすべてではなく、「B」バケット内の15個のオブジェクトを検索および検索するだけで済みます。
ハッシュコードを計算する限り、それについて不思議なことは何もありません。目標は、異なるオブジェクトが異なるコードを返し、等しいオブジェクトが等しいコードを返すようにすることです。すべてのインスタンスのハッシュコードと常に同じ整数を返すクラスを作成することはできますが、ハッシュテーブルが1つの巨大なバケットになるだけなので、ハッシュテーブルの有用性は本質的に破壊されます。
短くて甘い:
ハッシュテーブルは配列をラップし、それを呼び出しますinternalArray
。アイテムはこの方法で配列に挿入されます:
let insert key value =
internalArray[hash(key) % internalArray.Length] <- (key, value)
//oversimplified for educational purposes
2つのキーが配列内の同じインデックスにハッシュされることがあり、両方の値を保持したい場合があります。両方の値を同じインデックスに格納するのが好きです。これはinternalArray
、リンクリストの配列を作成することで簡単にコーディングできます。
let insert key value =
internalArray[hash(key) % internalArray.Length].AddLast(key, value)
したがって、ハッシュテーブルからアイテムを取得したい場合は、次のように記述できます。
let get key =
let linkedList = internalArray[hash(key) % internalArray.Length]
for (testKey, value) in linkedList
if (testKey = key) then return value
return null
削除操作も簡単に記述できます。ご覧のとおり、リンクリストの配列からの挿入、ルックアップ、および削除はほぼ O(1)です。
internalArrayがいっぱいになり、容量が約85%になると、内部配列のサイズを変更して、すべての項目を古い配列から新しい配列に移動できます。
それよりももっと簡単です。
ハッシュテーブルは、キーと値のペアを含むベクトルの配列(通常はスパース配列)にすぎません。この配列の最大サイズは、通常、ハッシュテーブルに格納されているデータのタイプの可能な値のセット内のアイテム数よりも小さいです。
ハッシュアルゴリズムは、配列に格納される項目の値に基づいて、その配列へのインデックスを生成するために使用されます。
これは、配列にキー/値ペアのベクトルを格納する場所です。配列内のインデックスになることができる値のセットは、通常、タイプが持つことができるすべての可能な値の数よりも小さいため、ハッシュがアルゴリズムは、2つの別々のキーに対して同じ値を生成します。良いのハッシュアルゴリズムは、(それが一般的なハッシュアルゴリズムはおそらく知ることができない特定の情報を持っているので、それは通常タイプに追いやられている理由である)可能な限りこれを防ぐことができますが、防ぐことは不可能です。
このため、同じハッシュコードを生成する複数のキーを持つことができます。これが発生すると、ベクター内のアイテムが反復処理され、ベクター内のキーと検索されているキーの間で直接比較が行われます。見つかった場合は、greatとキーに関連付けられた値が返されます。それ以外の場合は何も返されません。
あなたはたくさんのものと配列をとります。
それぞれについて、ハッシュと呼ばれるインデックスを作成します。ハッシュについての重要なことは、ハッシュがたくさん「散在する」ことです。2つの類似したものに類似したハッシュを持たせたくない場合。
ハッシュで示された位置にあるものを配列に入れます。与えられたハッシュで複数のものが発生する可能性があるため、配列など適切なものに格納します。これは通常、バケットと呼ばれます。
ハッシュで物事を調べるときは、同じ手順を実行してハッシュ値を計算し、その場所のバケットの内容を確認して、探しているものかどうかを確認します。
ハッシュがうまく機能していて、配列が十分に大きい場合、配列内の特定のインデックスにはせいぜい数個のものが存在するだけなので、あまり調べる必要はありません。
ボーナスポイントについては、ハッシュテーブルにアクセスしたときに、見つかったものがあれば(存在する場合)バケットの先頭に移動するようにしてください。次回は最初にチェックされます。
これまでの答えはすべて良好で、ハッシュテーブルの動作のさまざまな側面を理解できます。ここに役立つかもしれない簡単な例があります。小文字のアルファベット文字列をキーとしていくつかのアイテムを保存するとします。
サイモンが説明したように、ハッシュ関数は大きな空間から小さな空間へのマッピングに使用されます。この例のハッシュ関数の単純で素朴な実装では、文字列の最初の文字を取得して整数にマッピングできるため、「alligator」のハッシュコードは0、「bee」のハッシュコードは1になります。ゼブラ」は25などになります。
次に、26個のバケット(JavaではArrayListsの可能性があります)の配列があり、キーのハッシュコードと一致するアイテムをバケットに入れます。同じ文字で始まるキーを持つアイテムが複数ある場合、それらは同じハッシュコードを持つため、すべてがそのハッシュコードのバケットに入り、バケット内で線形検索を行う必要があります。特定のアイテムを見つけます。
この例では、アルファベットにまたがるキーを持つアイテムが数ダースしかない場合、非常にうまく機能します。ただし、100万個のアイテムまたはすべてのキーがすべて「a」または「b」で始まる場合、ハッシュテーブルは理想的ではありません。パフォーマンスを向上させるには、別のハッシュ関数やバケットを追加する必要があります。
ここにそれを見る別の方法があります。
私はあなたが配列Aの概念を理解していると思います。これは、Aがどんなに大きくても、1番目のステップでI番目の要素A [I]に到達できるインデックス作成の操作をサポートするものです。
したがって、たとえば、たまたま年齢が異なる人々のグループに関する情報を格納したい場合、単純な方法は、十分な大きさの配列を用意し、各人の年齢を配列のインデックスとして使用することです。そうすれば、誰の情報にもワンステップでアクセスできます。
ただし、もちろん同じ年齢の人が複数存在する可能性があるため、各エントリで配列に入力するのは、その年齢のすべての人のリストです。そのため、1つのステップで個人の情報にアクセスし、そのリストで少し検索することもできます(「バケット」と呼ばれます)。バケットが大きくなるほど多くの人がいる場合にのみ、速度が低下します。次に、より大きな配列と、年齢を使用する代わりに、姓の最初の数文字など、個人に関するより多くの識別情報を取得する他の方法が必要です。
それが基本的な考え方です。年齢を使用する代わりに、値の良い広がりを生成する人の任意の関数を使用できます。それがハッシュ関数です。同様に、人の名前のASCII表現の3ビットごとに、ある順序でスクランブルをかけることができます。重要なのは、同じバケットにハッシュ化する人が多すぎないようにすることです。速度はバケットのサイズが小さいことに依存するためです。
ハッシュテーブルは、実際の計算がランダムアクセスマシンモデルに従っているという事実に完全に対応しています。つまり、メモリ内の任意のアドレスの値は、O(1)時間または一定時間でアクセスできます。
したがって、キーのユニバースがある場合(アプリケーションで使用できるすべての可能なキーのセット、たとえば学生のロール番号、4桁の場合、このユニバースは1から9999までの数値のセットです)、およびそれらをシステムのメモリを割り当てることができるサイズの有限の数のセットにマップする方法、理論的には私のハッシュテーブルは準備ができています。
一般に、アプリケーションでは、キーのユニバースのサイズが、ハッシュテーブルに追加する要素の数よりも非常に大きい(32 GBであるため、1 GBのメモリを無駄にしたくない、たとえば10000または100000の整数値をハッシュしたくない)バイナリ表現では少し長い)。したがって、このハッシュを使用します。これは、一種の「数学的な」操作の混合です。これは、私の大きな宇宙を、メモリに収容できる小さな値のセットにマッピングします。実際の場合、ハッシュテーブルのスペースは、(要素数*各要素のサイズ)と同じ「順序」(big-O)であることが多いため、多くのメモリを無駄にしません。
現在、大きなセットは小さなセットにマッピングされています。マッピングは多対1でなければなりません。したがって、異なるキーには同じスペースが割り当てられます(??不公平)。これを処理するにはいくつかの方法がありますが、私はそれらのうち人気のある2つを知っています。
CLRSによるアルゴリズムの概要は、このトピックに関する非常に優れた洞察を提供します。
プログラミング用語を探しているすべての人のために、これがどのように機能するかを示します。高度なハッシュテーブルの内部実装には、ストレージの割り当て/割り当て解除と検索について多くの複雑さと最適化がありますが、トップレベルのアイデアはほとんど同じです。
(void) addValue : (object) value
{
int bucket = calculate_bucket_from_val(value);
if (bucket)
{
//do nothing, just overwrite
}
else //create bucket
{
create_extra_space_for_bucket();
}
put_value_into_bucket(bucket,value);
}
(bool) exists : (object) value
{
int bucket = calculate_bucket_from_val(value);
return bucket;
}
ここcalculate_bucket_from_val()
で、すべての一意性の魔法が発生する必要があるハッシュ関数です。
経験則として 、特定の値が挿入されるためには、バケットは、格納することになっている値から一意かつ派生可能である必要があります。
バケットは、値が格納される任意のスペースです。ここでは、配列インデックスとしてintを保持しましたが、おそらくメモリの場所でもあります。
create_extra_space_for_bucket()
は新しいキーの挿入中のステップを文書化します。ただし、バケットはポインタの場合があります。