SO re JavaハッシュマップとそれらのO(1)
ルックアップ時間に関する興味深い主張を見てきました。誰かがこれがなぜそうなのか説明できますか?これらのハッシュマップが購入したハッシュアルゴリズムと大きく異なる場合を除き、衝突を含むデータセットが常に存在する必要があります。
その場合、ルックアップはO(n)
ではなくO(1)
になります。
誰かがare O(1)であるか、もしそうなら、どのようにこれを達成するかを説明できますか?
HashMapの特定の機能は、たとえばバランスの取れたツリーとは異なり、その動作が確率的であることです。これらの場合、最悪の事態が発生する確率の観点から複雑さについて話すのが通常最も役立ちます。ハッシュマップの場合、それはもちろん、マップがどの程度いっぱいになっているかに関して衝突する場合です。衝突の推定は非常に簡単です。
p衝突 = n /容量
したがって、適度な数の要素を持つハッシュマップでも、少なくとも1回の衝突が発生する可能性が高くなります。ビッグO表記法を使用すると、より魅力的なことができます。任意の固定定数kについて観察します。
O(n) = O(k * n)
この機能を使用して、ハッシュマップのパフォーマンスを向上させることができます。代わりに、最大2つの衝突の可能性を考えることができます。
p衝突x 2 =(n /容量)2
これはずっと低いです。余分な衝突を1回処理するコストはBig Oのパフォーマンスとは無関係であるため、実際にアルゴリズムを変更せずにパフォーマンスを改善する方法を見つけました。これを一般化できます
p衝突x k =(n /容量)k
そして、今では、任意の数の衝突を無視し、私たちが考慮しているよりも多くの衝突のごくわずかな可能性に終わります。アルゴリズムの実際の実装を変更することなく、正しいkを選択することにより、任意の小さなレベルまで確率を得ることができます。
これについては、ハッシュマップにO(1)アクセスと高い確率があると言って話します。 =
最悪の場合の動作と平均的な場合の(予想される)ランタイムを混同しているようです。前者は確かに一般的なハッシュテーブルではO(n)です(つまり、完全なハッシュを使用していません)が、これは実際にはほとんど関係ありません。
信頼できるハッシュテーブルの実装は、半分のまともなハッシュと相まって、O(1)の検索パフォーマンスを持ち、期待されるケースでは非常に小さな係数(実際には2)で、分散。
Javaでは、HashMapはhashCodeを使用してバケットを特定します。各バケットは、そのバケットにあるアイテムのリストです。比較のために等号を使用して、アイテムがスキャンされます。アイテムを追加する場合、特定の負荷率に達するとHashMapのサイズが変更されます。
そのため、いくつかの項目と比較する必要がある場合がありますが、一般的にはO(n)よりもO(1)にはるかに近いです。実用的な目的のために、あなたが知る必要があるのはそれだけです。
o(1)は、各ルックアップが単一のアイテムのみを検査することを意味するものではないことに注意してください。つまり、チェックされるアイテムの平均数が一定のw.r.tのままであることを意味します。コンテナ内のアイテムの数。したがって、100個のアイテムを持つコンテナ内のアイテムを見つけるのに平均4回の比較が必要な場合、10000個のアイテムを持つコンテナ内のアイテムを見つけるのに平均4回の比較を行う必要があります。特にハッシュテーブルが再ハッシュされるポイントの周辺や、アイテムの数が非常に少ない場合)。
そのため、バケットごとのキーの平均数が固定範囲内にある限り、コンテナがo(1)操作を行うことを衝突は防止しません。
これは古い質問ですが、実際には新しい答えがあります。
厳密に言えば、ハッシュマップは実際にはO(1)
ではないということです。要素の数がarbitrarily意的に大きくなると、最終的には一定時間で検索できなくなるためです(O表記は任意に大きくなる数値の項)。
しかし、リアルタイムの複雑さがO(n)
であるということにはなりません。バケットを線形リストとして実装する必要があるというルールがないためです。
実際、Java 8は、バケットがしきい値を超えるとTreeMaps
としてバケットを実装します。これにより、実際の時間がO(log n)
になります。
O(1+n/k)
ここで、k
はバケットの数です。
実装がk = n/alpha
を設定する場合、alpha
は定数であるため、O(1+alpha) = O(1)
です。
バケットの数(bと呼ぶ)が一定に保持されている場合(通常の場合)、ルックアップは実際にはO(n)です。
nが大きくなると、各バケットの要素数は平均n/bになります。衝突解決が通常の方法のいずれか(たとえば、リンクリスト)で行われる場合、ルックアップはO(n/b) = O(n)です。
O表記は、nがますます大きくなると何が起こるかについてです。特定のアルゴリズムに適用すると誤解を招く可能性があり、ハッシュテーブルがその典型です。処理する要素の数に基づいてバケットの数を選択します。 nがbとほぼ同じサイズの場合、ルックアップはほぼ一定時間ですが、Oはn→∞の制限に関して定義されているため、O(1)と呼ぶことはできません。
O(1)であるハッシュテーブルルックアップの標準的な説明は、厳密な最悪ケースのパフォーマンスではなく、平均ケースの予想時間を指すことを確立しました。連鎖(Javaのハッシュマップなど)との衝突を解決するハッシュテーブルの場合、これは技術的にはO(1 +α)で 適切なハッシュ関数 です。ここで、αはテーブルの負荷係数です。格納するオブジェクトの数がテーブルサイズよりも大きい一定の係数以下である限り、一定です。
また、厳密に言えば、決定論的なハッシュ関数に対してO(n)ルックアップを必要とする入力を構築することが可能であることも説明されています。しかし、平均検索時間とは異なる最悪時間expected時間を考慮することも興味深いです。連鎖を使用すると、O(1 +最も長い連鎖の長さ)、たとえばΘ(logn/ log logn)α= 1の場合。
一定の時間で予想される最悪の場合のルックアップを達成するための理論的な方法に興味がある場合は、 動的完全ハッシュ について読むことができます。
ハッシュ関数が非常に優れている場合にのみ、O(1)です。 Javaハッシュテーブルの実装は、不正なハッシュ関数から保護しません。
項目を追加するときにテーブルを拡大する必要があるかどうかは、ルックアップ時間に関するため、質問には関係ありません。
HashMap内の要素はリンクリスト(ノード)の配列として格納され、配列内の各リンクリストは、1つ以上のキーの一意のハッシュ値のバケットを表します。
HashMapにエントリを追加する際、キーのハッシュコードを使用して、配列内のバケットの場所を決定します。
location = (arraylength - 1) & keyhashcode
ここで、&はビット単位のAND演算子を表します。
例:100 & "ABC".hashCode() = 64 (location of the bucket for the key "ABC")
Get操作中、同じ方法を使用してキーのバケットの場所を決定します。最良のケースでは、各キーに一意のハッシュコードがあり、各キーに一意のバケットが生成されます。この場合、getメソッドはバケットの場所を特定し、定数O(1)である値を取得するためだけに時間を費やします。
最悪の場合、すべてのキーは同じハッシュコードを持ち、同じバケットに保存されます。これにより、リスト全体を走査してO(n)になります。
Java 8の場合、サイズが8を超えるとリンクリストバケットがTreeMapに置き換えられ、最悪の場合の検索効率がO(log n)に低下します。
アカデミックは別として、実用的な観点からは、HashMapsはパフォーマンスにわずかな影響を与えるものとして受け入れられるべきです(プロファイラーから別の指示がない限り)。
これは基本的に、アルゴリズム自体は実際には変更されないため、ほとんどのプログラミング言語のほとんどのハッシュテーブルの実装に当てはまります。
テーブルに衝突が存在しない場合は、1回のルックアップのみを行う必要があるため、実行時間はO(1)です。衝突が存在する場合、複数のルックアップを実行する必要があり、これによりパフォーマンスがO(n)に向かって低下します。
衝突を回避するために選択したアルゴリズムに依存します。実装で個別のチェーンを使用する場合、すべてのデータ要素が同じ値にハッシュされるという最悪のシナリオが発生します(たとえば、ハッシュ関数の選択が不適切)。その場合、データルックアップはリンクリストでの線形検索、つまりO(n)と変わりません。ただし、その発生の可能性はごくわずかであり、最適な検索と平均的な検索は一定、つまりO(1)のままです。
理論的な場合にのみ、ハッシュコードが常に異なり、すべてのハッシュコードのバケットも異なる場合、O(1)が存在します。それ以外の場合、それは一定の順序です。つまり、ハッシュマップの増分では、検索の順序は一定のままです。
もちろん、ハッシュマップのパフォーマンスは、指定されたオブジェクトのhashCode()関数の品質に依存します。ただし、衝突の可能性が非常に低いように関数が実装されている場合、非常に優れたパフォーマンスが得られます(これは厳密にはO(1) in every可能な場合ではありませんしかし、それはmostの場合です)。
たとえば、Oracle JREのデフォルトの実装では、乱数(オブジェクトインスタンスに格納されて変更されないようにします。ただし、バイアスロックを無効にしますが、それは別の議論です)とても低い。