web-dev-qa-db-ja.com

ハッシュテーブルはどのように機能しますか?

ハッシュテーブルがどのように機能するかの説明を探しています - 私のような単純語のために平易な英語で!

例えば、私はそれがキーを取り、ハッシュを計算し(私は説明を探しています)、そして値が格納されている配列のどこにあるかを解くためにある種のモジュロを実行します。

誰もがプロセスを明確にすることができますか?

編集:ハッシュコードの計算方法については特に質問しませんが、ハッシュテーブルのしくみの概要について説明します。

465
Arec Barrwin

これが素人の言葉による説明です。

図書館に本を一杯に詰め込むだけでなく、本でいっぱいにしたいのですが、必要なときに再び簡単に見つけられるようにしたいとしましょう。

ですから、本を読みたい人が本のタイトルと起動する正確なタイトルを知っているのであれば、それだけで十分です。タイトルがあれば、その人は司書の助けを借りて、本を簡単かつ迅速に見つけることができるはずです。

それで、どのようにあなたはそれをすることができますか?さて、明らかにあなたはあなたがそれぞれの本を置いた場所のある種のリストを維持することができます、しかしそれからあなたは図書館を検索するのと同じ問題を抱えています、あなたはリストを検索する必要があります。確かに、リストは小さくて検索が簡単ですが、それでもライブラリ(またはリスト)の一方の端からもう一方の端まで順番に検索することは望ましくありません。

あなたは、その本のタイトルで、あなたにすぐに正しい場所を与えることができる何かがほしいと思う、それであなたがしなければならないのはちょうどちょうど正しい棚まで歩いて行き、そして本を拾うことである。

しかし、どうすればそれができますか?さて、あなたが図書館をいっぱいにするときあなたが図書館をいっぱいにするとき多くの仕事をするときには、少しの思いやりがあります。

一方の端からもう一方の端までライブラリをいっぱいにし始めるのではなく、賢い小さな方法を考案します。あなたは本のタイトルを取り、それをその棚の上に棚番号とスロット番号を吐き出す小さなコンピュータプログラムを通して実行します。これはあなたが本を置くところです。

このプログラムの長所は、後で本を読むために人が戻ってきたときに、もう一度プログラムを通してタイトルを入力し、最初に与えられたのと同じ棚番号とスロット番号を返すことです。本がある場所。

このプログラムは、他の人がすでに述べたように、ハッシュアルゴリズムまたはハッシュ計算と呼ばれ、通常、そこに入力されたデータ(この場合は本のタイトル)を受け取ってそれから数を計算します。

簡単にするために、各文字と記号を数字に変換してそれらをすべて合計するとしましょう。実際には、それよりはるかに複雑ですが、ここではそのままにしましょう。

このようなアルゴリズムの利点は、同じ入力を何度も繰り返し入力すると、毎回同じ数字が吐き出されることです。

それで、基本的にハッシュテーブルはどのように機能するのでしょう。

技術的なものが続きます。

まず、数の大きさがあります。通常、このようなハッシュアルゴリズムの出力は、ある数の範囲内にあります。通常は、テーブルにあるスペースよりもはるかに大きくなります。たとえば、図書館にはちょうど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で17に2回入り、17と14の差は3です)。

したがって、あなたは本をスロット番号3に入れます。

これは次の問題につながります。衝突このアルゴリズムでは、書籍を正確にライブラリ(または作成する場合はハッシュテーブル)を埋め尽くすために書籍のスペースを空けることができないため、必ず以前に使用された数値を計算することになります。図書館という意味では、本を入れたい棚とスロット番号にたどり着くと、すでにそこに本があります。

テーブル内の別のスポットを取得するためのデータをさらに計算する( ダブルハッシング )、または単にスペースを見つけるなど、さまざまな衝突処理方法が存在します。あなたが与えられたものに近い(すなわち、スロットが利用可能であると仮定して前の本のすぐ隣に 線形プロービング としても知られている)。これは、後でその本を見つけようとしたときにやるべきことがいくつかあることを意味しますが、それでもライブラリの一方の端から始めるよりはましです。

最後に、ある時点で、図書館が許可する以上の本を図書館に入れることをお勧めします。言い換えれば、もっと大きなライブラリを構築する必要があります。正確な現在のサイズのライブラリを使用してライブラリ内の正確なスポットが計算されているため、ライブラリのサイズを変更すると、計算後にすべての書籍の新しいスポットを見つける必要が生じる可能性があります。変更されました。

私はこの説明がバケツと機能よりも地球上のものであることを少し願っています:)

用法と用語:

  1. ハッシュテーブル データ(またはレコード)をすばやく保存および取得するために使用されます。
  2. レコードは バケツ 使う ハッシュキー
  3. ハッシュキー ハッシュアルゴリズムを選択した値に適用して計算されます( キー レコード内に含まれるこの選択された値は、すべてのレコードに共通の値でなければなりません。
  4. 各 バケツ 特定の順序で編成された複数のレコードを持つことができます。

実世界の例:

ハッシュ&カンパニー.、1803年に設立され、コンピュータ技術を欠いている彼らの約3万人のクライアントのための詳細な情報(記録)を保管するための合計300のファイリングキャビネットがありました。各ファイルフォルダは、クライアント番号、0から29,999までの一意の番号で明確に識別されていました。

当時のファイリング担当者は、作業スタッフのためにクライアントレコードをすばやく取得して保存する必要がありました。スタッフは、自分たちのレコードを保存および取得するためにハッシュ方法論を使用することがより効率的であると判断しました。

クライアントレコードを提出するには、ファイリング担当者はフォルダに書かれた一意のクライアント番号を使用します。このクライアント番号を使用して、彼らは ハッシュキー それが含まれているファイリングキャビネットを識別するために300であります。正しい場所を特定した後、彼らは単にそれを滑り込ませるでしょう。

顧客記録を検索するために、ファイリング担当者は、1枚の紙に顧客番号を付与されます。この一意のクライアント番号( ハッシュキー)、どのファイリングキャビネットにクライアントフォルダがあるかを判断するために、300で変調します。彼らがファイリングキャビネットを開けたとき、彼らはそれがクライアント番号によって順序付けられた多くのフォルダーを含んでいたことを発見するでしょう。レコードを検索すると、クライアントフォルダをすばやく見つけて取得できます。

実際の例では、 バケツ あります ファイリングキャビネット そして私達の 記録 あります ファイルフォルダ


覚えておくべき重要なことは、コンピュータ(そしてそのアルゴリズム)は文字列よりも数値をうまく扱うということです。そのため、インデックスを使用して大きな配列にアクセスする方が、順次アクセスするよりもはるかに高速です。

サイモンが述べたように 私はそれを信じる 非常に重要 ハッシュ部分は、大きなスペース(任意の長さ、通常は文字列など)を変換し、それをインデックス作成のために小さなスペース(既知のサイズ、通常数字)にマッピングすることです。覚えておくことが非常に重要な場合!

そのため、上記の例では、3万人ほどのクライアントが小さなスペースにマッピングされています。


ここでの主な考え方は、実際の検索を高速化するためにデータセット全体をセグメントに分割することです。これは通常時間がかかります。上記の例では、300のファイリングキャビネットのそれぞれに(統計的に)約100のレコードが含まれます。 100レコードを検索する(順序に関係なく)のは、3万を処理するよりもはるかに高速です。

お気づきかもしれませんが、実際にこれを実行している人もいます。しかし、ハッシュキーを生成するためのハッシュ方法論を考案する代わりに、彼らはほとんどの場合単に姓の最初の文字を使うでしょう。したがって、26個のファイリングキャビネットにそれぞれAからZの文字が入っている場合、理論上はデータをセグメント化してファイリングと検索のプロセスを強化しただけです。

お役に立てれば、

やあ!

94
Jeach

これは理論のかなり深い分野であることがわかりますが、基本的な概要は単純です。

基本的に、ハッシュ関数は単なる1つのスペース(任意の長さの文字列など)からものを取り出し、それらをインデックス作成に役立つスペース(符号なし整数など)にマップする関数です。

ハッシュするスペースが少ない場合は、それらを整数として解釈するだけで済みます(4バイトの文字列など)。

しかし通常は、もっと広いスペースがあります。キーとして許可するもののスペースが、インデックス付けに使用しているもの(あなたのuint32など)のスペースよりも大きい場合は、それぞれに固有の値を設定することはできません。 2つ以上のものが同じ結果にハッシュされるときは、冗長性を適切な方法で処理する必要があります(これは通常衝突と呼ばれ、どのようにそれを処理するかしないかはあなたがいるものに少し依存します) )のハッシュを使用します。

これは、同じ結果にならないようにしたいこと、そしてハッシュ関数を高速にしたいことを意味します。

これら2つのプロパティ(および他のいくつか)のバランスをとることで、多くの人が忙しくなりました。

実際には、あなたは通常あなたのアプリケーションでうまく機能することが知られている関数を見つけてそれを使うことができるはずです。

これをハッシュテーブルとして機能させるために、メモリ使用量を気にしないと想像してください。そうすれば、あなたのインデックスが設定されている限り、あなたは配列を作成することができます(例えば、すべてのuint32)。テーブルに何かを追加したら、それをキーにハッシュし、そのインデックスの配列を調べます。そこに何もなければ、あなたはそこにあなたの価値を置きます。すでに何かがある場合は、このエントリをそのアドレスのもののリストに追加します。また、どのエントリが実際にどのキーに属しているかを見つけるのに十分な情報(元のキー、または何か巧妙なもの)を追加します。

つまり、長い間、ハッシュテーブル(配列)のすべてのエントリは空になっているか、1つのエントリ、またはエントリのリストを含んでいます。取得は配列へのインデックス付け、そして値を返すこと、または値のリストをたどって正しいものを返すことのどちらかと同じくらい簡単です。

もちろん実際には、これを行うことはできません。あまりにも多くのメモリを浪費します。つまり、スパース配列に基づいてすべての処理を実行します(唯一のエントリは実際に使用するもので、それ以外のものはすべて暗黙的にnullです)。

これをうまく機能させるためのスキームやトリックはたくさんありますが、それが基本です。

64
simon

たくさんの答えがありますが、どれも非常に視覚的ではありません。ハッシュテーブルは視覚化されたときに簡単に「クリック」できます。

多くの場合、ハッシュテーブルはリンクリストの配列として実装されます。人々の名前を格納するテーブルを想像すると、数回挿入した後、次のようにメモリにレイアウトされる場合があります。()- enclosed numberはテキスト/名前のハッシュ値です。

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]...を示す)はbucketとして知られており、(おそらく空の)リンクされたリストvalues(akaelements、この例では-人のnames
  • 各値(たとえば、ハッシュ"fred"付きの42)は、バケット[hash % number_of_buckets]からリンクされています。 42 % 10 == [2]; %modulo operator -バケット数で割ったときの余り
  • 複数のデータ値はcollideで同じバケットからリンクされることがありますが、ほとんどの場合、それらのハッシュ値はモジュロ演算後に衝突するためです(例:42 % 10 == [2]9282 % 10 == [2] )、ただし時折ハッシュ値が同じであるため(例:"fred""jane"は両方とも上記のハッシュ42で表示されます)
    • ほとんどのハッシュテーブルは衝突を処理します-パフォーマンスはわずかに低下しますが、機能の混乱はありません-検索または挿入される値の完全な値(ここではテキスト)を、ハッシュ先バケットのリンクリストに既にある各値と比較します

リンクされたリストの長さは、値の数ではなく負荷係数に関係します

テーブルサイズが大きくなると、上記のように実装されたハッシュテーブルは、バケットに対する値の比率を維持するために、サイズを変更する傾向があります(バケットの大きな配列を作成、そこから新しい/更新されたリンクリストを作成、古い配列を削除) load factor)0.5から1.0の範囲のどこか。

ハンスは以下のコメントで他の負荷係数の実際の式を示していますが、指標値の場合:負荷係数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億個のバケット)。これが、lookup/insert/eraseが O(1) 一定時間操作である理由です。

ハッシュテーブルでキーを値に関連付ける方法

上記のようなハッシュテーブルの実装を考えると、struct Value { string name; int age; };などの値型、およびnameフィールド(年齢を無視)のみを見る等値比較およびハッシュ関数の作成を想像できます。テーブルに{"sue", 63}のようなValueレコードを保存し、その後、彼女の年齢を知らずに「スー」を検索し、保存された値を見つけ、彼女の年齢を回復または更新する
-お誕生日おめでとうスー-興味深いことに、ハッシュ値を変更しないため、スーのレコードを別のバケットに移動する必要はありません。

これを行うとき、ハッシュテーブルを 連想コンテナakamap として使用しています。そして、それが格納する値は、key(名前)と、まだ混乱している-value(私の例では、年齢のみ)。マップとして使用されるハッシュテーブルの実装は、ハッシュマップとして知られています。

これは、「sue」などの個別の値を保存したこの回答の前の例とは対照的です。「sue」は独自のキーと考えることができます。そのような使用法はhash set

ハッシュテーブルを実装する方法は他にもあります

すべてのハッシュテーブルがリンクリスト( separate chaining として知られる)を使用するわけではありませんが、最も一般的な目的は、主な代替として closed hashing(akaopen addressing -特に消去操作がサポートされている場合-衝突が発生しやすいキーのパフォーマンスプロパティが安定していない/ハッシュ関数。


ハッシュ関数に関するいくつかの言葉

強力なハッシュ...

最悪の場合の衝突を最小化するハッシュ関数の一般的な目的は、同じキーに対して常に同じハッシュ値を生成しながら、ハッシュテーブルバケットの周りにランダムにキーを効果的にスプレーすることです。キーの任意の場所で1ビット変更しても、理想的には-ランダムに-結果のハッシュ値の約半分のビットを反転します。

これは通常、私が理解できないほど複雑な数学で編成されています。最もスケーラブルまたはキャッシュフレンドリーではなく、本質的にエレガントな(ワンタイムパッドを使用した暗号化など!)-上記の望ましい品質を引き出すのに役立つと思うので、わかりやすい方法を1つ挙げます。 64ビットのdoublesをハッシュするとします-256個の乱数(以下のコード)ごとに8つのテーブルを作成し、doubleのメモリ表現の各8ビット/ 1バイトスライスを使用してインデックスを作成できます別のテーブルに、検索した乱数をXORします。このアプローチでは、doubleのどこかで(2進数の意味で)ビットが変化すると、いずれかのテーブルで異なる乱数が検索され、完全に無相関の最終値になることが簡単にわかります。

// 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]];

弱いが頻繁に高速なハッシュ...

多くのライブラリのハッシュ関数は、変更されずに整数を渡します(trivialまたはidentity hash function) ;上記の強力なハッシングのもう1つの極端な例です。アイデンティティハッシュは最悪の場合extremely衝突の傾向がありますが、増加する傾向がある整数キーのかなり一般的なケースでは(おそらくいくつかのギャップがある)、ランダムハッシュリーフより空の数が少ない連続バケットにマップします(前述の負荷係数1で〜36.8%)。これにより、ランダムマッピングよりも衝突が少なくなり、衝突要素のリンクリストが長くなります。強力なハッシュを生成するのにかかる時間を節約することも素晴らしいことです。キーがメモリ内の近くのバケットで見つかるように検索されると、キャッシュヒットが改善されます。キーが適切にインクリメントしない場合、バケットへの配置を完全にランダム化するための強力なハッシュ関数を必要としないほど十分にランダムになります。

42
Tony Delroy

皆さんはこれを十分に説明していますが、2、3のことを見逃しています。ハッシュテーブルは単なる配列です。配列自体は各スロットに何かを含みます。最低限、このスロットにハッシュ値または値そのものを格納します。これに加えて、あなたはこのスロットで衝突した値のリンクされた/連鎖されたリストを保存することができます、あるいはあなたはオープンアドレッシング方法を使うことができます。このスロットから取り出したい他のデータへのポインタを保存することもできます。

ハッシュ値自体は一般に値を入れるスロットを示していないことに注意することが重要です。たとえば、ハッシュ値は負の整数値です。明らかに負の数は配列の位置を指すことはできません。さらに、ハッシュ値は多くの場合、利用可能なスロットよりも大きい数になる傾向があります。したがって、値がどのスロットに入るべきかを判断するために、ハッシュテーブル自体によって別の計算を実行する必要があります。これは次のようなモジュラス数学演算で行われます。

uint slotIndex = hashValue % hashTableSize;

この値は値が入るスロットです。オープンアドレッシングでは、スロットがすでに別のハッシュ値や他のデータで埋められている場合は、次のスロットを見つけるためにモジュラス演算がもう一度実行されます。

slotIndex = (remainder + 1) % hashTableSize;

私はスロットインデックスを決定するための他のより高度な方法があるかもしれないと思いますが、これは私が見た一般的なものです...もっとパフォーマンスの良い他のものに興味があるでしょう。

モジュラス法では、サイズが1000のテーブルがある場合、1から1000の間のハッシュ値は対応するスロットに入ります。負の値や1000を超える値は、スロット値と衝突する可能性があります。それが起こる可能性はあなたのハッシュ方法と、あなたがハッシュテーブルにどれだけの合計アイテムを追加するかによって異なります。一般に、ハッシュテーブルのサイズは、それに追加される値の総数がそのサイズの約70%になるようにするのがベストプラクティスです。あなたのハッシュ関数が一様な配布の良い仕事をしているならば、あなたは一般的にバケット/スロット衝突がほとんどないか全く遭遇しないでしょう、そしてそれは検索と書き込み操作のために非常に速く実行するでしょう。追加する値の総数が事前にわかっていない場合は、何らかの方法を使用して適切な推測を行い、追加された要素の数が容量の70%に達したら、ハッシュテーブルのサイズを変更します。

これが助けになったことを願っています。

PS - C#ではGetHashCode()メソッドはかなり遅く、私がテストした多くの条件下で実際の値の衝突が起こります。本当に楽しいのは、独自のハッシュ関数を構築し、ハッシュ化している特定のデータに衝突しないようにし、GetHashCodeよりも高速に実行し、かなり均等な分布にするようにすることです。 int sizeハッシュコード値の代わりにlongを使用してこれを実行しましたが、0衝突でハッシュテーブル内の最大3200万エントリのハッシュ値で非常にうまく機能しました。残念ながら、コードは私の雇用主のものであるため共有できません...しかし、特定のデータドメインでは可能であることがわかります。あなたがこれを達成することができるとき、ハッシュテーブルは非常に速いです。 :)

24
Chris

これが私の理解ではどのように機能するかです。

例を示しましょう。テーブル全体を一連のバケットとして描きます。英数字ハッシュコードを使用した実装があり、アルファベットの各文字に対して1つのバケットがあるとします。この実装は、ハッシュコードが特定の文字で始まる各項目を対応するバケットに入れます。

あなたが200個のオブジェクトを持っているとしましょう、しかしそれらの15個だけが文字 'B'で始まるハッシュコードを持っていますハッシュテーブルは、200個すべてのオブジェクトではなく、 'B'バケット内の15個のオブジェクトを検索して検索するだけで済みます。

ハッシュコードを計算する限り、それについて不思議なことは何もありません。目標は、異なるオブジェクトが異なるコードを返すようにし、等しいオブジェクトが等しいコードを返すようにすることです。すべてのインスタンスに対して常にハッシュコードと同じ整数を返すクラスを作成することもできますが、ハッシュテーブルは1つの巨大なバケットになるため、ハッシュテーブルの有用性を実質的に損なうことになります。

17
AndreiM

短くて甘い:

ハッシュテーブルは配列をまとめ、それを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%の容量で、内部配列のサイズを変更して、古い配列から新しい配列にすべての項目を移動できます。

12
Juliet

それよりもさらに簡単です。

ハッシュテーブルは、キーと値のペアを含むベクトルの配列(通常 スパース の1つ)にすぎません。この配列の最大サイズは、通常、ハッシュテーブルに格納されているデータのタイプに有効な値のセット内の項目数よりも小さいです。

ハッシュアルゴリズムは、配列に格納される項目の値に基づいてその配列へのインデックスを生成するために使用されます。

これが、配列内のキーと値のペアのベクトルを格納する場所です。配列内のインデックスになることができる値のセットは、通常、型が持つことができるすべての可能な値の数よりも少ないため、アルゴリズムは2つの別々のキーに同じ値を生成しようとしています。 良いハッシュアルゴリズムはこれを可能な限り防止します(一般的なハッシュアルゴリズムではおそらく知ることができない特定の情報を持っているため、このタイプに分類されます)。しかし、それを防ぐことは不可能です。

このため、同じハッシュコードを生成する複数のキーを持つことができます。それが起こるとき、ベクトルの中のアイテムは繰り返されて、ベクトルのキーと調べられているキーの間で直接比較が行われます。見つかった場合、greatとキーに関連付けられている値が返され、それ以外の場合は何も返されません。

10
casperOne

あなたはたくさんのものと配列を取ります。

それぞれについて、あなたはハッシュと呼ばれるそれのためのインデックスを作ります。ハッシュについての重要なことはそれがたくさん「散在する」ということです。あなたは2つの似たようなものが似たようなハッシュを持つことを望まない。

あなたはハッシュで示された位置であなたのものを配列に入れます。特定のハッシュに複数のものが巻きついてしまう可能性があるため、それらを配列などの適切なものに格納します。これを一般にバケットと呼びます。

ハッシュで物事を調べているときは、同じ手順を実行してハッシュ値を計算し、その場所のバケットの内容を調べて、探しているものかどうかを確認します。

あなたのハッシングがうまくいっていて、あなたの配列が十分に大きいとき、配列のどの特定のインデックスでもせいぜいいくつかのことがあるだけなので、あなたはそれほど見る必要はありません。

ボーナスポイントのために、あなたのハッシュテーブルがアクセスされたとき、それがバケットの始めに見つかったもの(もしあれば)を動かすようにそれを作るので、次回は最初にチェックされるものです。

9
chaos

これを見る別の方法があります。

配列Aの概念を理解していることを前提としています。これは、Aの大きさにかかわらず、1つのステップでI番目の要素A [I]に到達できる、インデックスの操作をサポートするものです。

したがって、たとえば、年齢が異なるすべての人々のグループに関する情報を格納する場合、簡単な方法として、十分に大きい配列を用意し、その配列のインデックスとして各自の年齢を使用します。さて、あなたは誰の情報にもワンステップでアクセスできます。

しかし、もちろん、同じ年齢の人が複数存在する可能性があるため、各エントリで配列に入れるのは、その年齢のすべての人のリストです。そのため、あなたは一歩で個人の情報にアクセスすることができ、そのリストの中で少し検索することができます( "バケツ"と呼ばれます)。バケツが大きくなるほど多くの人がいる場合にのみ遅くなります。それからあなたは、年齢を使用する代わりに、より大きな配列、そして姓の最初の数文字のように、その人に関するもっと識別的な情報を得るための何か他の方法を必要とします。

それが基本的な考え方です。年齢を使う代わりに、価値の良い広がりを生み出す人のどんな機能でも使うことができます。それがハッシュ関数です。あなたが人の名前のASCII表現の3ビットごとに取ることができるように、何らかの順序でスクランブルをかけます。重要なのは、スピードが小さいバケットに依存するため、あまりにも多くの人が同じバケットにハッシュしないようにすることです。

3
Mike Dunlavey

これまでの答えはすべて優れており、ハッシュテーブルがどのように機能するかについてさまざまな側面を把握しています。これは役に立つかもしれない簡単な例です。キーとして小文字のアルファベット文字列を持ついくつかのアイテムを格納したいとしましょう。

サイモンが説明したように、ハッシュ関数は大きな空間から小さな空間への写像に使われます。私たちの例のためのハッシュ関数の単純で素朴な実装は文字列の最初の文字を取り、それを整数に写像することができます。シマウマ "25などになります.

次に、26個のバケットの配列(Javaの場合はArrayListsになります)を作成し、キーのハッシュコードと一致するアイテムをバケットに入れます。同じ文字で始まるキーを持つ複数のアイテムがある場合、それらは同じハッシュコードを持つことになるため、すべてのハッシュコードのバケットに入るため、バケット内で線形検索を行う必要があります特定の商品を探す.

この例では、アルファベットにまたがるキーを持つ数十個のアイテムがあるだけなら、それは非常にうまく機能します。ただし、100万個のアイテムがある場合、またはすべてのキーが 'a'または 'b'で始まる場合、ハッシュテーブルは理想的ではありません。より良いパフォーマンスを得るためには、異なるハッシュ関数やもっと多くのバケットが必要です。

3
Greg Graham

ハッシュがどのように計算されるかは、通常ハッシュテーブルではなく、それに追加された項目に依存します。 .netやJavaなどのフレームワーク/基本クラスライブラリでは、各オブジェクトにこのオブジェクトのハッシュコードを返すGetHashCode()(または同様の)メソッドがあります。理想的なハッシュコードアルゴリズムと正確な実装はオブジェクトで表されるデータに依存します。

2
Lucero

ハッシュテーブルは、実際の計算がランダムアクセスマシンモデルに従うという事実、すなわち、メモリ内の任意のアドレスの値にO(1)時間または一定時間でアクセスできるという事実に完全に作用する。

したがって、私がキーのユニバース(アプリケーションで使用できるすべての可能なキーのセット、たとえば学生用のロール番号、4桁の場合、このユニバースは1〜9999の数字のセットです)。それらをサイズの有限の数の集合にマッピングする方法私のシステムにメモリを割り当てることができます。理論的には私のハッシュテーブルは準備ができています。

一般的に、アプリケーションでは、キーのユニバースのサイズはハッシュテーブルに追加したい要素の数よりも非常に大きくなります(1 GBのメモリを無駄にしたくない、たとえば整数が10万または10万の整数値です)。バイナリ表現で少し長い)。だから、私たちはこのハッシュを使います。それは一種の混合した種類の「数学的」操作であり、それは私の大きな宇宙を私が記憶に収容できる小さな値の集合に写像する。実際の場合、多くの場合、ハッシュテーブルのスペースは(要素数*各要素のサイズ)と同じ「次数」(big-O)であるため、メモリを無駄に消費することはありません。

現在、大規模なセットは小規模なセットにマッピングされていますが、マッピングは多対1である必要があります。そのため、異なる鍵が同じスペースに割り当てられます(公平ではありません)。これを処理する方法はいくつかありますが、私はそのうち人気のある2つを知っているだけです。

  • リンクリストへの参照として、値に割り当てられていたスペースを使用します。このリンクリストは、1対1マッピングで同じスロットに存在するようになる1つ以上の値を格納します。リンクされたリストには、検索してくる人を助けるためのキーも含まれています。配達員が来るとき、それは同じアパートにいる多くの人々のようなものです、彼は部屋に行き、そして特にその人に尋ねます。
  • 単一の値ではなく、毎回同じ値のシーケンスを返す配列で二重ハッシュ関数を使用します。値を保存しようとすると、必要なメモリ位置が空いているのか、占有されているのかがわかります。無料の場合は、自分の価値をそこに格納できます。占有されている場合は、シーケンスから次の価値を取得するなど、空きの場所が見つかるまで自分の価値を格納できます。値を検索または取得するときは、シーケンスで指定されたのと同じパスに戻り、値が見つかるまで、または配列内のすべての可能性のある位置を検索するまで、値があるかどうかを各位置でたずねます。

CLRSによるアルゴリズム入門は、このトピックに関する非常に優れた洞察を提供します。

2
div

プログラミング用語を探しているすべての人にとって、これがどのように機能するかです。高度なハッシュテーブルの内部実装には、ストレージの割り当て/割り当て解除および検索に関して多くの複雑さと最適化がありますが、トップレベルの考え方はほとんど同じです。

(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()はすべての一意性の魔法が起こらなければならないハッシュ関数です。

経験則は以下のとおりです。指定された値を挿入するには、bucketは一意であり、値から派生可能である必要があります

バケツは値が格納されているスペースです - ここでは配列インデックスとしてintを保持していますが、それはおそらくメモリロケーションでもあります。

0
Nirav Bhatt