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


494

ハッシュテーブルがどのように機能するかについての説明を探しています。

たとえば、私はそれがキーを受け取り、ハッシュを計算し(方法の説明を探しています)、ある種のモジュロを実行して、値が格納されている配列のどこにあるかを計算しますが、ここで私の知識が停止します。

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

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


4
最近、この(en.algoritmy.net/article/50101/Hash-table)記事を書いて、ハッシュテーブルとその戦略(分離チェーン、線形プローブ、ダブルハッシュ)にアクセントを付けて、データを格納および検索する方法を説明しました)
malejpavouk 2013年

1
ハッシュテーブルは、配列の拡張バージョンと考えることができます。これは、連続する整数キーだけに限定されません。
user253751 2016

1
ここでは別のものである:intelligentjava.wordpress.com/2016/10/19/...
nesvarbu

回答:


913

ここでは素人の言葉で説明します。

ライブラリを本でいっぱいにしたいだけでなく、必要なときに簡単に見つけられるようにしたいとします。

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

それで、どうすればそれができますか?まあ、もちろん、各本を置いた場所のある種のリストを保持することはできますが、ライブラリを検索するのと同じ問題が発生するので、リストを検索する必要があります。当然のことながら、リストは小さくて検索しやすくなりますが、ライブラリ(またはリスト)の一方の端からもう一方の端まで順番に検索する必要はありません。

あなたは本のタイトルであなたにすぐにあなたに正しい場所を与えることができる何かが欲しいので、あなたがしなければならないことはただ正しい棚に歩いて行き、本を手に取るだけです。

しかし、それはどのように行うことができますか?ええと、ライブラリをいっぱいにするときは少し先見の明があり、ライブラリをいっぱいにするときは多くの作業が必要です。

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

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

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

簡単にするために、各文字と記号を数値に変換し、それらをすべて合計するとします。実際にはもっと複雑ですが、とりあえずはそのままにしておきましょう。

このようなアルゴリズムの優れた点は、同じ入力を何度もフィードすると、毎回同じ数が吐き出されることです。

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に入れます。

これは次の問題につながります。衝突。アルゴリズムは、本を正確に(または、可能であればハッシュテーブルに)埋めるように本を配置する方法がないため、常に、以前に使用された数を計算することになります。図書館という意味では、本を入れたい棚とスロット番号にたどり着くと、そこにはすでに本があります。

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

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

この説明がバケットと関数よりも地球に少し通じていることを願っています:)


そのような素晴らしい説明をありがとう。4.x .Netフレームワークでどのように実装されているかに関する技術的な詳細がどこにあるか知っていますか
Johnny_D 2015年

いいえ、それは単なる数字です。各シェルフとスロットに0または1から始まり、そのシェルフのスロットごとに1ずつ増加する番号を付け、次に次のシェルフで番号を付けるだけです。
Lasse V. Karlsen、2015年

2
「さまざまな衝突処理方法が存在し、データをさらに別の計算で実行して、テーブル内の別のスポットを取得する」-別の計算とはどういう意味ですか?それは単なる別のアルゴリズムですか?では、本の名前に基づいて異なる数値を出力する別のアルゴリズムを使用するとします。その後、その本を見つけた場合、どのアルゴリズムを使用すればよいのでしょうか。タイトルが探している本が見つかるまで、最初のアルゴリズム、2番目のアルゴリズムなどを使用しますか?
user107986 2015

1
@KyleDelaney:クローズドハッシュの場合(衝突は代替バケットを見つけることで処理されます。つまり、メモリ使用量は修正されますが、バケット全体の検索により多くの時間を費やします)。以下のために開いているの連鎖別名ハッシュ(故意にいくつかの敵/ハッカーによって衝突する細工ひどいハッシュ関数または入力)病的な場合には、あなたはハッシュ・バケットが空に最もで終わることができなかったが、総メモリ使用量には悪いです-ちょうどより多くのポインタの代わりにNULL有用なデータへのインデックス付け。
トニー・デルロイ2017

3
@KyleDelaney:コメントの通知を受け取るには "@Tony"が必要です。連鎖について疑問に思っているようです。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バケットは「無駄」ですか?ちょっと、そうではありません。同じ合計メモリ使用量。
Tony Delroy、2018

104

使用法とLingo:

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

実際の例:

Hash&Co .は1803年に設立され、コンピュータ技術を何も欠いており、約30,000のクライアントの詳細情報(レコード)を保持するために、合計300のファイリングキャビネットがありました。各ファイルフォルダーは、クライアント番号(0〜29,999の一意の番号)で明確に識別されました。

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

クライアントの記録を提出するために、事務員はフォルダに書かれた一意のクライアント番号を使用します。このクライアント番号を使用して、ハッシュキーを300 だけ変調して、含まれているファイリングキャビネットを識別します。ファイリングキャビネットを開くと、クライアント番号順に並べられた多くのフォルダーが含まれていることがわかります。正しい場所を特定した後、彼らは単にそれを差し込みます。

クライアントのレコードを取得するために、ファイリング担当者には、紙片にクライアント番号が付与されます。この一意のクライアント番号(ハッシュキー)を使用して、どのファイリングキャビネットにクライアントフォルダーがあったかを判断するために、この番号を300ずつ変調します。ファイリングキャビネットを開くと、クライアント番号順に並べられた多くのフォルダーが含まれていることがわかりました。レコードを検索すると、クライアントフォルダーがすぐに見つかり、取得されます。

実際の例では、バケットファイリングキャビネットで、レコードファイルフォルダーです。


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

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

したがって、上記の例では、30,000人程度のクライアントがより小さなスペースにマッピングされています。


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

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

お役に立てれば、

ジーチ!


2
可変的に「オープンアドレッシング」または「クローズドアドレッシング」(そうですが悲しいですが真実)または「チェーン」と呼ばれる特定のタイプのハッシュテーブル衝突回避戦略について説明します。リストバケットを使用せず、アイテムを「インライン」で保存する別のタイプがあります。
Konrad Rudolph、

2
優れた説明。ただし、各ファイリングキャビネットには、平均で約100レコードが含まれます(30kレコード/ 300キャビネット= 100)。編集する価値があるかもしれません。
Ryan Tuck 14年

@TonyD、このサイトsha-1にオンラインでアクセスTonyDし、テキストフィールドに入力したSHA-1ハッシュを生成します。最終的には、次のような値が生成されますe5dc41578f88877b333c8b31634cf77e4911ed8c。これは、160ビット(20バイト)の大きな16進数にすぎません。これを使用して、レコードの保存に使用するバケット(数量限定)を決定できます。
Jeachは2016年

@TonyD、「ハッシュキー」という用語が競合する問題のどこで参照されているのかわかりませんか?その場合は、2か所以上を指摘してください。それとも、「私たち」は「ハッシュキー」という用語を使用しているのに対し、Wikipediaなどの他のサイトは「ハッシュ値、ハッシュコード、ハッシュ合計、または単にハッシュ」を使用していると言っていますか?もしそうなら、使用された用語がグループまたは組織内で一貫している限り、誰が気にかけますか。プログラマーは「キー」という用語をよく使用します。私は個人的に、別の良い選択肢は「ハッシュ値」であると主張します。しかし、「ハッシュコード、ハッシュサム、または単にハッシュ」の使用は除外します。言葉ではなくアルゴリズムに集中してください!
Jeach

2
@TonyD、私はテキストを「ハッシュキーを300でモジュール化する」に変更しました。誰にとってもより明確で明確になることを期待しています。ありがとう!
Jeach

64

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

基本的に、ハッシュ関数は1つのスペース(たとえば、任意の長さの文字列)から物事を取り出し、それらをインデックス付けに役立つスペース(符号なし整数など)にマップする関数にすぎません。

ハッシュするもののスペースが少ない場合は、それらを整数として解釈するだけで済む可能性があり、これで完了です(例:4バイト文字列)

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

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

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

実際には、通常、アプリケーションでうまく機能することがわかっている関数を見つけて使用できるはずです。

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

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

もちろん、実際には通常これを行うことはできません。メモリを浪費しすぎます。したがって、スパース配列に基づいてすべてを実行します(唯一のエントリは実際に使用するものであり、その他はすべて暗黙的にnullです)。

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


1
申し訳ありませんが、これは古い質問/回答であることは承知していますが、この最後の指摘を理解しようと努めています。ハッシュテーブルはO(1)時間複雑です。ただし、スパース配列を使用すると、値を見つけるためにバイナリ検索を実行する必要がなくなりますか?その時点で、時間の複雑性はO(log n)にならないのですか?
herbrandson

@herbrandson:いいえ...スパース配列は、比較的少数のインデックスに値が入力されていることを意味します-キーから計算したハッシュ値の特定の配列要素に直接インデックスを付けることができます。それでも、サイモンが説明する疎配列の実装は非常に限られた状況でのみ正気です:バケットサイズがメモリページサイズのオーダーである場合(たとえばint、1000分の1 のスパース性と4kページのキー=タッチされたほとんどのページ)、およびOSの扱いすべて-0ページアドレス空間が十分にあるとき、効率的...、(ので、すべての未使用バケットページはバックアップメモリを必要としない)
トニー・デルロイ

@TonyDelroy-それは過度に単純化されていることは事実ですが、アイデアはそれらが何であるか、そしてその理由の概要を示すことであり、実際的な実装ではありません。後者の詳細は、拡張でうなずくように、より微妙です。
サイモン

48

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

ハッシュテーブルは、リンクリストの配列として実装されることがよくあります。人の名前を格納するテーブルを想像すると、数回挿入した後、次のようにメモリに配置される可能性があります。ここで、-で()囲まれた数字は、テキスト/名前のハッシュ値です。

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での%)。これにより、ランダムマッピングによって達成されるよりも、衝突が少なくなり、衝突する要素のリンクリストが長くなります。また、強力なハッシュを生成するのにかかる時間を節約でき、メモリ内の近くのバケットでキーが見つかるようにキーが検索されると、キャッシュヒットが改善されます。キーうまくインクリメントされない場合、それらの配置が完全にランダム化されてバケットに完全にランダム化される強力なハッシュ関数を必要としないほど十分にランダムになることが期待されます。


6
素晴らしい答えです。
CRThaze 2017年

@Tony Delroy素晴らしい答えをありがとう。私はまだ心の中で一つのオープンポイントを持っています。1億個のバケットがある場合でも、ルックアップ時間は負荷係数1と暗号強度ハッシュ関数を使用したO(1)になると言います。しかし、1億個の適切なバケットを見つけることはどうでしょうか。すべてのバケットをソートしても、O(log100.000.000)ではありませんか?バケットを見つけるにはどうすればO(1)ですか?
セルマン2018

@selman:質問には、O(log100,000,000)と思われる理由を説明する詳細はあまりありませんが、「すべてのバケットがソートされている場合でも」と答えます-ハッシュテーブルバケットの値は通常の意味で「ソート」されることはありません。どの値がどのバケットに現れるかは、キーにハッシュ関数を適用することによって決定されます。複雑さはO(log100,000,000)であると考えると、ソートされたバケットを使用してバイナリ検索を行うことを想像できますが、それはハッシュの仕組みではありません。たぶん他のいくつかの答えを読んで、それがより意味をなさないかどうかを確認してください。
トニーDelroy

@TonyDelroy確かに、「ソートされたバケット」は、私が想像する最高のシナリオです。したがって、O(log100,000,000)です。しかし、これが当てはまらない場合、アプリケーションは数百万の中から関連するバケットをどのように見つけることができますか?ハッシュ関数はどういうわけかメモリ位置を生成しますか?
セルマン2018

1
@selman:コンピュータメモリは一定時間の「ランダムアクセス」を許可するため、メモリアドレスを計算できれば、配列の他の部分のメモリにアクセスしなくても、メモリの内容を取得できます。したがって、最初のバケット、最後のバケット、またはその間のいずれのバケットにアクセスしても、CPUのL1 / L2 / L3メモリキャッシングの影響を受けますが、同じパフォーマンス特性になります(大まかに、同じ時間かかりますが、これらは、最近アクセスしたバケットまたは偶然に近くにあるバケットにすばやく再アクセスするためにのみ機能し、big-O分析では無視できます)。
トニーDelroy

24

皆さんはこれを完全に説明するのに非常に近いですが、いくつか欠落しています。ハッシュテーブルは単なる配列です。アレイ自体は、各スロットに何かが含まれます。少なくとも、ハッシュ値または値自体をこのスロットに格納します。これに加えて、このスロットで衝突したリンクされた値/チェーンされた値のリストを保存したり、オープンアドレス指定メソッドを使用したりすることもできます。このスロットから取り出したい他のデータへのポインタを格納することもできます。

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

uint slotIndex = hashValue % hashTableSize;

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

slotIndex = (remainder + 1) % hashTableSize;

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

係数法では、たとえばサイズが1000のテーブルがある場合、1〜1000のハッシュ値はすべて対応するスロットに入ります。負の値、および1000より大きい値は、競合するスロット値になる可能性があります。これが発生する可能性は、ハッシュ方式と、ハッシュテーブルに追加したアイテムの総数の両方に依存します。一般に、ハッシュテーブルに追加される値の総数がそのサイズの約70%に等しくなるように、ハッシュテーブルのサイズを作成することをお勧めします。ハッシュ関数が均等に分散する場合、通常、バケット/スロットの衝突はほとんど発生しないか、まったく発生せず、ルックアップと書き込みの両方の操作で非常に高速に実行されます。追加する値の総数が事前にわからない場合は、何らかの手段を使用して適切な推定を行ってください。

これがお役に立てば幸いです。

PS-C#ではGetHashCode()メソッドがかなり遅く、テストした多くの条件下で実際の値の衝突が発生します。実際の楽しみとして、独自のハッシュ関数を作成し、ハッシュする特定のデータと衝突しないようにし、GetHashCodeよりも高速に実行し、かなり均等に分散するようにしてください。私はこれを、intサイズのハッシュコード値の代わりにlongを使用して行いました。これは、0の衝突で、ハッシュテーブル内の最大3200万のハッシュ値全体で非常にうまく機能しました。残念ながら、コードは私の雇用者のものなので共有することはできません...しかし、特定のデータドメインで可能であることを明らかにできます。これを達成できる場合、ハッシュテーブルは非常に高速です。:)


私は投稿がかなり古いことを知っていますが、誰かがここで(剰余+ 1)の意味を説明できます
Hari

3
@Hari remainderは元のモジュロ計算の結果を参照し、次の使用可能なスロットを見つけるために1を追加します。
x4nd3r 2015年

「配列自体には各スロットに何かが含まれます。少なくとも、このスロットにはハッシュ値または値自体を格納します。」-「スロット」(バケット)にはまったく値を格納しないのが一般的です。オープンアドレス指定の実装では、多くの場合、NULLまたはリンクされたリストの最初のノードへのポインターが格納されます-スロット/バケットに直接値はありません。 「他のものに興味があるだろう」 -あなたが説明する「+1」は、線形探索と呼ばれ、より優れたパフォーマンスを発揮します二次探索と呼ばれます「通常、バケット/スロットの衝突はほとんど発生しないか、まったく発生しません」 -@ 70%の容量、最大12%のスロット(2つの値)、最大3%3 ....
トニーデルロイ

「これは、intサイズのハッシュコード値の代わりにlongを使用してこれを実行しました。これは、0の衝突で、ハッシュテーブル内の最大3200万のハッシュ値全体で非常にうまく機能しました。」-これは、バケットの数よりもはるかに大きな範囲でキーの値が実質的にランダムである一般的なケースでは、単純に不可能です。多くの場合、個別のハッシュ値を持つことは十分に簡単です(そして、longハッシュ値の話はそれが達成したことを意味します)が、mod /%操作の後にハッシュテーブルでそれらが衝突しないことを保証することに注意してください(一般的な場合) )。
Tony Delroy、

(すべての衝突を回避することは完全なハッシュとして知られています。一般に、事前にわかっている数百または数千のキーに対して実用的です-gperfは、そのようなハッシュ関数を計算するためのツールの例です。状況-たとえば、キーが独自のメモリプールからのオブジェクトへのポインタであり、かなりフルに保たれている場合、各ポインタを一定の距離だけ離して、ポインタをその距離で除算し、インデックスをわずかにまばらな配列にして、衝突。)
トニー・デルロイ、

17

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

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

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

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


13

短くて甘い:

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


11

それよりももっと簡単です。

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

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

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

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


10

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

それぞれについて、ハッシュと呼ばれるインデックスを作成します。ハッシュについての重要なことは、ハッシュがたくさん「散在する」ことです。2つの類似したものに類似したハッシュを持たせたくない場合。

ハッシュで示された位置にあるものを配列に入れます。与えられたハッシュで複数のものが発生する可能性があるため、配列など適切なものに格納します。これは通常、バケットと呼ばれます。

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

ハッシュがうまく機能していて、配列が十分に大きい場合、配列内の特定のインデックスにはせいぜい数個のものが存在するだけなので、あまり調べる必要はありません。

ボーナスポイントについては、ハッシュテーブルにアクセスしたときに、見つかったものがあれば(存在する場合)バケットの先頭に移動するようにしてください。次回は最初にチェックされます。


1
他の誰もが言及し忘れていた最後のポイントに感謝します
Sandeep Raju Prabhakar

4

これまでの答えはすべて良好で、ハッシュテーブルの動作のさまざまな側面を理解できます。ここに役立つかもしれない簡単な例があります。小文字のアルファベット文字列をキーとしていくつかのアイテムを保存するとします。

サイモンが説明したように、ハッシュ関数は大きな空間から小さな空間へのマッピングに使用されます。この例のハッシュ関数の単純で素朴な実装では、文字列の最初の文字を取得して整数にマッピングできるため、「alligator」のハッシュコードは0、「bee」のハッシュコードは1になります。ゼブラ」は25などになります。

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

この例では、アルファベットにまたがるキーを持つアイテムが数ダースしかない場合、非常にうまく機能します。ただし、100万個のアイテムまたはすべてのキーがすべて「a」または「b」で始まる場合、ハッシュテーブルは理想的ではありません。パフォーマンスを向上させるには、別のハッシュ関数やバケットを追加する必要があります。


3

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

私はあなたが配列Aの概念を理解していると思います。これは、Aがどんなに大きくても、1番目のステップでI番目の要素A [I]に到達できるインデックス作成の操作をサポートするものです。

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

ただし、もちろん同じ年齢の人が複数存在する可能性があるため、各エントリで配列に入力するのは、その年齢のすべての人のリストです。そのため、1つのステップで個人の情報にアクセスし、そのリストで少し検索することもできます(「バケット」と呼ばれます)。バケットが大きくなるほど多くの人がいる場合にのみ、速度が低下します。次に、より大きな配列と、年齢を使用する代わりに、姓の最初の数文字など、個人に関するより多くの識別情報を取得する他の方法が必要です。

それが基本的な考え方です。年齢を使用する代わりに、値の良い広がりを生成する人の任意の関数を使用できます。それがハッシュ関数です。同様に、人の名前のASCII表現の3ビットごとに、ある順序でスクランブルをかけることができます。重要なのは、同じバケットにハッシュ化する人が多すぎないようにすることです。速度はバケットのサイズが小さいことに依存するためです。


2

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


2

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

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

一般に、アプリケーションでは、キーのユニバースのサイズが、ハッシュテーブルに追加する要素の数よりも非常に大きい(32 GBであるため、1 GBのメモリを無駄にしたくない、たとえば10000または100000の整数値をハッシュしたくない)バイナリ表現では少し長い)。したがって、このハッシュを使用します。これは、一種の「数学的な」操作の混合です。これは、私の大きな宇宙を、メモリに収容できる小さな値のセットにマッピングします。実際の場合、ハッシュテーブルのスペースは、(要素数*各要素のサイズ)と同じ「順序」(big-O)であることが多いため、多くのメモリを無駄にしません。

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

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

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


0

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

(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を保持しましたが、おそらくメモリの場所でもあります。


1
「経験則として、特定の値を挿入するには、バケットが一意であり、格納するはずの値から派生可能である必要があります。」-これは完全なハッシュ関数を表します。これは通常、コンパイル時に既知の数百または数千の値に対してのみ可能です。ほとんどのハッシュテーブルは衝突を処理する必要があります。また、ハッシュテーブルは空であるかどうかに関係なく、すべてのバケットにスペースを割り当てる傾向がありますが、擬似コードcreate_extra_space_for_bucket()は新しいキーの挿入中のステップを文書化します。ただし、バケットはポインタの場合があります。
トニーデルロイ2017
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.