ハッシュテーブルがO(1)を達成できることはよく知られているようですが、それは私には意味がありません。誰か説明してもらえますか?ここに思い浮かぶ2つの状況があります:
A.値は、ハッシュテーブルのサイズより小さいintです。したがって、値は独自のハッシュなので、ハッシュテーブルはありません。しかし、もしあれば、O(1)であり、それでも非効率的です。
B.値のハッシュを計算する必要があります。この状況では、順序はO(n)検索されるデータのサイズについて。検索はO(1)作業後O(n)であるが、それでも出てくるto O(n)私の目に。
また、完全なハッシュまたは大きなハッシュテーブルがない限り、バケットごとにおそらくいくつかのアイテムがあります。そのため、いずれかの時点で小さな線形検索に移ります。
ハッシュテーブルは素晴らしいと思いますが、理論的に想定されている場合を除き、O(1)の指定は得られません。
ウィキペディアの ハッシュテーブルの記事 は常に一定のルックアップ時間を参照し、ハッシュ関数のコストを完全に無視します。それは本当に公平な尺度ですか?
Edit:私が学んだことを要約するには:
ハッシュ関数はキー内のすべての情報を使用する必要がないため一定の時間になる可能性があり、十分に大きいテーブルは衝突を一定の時間近くにまで低下させる可能性があるため、技術的には真実です。
ハッシュ関数とテーブルサイズが衝突を最小限に抑えるように選択されている限り、一定の時間のハッシュ関数を使用しないことを意味する場合が多いので、実際には事実です。
ここには、mとnの2つの変数があります。ここで、mは入力の長さ、nはハッシュ内のアイテムの数です。
O(1)ルックアップパフォーマンスの主張は、少なくとも2つの仮定を行います。
オブジェクトが可変サイズであり、等価性チェックですべてのビットを調べる必要がある場合、パフォーマンスはO(m)になります。ただし、ハッシュ関数はO(m)-O(1)である必要はありません。暗号化ハッシュとは異なり、辞書で使用するハッシュ関数は見る必要はありません。ハッシュを計算するために入力のすべてのビットを実装します。実装では、固定数のビットのみを自由に調べることができます。
十分な数のアイテムの場合、アイテムの数は可能なハッシュの数よりも多くなり、衝突が発生するとパフォーマンスがO(1)を超えます。たとえば、O(n)単純なリンクリストトラバーサル(または両方の仮定が偽の場合はO(n * m))。
実際には、O(1)クレームは技術的に偽ですが、多くの現実世界の状況、特に上記の仮定が当てはまる状況ではおよそ trueです。
ハッシュを計算する必要があるため、検索されるデータのサイズの順序はO(n)です。検索はO(1) =行った後O(n)仕事ですが、それでもO(n)になります。
何?単一の要素をハッシュするには一定の時間がかかります。なぜそれが他のものになるのでしょうか? n
要素を挿入する場合、はい、n
ハッシュを計算する必要があり、線形時間を要します...要素を検索するには、何の単一のハッシュを計算します「探している、それで適切なバケットを見つけます。ハッシュテーブルに既にあるすべてのハッシュを再計算することはありません。
また、完全なハッシュまたは大きなハッシュテーブルがない限り、バケットごとに複数のアイテムが存在する可能性があるため、いずれにしても、ある時点で小さな線形検索になります。
必ずしも。バケットは必ずしもリストや配列である必要はありません。バランスの取れたBSTなど、任意のコンテナタイプにすることができます。これは、O(log n)
最悪の場合を意味します。しかし、これが、1つのバケットに多くの要素を入れないように適切なハッシュ関数を選択することが重要な理由です。 KennyTMが指摘したように、平均して、たまにバケットを掘らなければならない場合でも、O(1)
時間を取得できます。
ハッシュテーブルのトレードオフは、もちろんスペースの複雑さです。あなたは時間と空間を交換していますが、これはコンピューティング科学の通常のケースのようです。
他のコメントの1つで、文字列をキーとして使用することに言及しています。あなたは文字列のハッシュを計算するのにかかる時間を心配しています、なぜならそれはいくつかの文字で構成されているからですか?他の誰かが再び指摘したように、ハッシュを計算するために必ずしもすべての文字を見る必要はありませんが、そうすればより良いハッシュが生成されるかもしれません。その場合、キーに平均m
文字があり、それらすべてを使用してハッシュを計算した場合、その検索にはO(m)
が必要だと思います。 m >> n
その後、問題が発生する可能性があります。その場合、おそらくBSTを使用した方が良いでしょう。または、より安価なハッシュ関数を選択します。
ハッシュのサイズは固定です-適切なハッシュバケットの検索は、固定コストの操作です。これは、O(1)であることを意味します。
ハッシュの計算は、特に費用のかかる操作である必要はありません。ここでは、暗号化ハッシュ関数について説明していません。しかし、それはby byです。ハッシュ関数の計算自体は、要素の数nに依存しません。要素内のデータのサイズに依存する場合がありますが、これはnが参照するものではありません。したがって、ハッシュの計算はnに依存せず、O(1)でもあります。
ハッシュはO(1)テーブル内に一定数のキーのみがあり、他の仮定が行われている場合のみです。しかし、そのような場合には利点があります。
キーにnビット表現がある場合、ハッシュ関数はこれらのビットの1、2、... nを使用できます。 1ビットを使用するハッシュ関数について考えます。評価はO(1)です。ただし、キースペースを2に分割するだけです。したがって、2 ^(n-1)個ものキーを同じビンにマッピングしています。 BST検索では、ほぼ一杯の場合、特定のキーを見つけるのに最大n-1ステップかかります。
これを拡張して、ハッシュ関数がKビットを使用する場合、ビンサイズが2 ^(n-k)であることを確認できます。
したがって、Kビットハッシュ関数==> 2 ^ K以下の有効なビン==>ビンごとに最大2 ^(n-K)nビットキー==>(n-K)ステップ(BST)で衝突を解決します。実際、ほとんどのハッシュ関数は「効果的」ではなく、2 ^ k個のビンを生成するためにKビット以上必要です。したがって、これでも楽観的です。
このように表示できます。最悪の場合、nビットのキーのペアを一意に区別できるようにするには、〜nステップが必要です。この情報理論の制限を回避する方法は、ハッシュテーブルかどうかにかかわらず、本当にありません。
ただし、これはハッシュテーブルの使用方法/使用方法ではありません!
複雑さの分析では、nビットキーの場合、テーブルにO(2 ^ n)キーがあると想定しています(すべての可能なキーの1/4など)。しかし、ハッシュテーブルを常に使用するとは限りませんが、テーブルにはnビットキーが一定数しかありません。テーブルに一定数のキーのみが必要な場合、たとえばCが最大数である場合、O(C) binsのハッシュテーブルを作成できます。キーのnビットの〜logCを使用するハッシュ関数。すべてのクエリはO(logC) = O(1)です。これが人々の主張です。ハッシュテーブルアクセスはO(1) "/
ここにはいくつかの問題があります-最初に、すべてのビットが必要なわけではないということは、請求のトリックにすぎないかもしれません。最初に、キー値をハッシュ関数に実際に渡すことはできません。これは、O(n)であるメモリ内のnビットを移動するためです。だから、例えばする必要があります参照渡し。ただし、O(n)操作であった)を既にどこかに保存する必要があります;ハッシュに課金しないだけで、計算タスク全体でこれを回避することはできません。ハッシュ、ビンを見つけ、複数のキーを見つけました。コストは解決方法に依存します-比較ベース(BSTまたはリスト)を行う場合、O(n)操作(リコールキーはnビット); 2番目のハッシュを行う場合、2番目のハッシュに衝突がある場合、同じ問題が発生します。したがって、O(1)は100%保証されません。衝突しない(キーよりも多くのビンを持つテーブルを使用することでチャンスを改善できますが、それでも可能です)。
代替案を検討してください。この場合、BST。 Cキーがあるため、バランスのとれたBSTはO(logC)=の深さになるため、検索にはO(logC)ステップが必要です。ただし、この場合はO(n)操作...になるので、この場合はハッシュがより良い選択であると思われます。
TL; DR:ハッシュ関数の普遍的なファミリからランダムにハッシュ関数を一様に選択した場合、ハッシュテーブルはO(1)
で予測される最悪のケース時間を保証します。予想される最悪のケースは、平均的なケースと同じではありません。
免責事項:ハッシュテーブルがO(1)
であることを正式には証明しません。そのため、コースラのこのビデオをご覧ください[ 1 ]。また、ハッシュテーブルのamortizedの側面についても説明しません。これは、ハッシュと衝突に関する議論と直交しています。
他の回答やコメントでこのトピックの周りに驚くほど多くの混乱が見られますが、この長い回答でそれらのいくつかを修正しようとします。
最悪の場合の分析にはさまざまな種類があります。ここまでほとんどの答えがここまでに行った分析ではない最悪のケースではなく、むしろ平均的なケース [ 2 ]。 平均ケース分析分析はより実用的である傾向があります。アルゴリズムには最悪の最悪の入力が1つあるかもしれませんが、実際には他のすべての入力に対してうまく機能します。ボトムラインはランタイムですデータセットに依存実行中です。
ハッシュテーブルのget
メソッドの次の擬似コードを検討してください。ここでは、連鎖によって衝突を処理すると想定しているため、テーブルの各エントリは(key,value)
ペアのリンクリストです。また、バケットの数m
は固定されているが、O(n)
であると想定しています。ここで、n
は入力の要素数です。
function get(a: Table with m buckets, k: Key being looked up)
bucket <- compute hash(k) modulo m
for each (key,value) in a[bucket]
return value if k == key
return not_found
他の回答が指摘しているように、これは平均O(1)
および最悪の場合O(n)
で実行されます。ここでチャレンジすることで証明のスケッチを作成できます。課題は次のとおりです。
(1)ハッシュテーブルアルゴリズムを敵に渡します。
(2)敵はそれを研究し、望む限り準備することができます。
(3)最後に、攻撃者はテーブルに挿入するサイズn
の入力を提供します。
問題は、攻撃者の入力に対するハッシュテーブルの速度です。
ステップ(1)から、攻撃者はハッシュ関数を知っています。ステップ(2)で、攻撃者は同じhash modulo m
を持つn
要素のリストを作成できます。一連の要素のハッシュをランダムに計算します。その後、(3)でそのリストを提供できます。ただし、すべてのn
要素が同じバケットにハッシュされるため、アルゴリズムはそのバケット内のリンクリストを走査するのにO(n)
時間かかります。チャレンジを何度再試行しても、敵は常に勝ちます。それはアルゴリズムがどれほど悪いか、最悪の場合O(n)
です。
前の課題で私たちを驚かせたのは、敵がハッシュ関数を非常によく知っていて、その知識を使用して可能な限り最悪の入力を作成できることでした。常に1つの固定ハッシュ関数を使用する代わりに、実行時にアルゴリズムがランダムに選択できる一連のハッシュ関数H
が実際にあったとしたらどうでしょうか。好奇心が強い場合、H
はユニバーサルファミリーのハッシュ関数と呼ばれます[]。さて、これにrandomnessを追加してみましょう。
まず、ハッシュテーブルにシードr
が含まれており、r
が構築時に乱数に割り当てられているとします。一度割り当てると、そのハッシュテーブルインスタンスに対して固定されます。それでは、疑似コードをもう一度見てみましょう。
function get(a: Table with m buckets and seed r, k: Key being looked up)
rHash <- H[r]
bucket <- compute rHash(k) modulo m
for each (key,value) in a[bucket]
return value if k == key
return not_found
もう一度チャレンジすると、ステップ(1)から、攻撃者はH
にあるすべてのハッシュ関数を知ることができますが、使用する特定のハッシュ関数はr
に依存します。 r
の値は構造に対してプライベートであり、攻撃者は実行時にそれを検査したり、事前に予測したりすることはできません。したがって、常に悪いリストを作成することはできません。ステップ(2)で、敵がhash
内の1つの関数H
をランダムに選択し、hash modulo m
の下でn
衝突のリストを作成すると仮定します。実行時にH[r]
が選択したhash
と同じになるように、ステップ(3)にそれを送信します。
これは敵にとって深刻な賭けです。彼が作成したリストはhash
の下で衝突しますが、H
の他のハッシュ関数の下でのランダムな入力になります。彼がこの賭けに勝った場合、実行時間は以前のように最悪の場合O(n)
になりますが、負けた場合、平均O(1)
時間を取るランダムな入力が与えられます。そして実際、ほとんどの場合、敵は負け、|H|
チャレンジごとに1回だけ勝ち、|H|
を非常に大きくすることができます。
この結果を、敵が常にチャレンジに勝った以前のアルゴリズムと比較してください。ここで少し手を振っていますが、most times敵は失敗し、これは敵が試行できるすべての可能な戦略に当てはまります。最悪のケースはO(n)
ですが、 予想される最悪の場合は実際にはO(1)
です。
繰り返しますが、これは正式な証拠ではありません。この予想される最悪のケース分析から得られる保証は、実行時間が特定の入力に依存しないになったことです。これは、動機付けられた敵が悪い入力を簡単に作成できることを示した平均的なケース分析とは対照的に、本当にランダムな保証です。
A.値は、ハッシュテーブルのサイズより小さいintです。したがって、値は独自のハッシュであるため、ハッシュテーブルはありません。しかし、もしあれば、O(1)であり、それでも非効率的です。
これは、キーを個別のバケットに簡単にマッピングできる場合です。そのため、配列はハッシュテーブルよりもデータ構造の選択として適しているようです。それでも、非効率性はテーブルのサイズに応じて増加しません。
(プログラムの進化に合わせてintがテーブルサイズよりも小さいことを信頼していないため、ハッシュテーブルを使用することもできます。その関係が成り立たない場合はコードを潜在的に再利用可能にしたい、または単にコードを読んだり維持したりする人々が、関係を理解し、維持する精神的な努力を無駄にしなければならないようにする).
B.値のハッシュを計算する必要があります。この状況では、順序はO(n)検索されるデータのサイズに対してです。検索はO(1) O(n)仕事ですが、それでも私の目ではO(n)になります。
キーのサイズ(バイト単位など)と、ハッシュテーブルに格納されているキーの数のサイズを区別する必要があります。ハッシュテーブルが提供するクレームO(1)操作は、操作(insert/erase/find)は傾向がないことを意味しますキーの数が数百から数千から数百万から数十億に増加するとさらに遅くなります(少なくともすべてのデータが同等の高速ストレージでアクセス/更新される場合は、 RAMまたはディスク-キャッシュ効果が作用する可能性がありますが、最悪の場合のキャッシュミスのコストでさえ、最高の場合のヒットの定数倍になる傾向があります)。
電話帳を考えてみましょう:かなり長い名前があるかもしれませんが、本の名前が100であっても1000万であっても、平均的な名前の長さはかなり一貫しており、歴史上最悪です...
誰もが使用した最長名のギネス世界記録は、アドルフ・ブレイン・チャールズ・デイヴィッド・アール・フレデリック・ジェラルド・ヒューバート・アーヴィン・ジョン・ケネス・ロイド・マーティン・ネロ・オリバー・ポール・クインシー・ランドルフ・シャーマン・トーマス・アンカス・ヴィクター・ウィリアム・クセルクセス・ヤンシー・ウルフシュレーゲルシュタインハウゼンベルガードルフ、シニア
...wc
は215文字であることを教えてくれます-これはキーの長さの上限hardではありませんが、存在することを心配する必要はありませんさらにもっと。
これは、ほとんどの実世界のハッシュテーブルに当てはまります。平均キー長は、使用中のキーの数とともに増加する傾向はありません。例外があります。たとえば、キー作成ルーチンは増分整数を埋め込んだ文字列を返す場合がありますが、キーの数を1桁増やすたびにキーの長さを1文字だけ増やすことになります。重要ではありません。
固定サイズのキーデータからハッシュを作成することもできます。たとえば、MicrosoftのVisual C++にはstd::hash<std::string>
の標準ライブラリ実装が同梱されており、文字列に沿って等間隔で10バイトだけを組み込むハッシュを作成します。 O(1)衝突後の検索側の動作)。ただし、ハッシュを作成する時間には上限があります。
また、完全なハッシュまたは大きなハッシュテーブルがない限り、バケットごとにおそらくいくつかのアイテムがあります。そのため、いずれかの時点で小さな線形検索に移ります。
一般的に本当ですが、ハッシュテーブルの素晴らしい点は、これらの「小さな線形検索」中に訪問したキーの数が-衝突に対する分離チェーンアプローチの場合-ハッシュテーブルload factor(バケットに対するキーの比率)。
たとえば、ロードファクターが1.0の場合、キーの数に関係なく、これらの線形検索の長さの平均は約1.58です( my answer here を参照)。 closed hashing の場合、もう少し複雑ですが、負荷率が高すぎなければそれほど悪くはありません。
ハッシュ関数はキー内のすべての情報を使用する必要がないため一定の時間になる可能性があり、十分に大きいテーブルは衝突を一定の時間近くにまで低下させる可能性があるため、技術的には真実です。
この種のポイントを見逃しています。あらゆる種類の連想データ構造は、最終的にキーのすべての部分で操作を実行する必要があります(キーの一部だけで不平等が判断される場合がありますが、通常、すべてのビットを考慮する必要があります)。少なくとも、キーを1回ハッシュしてハッシュ値を保存できます。また、十分に強力なハッシュ関数を使用している場合は、たとえば64ビットMD5-2つのキーが同じ値にハッシュする可能性さえも事実上無視するかもしれません(私が働いていた会社は、分散データベースに対して正確にそれを行いました:ハッシュ生成時間は、WAN全体のネットワーク送信と比較してまだ重要ではありませんでした)。そのため、キーを処理するためのコストを気にすることはあまりありません。これは、データ構造に関係なくキーを保存することに固有のものであり、前述のように、キーが増えても平均的に悪化する傾向はありません。
衝突を引き起こす十分な大きさのハッシュテーブルに関しては、それもポイントがありません。個別のチェーンの場合、任意の負荷係数で一定の平均衝突チェーン長があります。負荷係数が高くなると、それだけ長くなり、その関係は非線形になります。 SOユーザーHansのコメント 私の答えも上記にリンクされています
空でないバケットを条件とする平均バケット長は、効率のより良い尺度です。 a /(1-e ^ {-a})[aは負荷係数、eは2.71828 ...]
したがって、ロードファクターaloneは、挿入/消去/検索操作中に検索する必要がある衝突キーの平均数を決定します。個別のチェーンの場合、負荷率が低いときに定数に近づくだけでなく、常にalwaysになります。クレームには有効性がありますが、オープンアドレッシングの場合、一部の衝突要素は代替バケットにリダイレクトされ、他のキーの操作に干渉する可能性があります。そのため、負荷係数が高い(特に.8または.9を超える)場合、衝突チェーンの長さは劇的に悪化します。
ハッシュ関数とテーブルサイズが衝突を最小限に抑えるように選択されている限り、一定の時間のハッシュ関数を使用しないことを意味する場合が多いので、実際には事実です。
まあ、テーブルサイズは、密接なハッシュまたは個別のチェーンの選択を考慮して正味の負荷係数になるはずですが、ハッシュ関数が少し弱く、キーがあまりランダムではない場合、バケットの素数を持つことはしばしば削減に役立ちますコリジョンも(hash-value % table-size
はラップし、ハッシュ値の上位1つまたは2つのビットへの変更のみが、ハッシュテーブルの異なる部分に擬似ランダムに広がるバケットに解決されるようにします)。
最悪の場合にO(1)時間を取得できる2つの設定があります。
here からコピー
ここでの議論に基づいているようで、Xが(テーブル内の要素の数/ビンの数)の上限である場合、より良い答えはO(log(X))ビン検索の実装。