HashMap
get/put
操作はO(1)であると言うのに慣れています。ただし、ハッシュの実装に依存します。デフォルトのオブジェクトハッシュは、実際にはJVMヒープの内部アドレスです。 get/put
がO(1)であると主張するのに十分であると確信していますか?
使用可能なメモリは別の問題です。 javadocsから理解したように、HashMap
load factor
は0.75でなければなりません。 JVMに十分なメモリがなく、load factor
が制限を超えた場合はどうなりますか?
したがって、O(1)は保証されていないようです。それは理にかなっていますか、何か不足していますか?
それは多くのことに依存します。それは通常 O(1)であり、それ自体は一定の時間であるまともなハッシュを使用しています...しかし、計算に長い時間がかかるハッシュを持つことができますandもしあれば同じハッシュコードを返すハッシュマップ内の複数のアイテムである場合、get
は、一致するものを見つけるためにそれぞれに対してequals
を呼び出してそれらを反復処理する必要があります。
最悪の場合、HashMap
は、同じハッシュバケット内のすべてのエントリをウォークスルーするために(たとえば、すべてが同じハッシュコードを持つ場合)O(n)ルックアップを持ちます。幸いなことに、その最悪のシナリオは、私の経験では、実際の生活ではあまり出てきません。いいえ、O(1)は保証されていません-しかし、通常、どのアルゴリズムとデータ構造を使用するかを検討する際に想定すべきことです。
JDK 8では、HashMap
が調整され、順序付けのためにキーを比較できる場合、密度の高いバケットがツリーとして実装されるため、同じハッシュコードのエントリが多数ある場合でも、複雑さはO(log n)です。もちろん、平等と順序が異なるキータイプを使用している場合、問題が発生する可能性があります。
そして、はい、もしあなたがハッシュマップのための十分なメモリを持っていないなら、あなたはトラブルに陥るでしょう...しかし、それはあなたが使用するどんなデータ構造でも本当です。
デフォルトのハッシュコードがアドレスであるかどうかはわかりません-少し前にハッシュコード生成用のOpenJDKソースを読みましたが、もう少し複雑なものであることを覚えています。それでも、おそらく良い分布を保証するものではありません。ただし、ハッシュマップのキーとして使用するクラスがデフォルトのハッシュコードを使用することはほとんどないため、ある程度は意味がありません。独自の実装を提供します。
その上、あなたが知らないかもしれない(これはソースの読み取りに基づいている-保証されていない)ことは、ハッシュを使用する前にハッシュをかき混ぜて、Word全体からエントロピーを最下位ビットに混合することです最も大きなハッシュマップを除くすべてに必要です。これは、特にそれ自体を実行しないハッシュの処理に役立ちますが、それを目にする一般的なケースは考えられません。
最後に、テーブルがオーバーロードされると、テーブルが一連の並列リンクリストに縮退します。パフォーマンスはO(n)になります。具体的には、トラバースされるリンクの数は、平均して負荷係数の半分になります。
n
がアイテムの数でm
がサイズの場合、ハッシュマップは平均でO(n/m)
であると既に述べました。また、原則として、全体がO(n)
クエリ時間で単一リンクリストに崩壊する可能性があることも言及されています。 (これはすべて、ハッシュの計算が一定時間であると仮定しています)。
しかし、あまり言及されていないのは、少なくとも1-1/n
(したがって、99.9%の確率で1000アイテムの場合)の確率で、O(logn)
を超えて最大のバケットが満たされないということです。したがって、バイナリ検索ツリーの平均複雑度に一致します。 (そして、定数は適切で、より厳密な境界は(log n)*(m/n) + O(1)
です)。
この理論的な限界に必要なのは、合理的に優れたハッシュ関数を使用することだけです(Wikipedia: niversal Hashing を参照してください。a*x>>m
と同じくらい簡単です)。そしてもちろん、ハッシュする値を提供してくれる人は、あなたがどのようにランダム定数を選択したかを知りません。
TL; DR:非常に高い確率で、ハッシュマップの最悪の場合のget/putの複雑さはO(logn)
です。
HashMap操作は、hashCode実装の依存要素です。理想的なシナリオでは、すべてのオブジェクトに一意のハッシュコードを提供する適切なハッシュ実装(ハッシュコリジョンなし)を言えば、最良、最悪、平均のケースシナリオはO(1)になります。 hashCodeの不適切な実装が常に1またはハッシュ衝突のあるそのようなハッシュを返すシナリオを考えてみましょう。この場合、時間の複雑さはO(n)になります。
メモリについての質問の2番目の部分に移ると、JVMはyesメモリ制約を処理します。
同意する:
hashCode()
実装により複数の衝突が発生する可能性があります。つまり、最悪の場合、すべてのオブジェクトが同じバケットに移動するため、O(N)各バケットがList
によってサポートされている場合。HashMap
は、各バケットで使用されるNodes(リンクされたリスト)をTreeNodes(リストが8要素より大きくなると赤黒ツリー)に動的に置き換えて、O( logN)。しかし、100%正確にしたい場合、これは完全な真実ではありません。 hashCode()
の実装、キーのタイプObject
(不変/キャッシュ、またはコレクションであること)も厳密な意味で実際の複雑さに影響する可能性があります。
次の3つのケースを想定してみましょう。
HashMap<Integer, V>
HashMap<String, V>
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()
実装はhashCode
を計算する前にhash == 0
をチェックしているため、上記にも独自の最悪のケースがあります。しかし、「f5a5a608」など、ゼロのhashcode
を出力する空でない文字列があります。 here を参照してください。この場合、メモ化は役に立たない可能性があります。
実際には、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))であり、これはO(1)であると単純に感じます。特定の実装max(n)は固定され、変化せず(保存するオブジェクトのサイズはビット単位で測定されます)、ハッシュコードを計算するアルゴリズムは高速です。
最後に、データ構造内の要素を見つけることがO(1)である場合、私たちは薄情から情報を作成します。 n要素のデータ構造を持つことで、1つの要素をn個の異なる方法で選択できます。これにより、log(n)ビット情報をエンコードできます。それをゼロビットでエンコードできる場合(つまり、O(1)が意味するもの)、無限圧縮Zipアルゴリズムを作成しました。