HashMap get / putの複雑さ


131

HashMap get/put操作はO(1)であると言うことに慣れています。ただし、ハッシュの実装によって異なります。デフォルトのオブジェクトハッシュは、実際にはJVMヒープの内部アドレスです。get/putO(1)であると主張するだけで十分でしょうか?

利用可能なメモリは別の問題です。javadocsからわかるHashMap load factorように、0.75 である必要があります。JVMに十分なメモリがなくload factor、制限を超えた場合はどうなりますか?

したがって、O(1)は保証されていないようです。それは意味がありますか、それとも何か不足していますか?


1
償却後の複雑さの概念を調べたい場合があります。たとえば、こちらをご覧ください。stackoverflow.com/ questions / 3949217 / time-complexity-of-hash
Dr G

3
正解です-それは償却された O(1)です-その最初の部分を忘れないでください、そしてあなたはこれらの種類の質問を持っていないでしょう:)
エンジニア

時間の複雑さの最悪のケースは、Java 1.8以降のO(logN)です。
Tarun Kolla

回答:


216

それは多くのものに依存します。イッツ通常自体は一定の時間でまともなハッシュで、O(1)...しかし、あなたは計算するのに長い時間がかかるハッシュを持つことができ、かつ同じハッシュコードを返し、ハッシュマップ内の複数の項目がある場合は、getそれらを繰り返してequals、一致を見つけるためにそれらのそれぞれを呼び出す必要があります。

最悪の場合、a HashMapは同じハッシュバケット内のすべてのエントリをウォークスルーするため(たとえば、すべてが同じハッシュコードを持つ場合)、O(n)ルックアップを実行します。幸い、その最悪のシナリオは、私の経験では、現実にはそれほど頻繁には発生しません。したがって、いいえ、O(1)は確かに保証されていません。ただし、使用するアルゴリズムとデータ構造を検討する際には、通常、これを想定する必要があります。

JDK 8では、HashMapキーを順序付けのために比較できる場合、密集したバケットがツリーとして実装されるように微調整されているため、同じハッシュコードを持つエントリが多数ある場合でも、複雑度はO(log n)。もちろん、等価性と順序付けが異なるキータイプがある場合、問題が発生する可能性があります。

そして、はい、ハッシュマップ用の十分なメモリがない場合、問題が発生します...しかし、それは、使用するデータ構造に関係なく当てはまります。


@marcog:単一の検索で O(n log n)を想定していますか?それは私にはふさわしく聞こえます。もちろん、ハッシュ関数と等式関数の複雑さに依存しますが、マップのサイズに依存することはほとんどありません。
Jon Skeet、2010

1
@marcog:では、O(n log n)だと何を想定していますか?nアイテムの挿入?
Jon Skeet、2010

1
良い答えは+1です。このウィキペディアエントリのようなハッシュテーブルのリンクを回答に含めていただけますか?そうすれば、より興味のある読者は、なぜあなたがあなたの答えを出したのを理解するための核心に達することができます。
David Weiser、

2
@SleimanJneidi:キーがComparable <T> `を実装していない場合もそうですが、時間があれば、答えを更新します。
Jon Skeet

1
@ ip696:はい、put「償却済みのO(1)」です。通常はO(1)、たまにO(n)ですが、バランスを取るのに十分な場合はめったにありません。
Jon Skeet

9

デフォルトのハッシュコードがアドレスであるかどうかはわかりません-少し前に、ハッシュコード生成のためにOpenJDKソースを読んで、それが少し複雑であることを覚えています。それでも、おそらく良いディストリビューションを保証するものではありません。ただし、ハッシュマップでキーとして使用するクラスのほとんどがデフォルトのハッシュコードを使用するため、これはある程度問題があります。それらは独自の実装を提供しますが、これは優れているはずです。

その上、あなたが知らないかもしれないこと(これもソースの読み取りに基づいています-これは保証されていません)は、HashMapが使用する前にハッシュを攪拌し、単語全体からエントロピーを最下位ビットに混合することです。最も巨大なハッシュマップを除くすべてに必要です。これは、特にそれ自体を行わないハッシュを処理するのに役立ちますが、それが表示されるような一般的なケースは考えられません。

最後に、テーブルが過負荷になると、並列リンクリストのセットに退化し、パフォーマンスはO(n)になります。具体的には、トラバースされるリンクの数は、平均で負荷係数の半分になります。


6
くそったれ。私は、これをフリップする携帯電話のタッチスクリーンでタイプする必要がなかったとしたら、ジョンシートを殴って打たれたかもしれないと信じることにしました。そのためのバッジがありますよね?
トムアンダーソン、

8

HashMap操作は、hashCode実装の依存要素です。理想的なシナリオでは、すべてのオブジェクトに一意のハッシュコードを提供する(ハッシュの衝突がない)優れたハッシュ実装を例に挙げると、最良、最悪、および平均のケースのシナリオはO(1)になります。hashCodeの不適切な実装が常に1またはハッシュの衝突があるそのようなハッシュを返すシナリオを考えてみましょう。この場合、時間の複雑さはO(n)になります。

メモリに関する質問の2番目の部分に進むと、メモリの制約はJVMによって処理されます。


8

ハッシュマップはO(n/m)nアイテムの数とmサイズである場合、平均であると既に述べられています。原則として、全体がO(n)クエリ時間で単一にリンクされたリストに崩壊する可能性があることも言及されました。(これはすべて、ハッシュの計算が一定の時間であることを前提としています)。

ただし、あまり言及されないのは、少なくとも確率で1-1/n(つまり、99.9%の確率で1000アイテムの場合)、最大のバケットがこれ以上満たされないということO(logn)です。したがって、バイナリ検索ツリーの平均的な複雑さを一致させます。(そして定数は良いです、より厳しい限界はです(log n)*(m/n) + O(1))。

この理論上の限界に必要なのは、適度に優れたハッシュ関数を使用することだけです(Wikipedia:Universal Hashingを参照してください。これは、と同じくらい簡単な場合もありますa*x>>m)。そしてもちろん、ハッシュする値をあなたに与えた人は、あなたがランダム定数をどのように選んだかを知りません。

TL; DR:非常に高い確率では、ハッシュマップの最悪のget / putの複雑さはO(logn)です。


(そして、これはランダムなデータを想定していないことに注意してください。確率は純粋にハッシュ関数の選択から生じます)
Thomas Ahle

ハッシュマップでのルックアップの実行時の複雑さについても同じ質問があります。定数要素が削除されることになっているので、それはO(n)のようです。1 / mは定数係数であるため、O(n)を残して削除されます。
nickdu 2017

4

同意する:

  • O(1)の一般的な償却後の複雑さ
  • 不適切なhashCode()実装は、複数の衝突を引き起こす可能性があります。つまり、最悪の場合、すべてのオブジェクトが同じバケットに移動するため、各バケットがによってサポートされている場合はO(N)になりListます。
  • Java 8以降、HashMap動的に各バケットで使用されるノード(リンクされたリスト)をTreeNodes(リストが8要素を超えると赤黒ツリー)に置き換えられ、O(logN)の最悪のパフォーマンスになります。

しかし、100%正確にしたい場合、これは完全な真実ではありません。hashCode()キーの実装とキーのタイプObject(不変/キャッシュまたはコレクション)も、厳密な意味で実際の複雑さに影響を与える可能性があります。

次の3つのケースを想定します。

  1. HashMap<Integer, V>
  2. HashMap<String, V>
  3. HashMap<List<E>, V>

彼らは同じ複雑さを持っていますか?まあ、最初のものの償却済みの複雑さは、予想通り、O(1)です。ただし、残りの部分ではhashCode()、ルックアップ要素の計算も必要です。つまり、アルゴリズムで配列とリストを走査する必要があるかもしれません。

上記のすべての配列/リストのサイズがkであると仮定しましょう。次いで、HashMap<String, V>及びHashMap<List<E>, V>O(k)は複雑さを償却と同様に、O(なりますK + logN個 Java8で)最悪のケース。

* Stringキーは不変であり、Javaは結果をhashCode()プライベート変数hashにキャッシュするため、キーの使用はより複雑なケースであり、一度しか計算されないことに注意してください。

/** Cache the hash code for the string */
    private int hash; // Default to 0

ただし、JavaのString.hashCode()実装がhash == 0計算前にチェックを行っているため、上記のケースにも独自の最悪のケースがありhashCodeます。しかし、ちょっと、hashcode「f5a5a608」のようにゼロのaを出力する空でない文字列があります。ここを参照してください。この場合、メモ化は役に立たない可能性があります。


2

実際にはO(1)ですが、これは実際にはひどく数学的に意味のない単純化です。O()表記は、問題のサイズが無限大になる傾向がある場合のアルゴリズムの動作を示しています。ハッシュマップのget / putは、限られたサイズのO(1)アルゴリズムのように機能します。この制限は、コンピューターのメモリとアドレッシングの観点からはかなり大きいですが、無限大にはほど遠いものです。

ハッシュマップのget / putがO(1)であると言うとき、実際には、get / putに必要な時間はほぼ一定であり、ハッシュマップが可能な限り、ハッシュマップ内の要素の数に依存しないと言う必要があります実際のコンピューティングシステムで提示されます。問題がそのサイズを超えて、より大きなハッシュマップが必要な場合、しばらくすると、記述可能な異なる要素が不足するにつれて、1つの要素を表すビット数も確実に増加します。たとえば、ハッシュマップを使用して32ビットの数値を格納し、後で問題のサイズを大きくして、ハッシュマップに2 ^ 32ビットを超える要素が含まれる場合、個々の要素は32ビットを超えて記述されます。

個々の要素を記述するために必要なビット数はlog(N)です。Nは要素の最大数であるため、getおよびputは実際にはO(log N)です。

これをO(log n)であるツリーセットと比較すると、ハッシュセットはO(long(max(n))であり、特定の実装ではmax(n)であるため、これは単にO(1)であると感じます。固定されており、変化せず(保存するオブジェクトのサイズはビット単位で測定されます)、ハッシュコードを計算するアルゴリズムは高速です。

最後に、データ構造内の要素を見つけることがO(1)である場合、薄い空気から情報を作成します。n要素のデータ構造を持っているので、1つの要素をnの方法で選択できます。これで、log(n)ビット情報をエンコードできます。これをゼロビットでエンコードできる場合(つまり、O(1)が意味するもの)、無限に圧縮するZIPアルゴリズムを作成しました。


O(log(n) * log(max(n)))それでは、ツリーセットの複雑さはどうでしょうか。すべてのノードでの比較はよりスマートかもしれませんが、最悪の場合O(log(max(n))、すべてのビットを検査する必要がありますよね?
maaartinus
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.