HashTablesはどのように衝突に対処しますか?


97

私の学位クラスではHashTable、新しいキーエントリが別のキーエントリと衝突すると、「次の利用可能」バケットに新しいエントリが配置されると聞きました。

HashTableコリジョンキーを使用して1つのバックを要求したときにこのコリジョンが発生した場合、どのようにして正しい値が返されますか?

Keysare Stringタイプであり、hashCode()Javaによって生成されたデフォルトを返すと想定しています。

独自のハッシュ関数を実装し、それをルックアップテーブルの一部として使用する場合(つまり、HashMapまたはDictionary)、衝突に対処するためにどのような戦略がありますか?

素数に関連するノートを見たことさえある!Google検索からはあまり明確ではない情報。

回答:


92

ハッシュテーブルは、2つの方法のいずれかで衝突を処理します。

オプション1:各バケットに、そのバケットにハッシュされる要素のリンクリストを含める。これが、不適切なハッシュ関数がハッシュテーブルの検索を非常に遅くする理由です。

オプション2:ハッシュテーブルのエントリがすべて一杯の場合、ハッシュテーブルはバケットの数を増やし、テーブル内のすべての要素を再配布できます。ハッシュ関数は整数を返し、ハッシュテーブルはハッシュ関数の結果を受け取り、テーブルのサイズに対してそれを変更して、確実にバケットに入れられるようにする必要があります。したがって、サイズを大きくすることで、再ハッシュしてモジュロ計算を実行します。運が良ければ、オブジェクトを別のバケットに送信する可能性があります。

Javaは、ハッシュテーブルの実装でオプション1と2の両方を使用します。


1
最初のオプションの場合、配列またはバイナリ検索ツリーの代わりにリンクリストが使用される理由はありますか?

1
上記の説明は高レベルです。リンクされたリストと配列の違いはそれほど大きくないと思います。二分探索木はやり過ぎだと思います。また、ConcurrentHashMapやその他のことを掘り下げた場合、パフォーマンスの違いをもたらす可能性のある多くの低レベルの実装の詳細があり、上記の高レベルの説明では説明できません。
AMS

2
チェーンが使用されている場合、キーが指定されたときに、どのアイテムを取得するかをどのようにして知ることができますか?
ChaoSXDemon

1
@ChaoSXDemonキーでチェーン内のリストをトラバースできます。重複するキーは問題ではありません。問題は、同じ2つの異なるキーにハッシュコードがあることです。
AMS

1
@ams:どちらが好ましいですか?ハッシュの衝突に制限はありますか?その後、JAVAによって2番目のポイントが実行されますか?
Shashank Vivek 2017年

77

「新しいキーエントリが他のエントリと衝突した場合、ハッシュテーブルは新しいエントリを「次に利用可能な」バケットに配置する」と述べたとき、ハッシュテーブルの衝突解決のオープンアドレッシング戦略について話していることになります。


衝突を解決するためのハッシュテーブルの戦略はいくつかあります。

最初の種類の大きなメソッドでは、キー(またはそれらへのポインター)を、関連する値と共にテーブルに格納する必要があります。

  • 個別の連鎖

ここに画像の説明を入力してください

  • オープンアドレッシング

ここに画像の説明を入力してください

  • 合体ハッシュ
  • カッコウハッシング
  • ロビンフッドハッシュ
  • 2つの選択肢のハッシュ
  • 石蹴りハッシュ

衝突を処理するもう1つの重要な方法は、動的サイズ変更です。これには、さらにいくつかの方法があります。

  • すべてのエントリをコピーしてサイズを変更する
  • 増分サイズ変更
  • 単調なキー

編集:上記はwiki_hash_tableから借用したものであり、詳細を確認するためにここに行く必要があります。


3
「[...]は、キー(またはキーへのポインター)が、関連する値とともにテーブルに格納されている必要があります。」おかげで、これは値を格納するメカニズムについて読むときに常にすぐに明確になるわけではない点です。
mtone 2015年

27

衝突を処理するために利用できる複数の手法があります。それらのいくつかを説明します

連鎖: 連鎖では、配列インデックスを使用して値を格納します。2番目の値のハッシュコードも同じインデックスを指す場合、そのインデックス値をリンクリストで置き換え、そのインデックスを指すすべての値がリンクリストに格納され、実際の配列インデックスはリンクリストの先頭を指します。ただし、配列のインデックスを指すハッシュコードが1つしかない場合、値はそのインデックスに直接格納されます。値を取得するときに同じロジックが適用されます。これは、衝突を回避するためにJava HashMap / Hashtableで使用されます。

線形プローブ:この手法は、テーブルに格納する値よりも多くのインデックスがある場合に使用されます。線形プローブ手法は、空のスロットが見つかるまでインクリメントを続けるという概念で機能します。擬似コードは次のようになります。

index = h(k) 

while( val(index) is occupied) 

index = (index+1) mod n

ダブルハッシュ手法:この手法では、2つのハッシュ関数h1(k)とh2(k)を使用します。h1(k)のスロットが占有されている場合は、2番目のハッシュ関数h2(k)を使用してインデックスを増分します。擬似コードは次のようになります。

index = h1(k)

while( val(index) is occupied)

index = (index + h2(k)) mod n

リニアプローブおよびダブルハッシュテクニックはオープンアドレッシングテクニックの一部であり、使用可能なスロットが追加されるアイテムの数よりも多い場合にのみ使用できます。ここでは余分な構造が使用されていないため、チェーンよりもメモリの使用量は少なくなりますが、空のスロットが見つかるまで多くの動きが発生するため、処理速度は遅くなります。また、アイテムがスロットから削除されるときのオープンアドレス指定手法では、アイテムがここから削除されることを示すためにトゥームストーンを配置します。

詳細については、このサイトを参照してください。


18

最近HackerNewsに掲載されたこのブログ投稿を読むことを強くお勧めします: JavaでのHashMapの動作

要するに、答えは

2つの異なるHashMapキーオブジェクトに同じハッシュコードがあるとどうなりますか?

それらは同じバケットに保存されますが、リンクリストの次のノードは保存されません。そして、keys equals()メソッドは、HashMapで正しいキーと値のペアを識別するために使用されます。


3
HashMapは非常に興味深いものであり、深く掘り下げています!:)
アレックス

1
質問はHashMapではなくHashTableに関するものだと思います
Prashant Shubham '11 / 10/17

10

私の学位クラスでは、新しいKeyエントリが別のエントリと衝突した場合、HashTableが新しいエントリを「次に利用可能な」バケットに入れると聞きました。

これは実際には、少なくともOracle JDKには当てはまりません(これ、APIの異なる実装間で異なる可能性ある実装の詳細です)。代わりに、各バケットには、Java 8より前のエントリのリンクリストと、Java 8以降のバランスツリーが含まれています。

次に、衝突キーで1つを要求するときにこの衝突が発生した場合、HashTableはどのようにして正しい値を返しますか?

equals()実際に一致するエントリを見つけるために使用します。

独自のハッシュ関数を実装し、それをルックアップテーブル(つまり、HashMapまたはディクショナリ)の一部として使用する場合、衝突に対処するためにどのような戦略がありますか?

さまざまな長所と短所を持つさまざまな衝突処理戦略があります。 ウィキペディアのハッシュテーブルに関するエントリは、概要を示しています。


これは、Sun / Oracleのjdk 1.6.0_22 Hashtableとその両方に当てはまりHashMapます。
Nikita Rybak

@Nikita:Hashtableについては不明ですが、現在ソースにアクセスできませんが、HashMapがデバッガーで見たすべてのバージョンで線形プローブではなく、チェーンプローブを使用していることを100%確信しています。
マイケルボルグワート

@Michaelさて、私はpublic V get(Object key)今HashMapのソースを調べています(上記と同じバージョン)。これらのリンクされたリストが表示される正確なバージョンを見つけた場合は、知りたいと思います。
Nikita Rybak

@Niki:私は今、同じ方法で探しています、と私はそれがのリンクリストを反復処理するループのために使用して参照Entryオブジェクト:localEntry = localEntry.next
マイケルBorgwardt

@マイケル申し訳ありませんが、それは私の間違いです。コードを間違って解釈しました。当然でe = e.nextはありません++index。+1
Nikita Rybak

7

Java 8以降の更新: Java 8は衝突処理に自己バランスツリーを使用し、最悪のケースをルックアップのO(n)からO(log n)に改善します。自己バランスツリーの使用は、連鎖リスト(java 7まで使用されていました)の改善としてJava 8で導入されました。リンクリストを使用し、ルックアップにO(n)の最悪のケースがあります(トラバースする必要があるため)リスト)

質問の2番目の部分に答えるために、挿入は、ハッシュマップの基になる配列の特定のインデックスに特定の要素をマッピングすることによって行われますが、衝突が発生した場合でも、すべての要素を保持する必要があります(2次データ構造に保存) 、および基になる配列で置換されるだけではありません)。これは通常、各配列コンポーネント(スロット)をセカンダリデータ構造(別名バケット)にすることで行われ、要素は指定された配列インデックスにあるバケットに追加されます(キーがバケットにまだ存在しない場合、いずれの場合も置き換えられます)。

ルックアップ中に、キーは対応する配列インデックスにハッシュされ、指定されたバケット内の(正確な)キーに一致する要素が検索されます。バケットは衝突を処理する必要がない(キーを直接比較する)ため、これは衝突の問題を解決しますが、セカンダリデータ構造で挿入とルックアップを実行する必要があるという代償を伴います。重要な点は、ハッシュマップにはキーと値の両方が格納されるため、ハッシュが衝突した場合でも、キーが(バケット内で)等しいかどうか直接比較されるため、バケット内で一意に識別できるということです。

衝突処理は、連鎖処理(リンクリストがセカンダリデータ構造として使用されます)およびO(log n)のためにO(n)への衝突処理がない場合に、O(1)からの挿入とルックアップの最悪の場合のパフォーマンスをもたらします。自己バランスツリーの場合。

参照:

Java 8には、衝突が多い場合のHashMapオブジェクトの次の改善/変更が含まれています。

  • Java 7で追加された代替のStringハッシュ関数は削除されました。

  • 多数の衝突するキーを含むバケットは、特定のしきい値に達した後、リンクされたリストではなく平衡ツリーにエントリを格納します。

上記の変更により、最悪のシナリオでO(log(n))のパフォーマンスが保証されます(https://www.nagarro.com/en/blog/post/24/performance-improvement-for-hashmap-in-java-8


リンクリストHashMapの最悪の場合の挿入がO(1)のみで、O(N)ではないことを説明できますか?重複していないキーの衝突率が100%の場合、リンクされたリストの最後を見つけるためにHashMap内のすべてのオブジェクトをトラバースする必要がありますよね?何が欠けていますか?
mbm29414 2018

ハッシュマップ実装の特定のケースでは、あなたは実際には正しいですが、リストの最後を見つける必要があるからではありません。一般的な場合のリンクリストの実装では、ポインターは先頭と末尾の両方に格納されるため、次のノードを末尾に直接アタッチすることでO(1)に挿入できますが、ハッシュマップの場合、挿入メソッドは重複がないことを確認する必要があるため、リストを検索して要素がすでに存在するかどうかを確認する必要があるため、最終的にO(n)になります。そして、O(N)を引き起こしているのは、リンクリストに課せられたセットプロパティです。私は私の答えを修正します:)
Daniel Valland


4

JavaのHashMapが(Sun / Oracle / OpenJDK実装で)使用しているアルゴリズムについて混乱があるため、ここに関連するソースコードスニペット(UbuntuのOpenJDK、1.6.0_20から):

/**
 * Returns the entry associated with the specified key in the
 * HashMap.  Returns null if the HashMap contains no mapping
 * for the key.
 */
final Entry<K,V> getEntry(Object key) {
    int hash = (key == null) ? 0 : hash(key.hashCode());
    for (Entry<K,V> e = table[indexFor(hash, table.length)];
         e != null;
         e = e.next) {
        Object k;
        if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
            return e;
    }
    return null;
}

このメソッド(引用は355行から371行まで)は、テーブルからエントリを検索するときに呼び出されます。たとえばget()containsKey()およびいくつかの他。ここのforループは、エントリオブジェクトによって形成されたリンクリストを通過します。

ここにエントリオブジェクトのコード(行691-705 + 759):

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;
    final int hash;

    /**
     * Creates new entry.
     */
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    }

  // (methods left away, they are straight-forward implementations of Map.Entry)

}

この直後にaddEntry()メソッドがあります:

/**
 * Adds a new entry with the specified key, value and hash code to
 * the specified bucket.  It is the responsibility of this
 * method to resize the table if appropriate.
 *
 * Subclass overrides this to alter the behavior of put method.
 */
void addEntry(int hash, K key, V value, int bucketIndex) {
    Entry<K,V> e = table[bucketIndex];
    table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
    if (size++ >= threshold)
        resize(2 * table.length);
}

これにより、バケットの前面に新しいエントリが追加され、古い最初のエントリへのリンクが追加されます(そのようなエントリがない場合はnull)。同様に、removeEntryForKey()メソッドはリストを調べて、1つのエントリのみを削除し、リストの残りの部分はそのままにします。

それで、ここに各バケットのリンクされたエントリリストがあります。これは1.2からはこのようになっていたので、からに変更され_20たの_22ではないでしょうか。

(このコードは(c)1997-2007 Sun Microsystemsであり、GPLの下で利用できますが、コピーをより適切に行うには、Sun / Oracleの各JDKのsrc.zipに含まれているオリジナルファイルとOpenJDKも使用します。)


1
私はこれをコミュニティWikiとしてマークしました。これは実際には答えではないので、他の答えに対する議論が増えます。コメントでは、そのようなコード引用のための十分なスペースではありません。
–PaŭloEbermann、2011

3

これは、Javaでの非常に単純なハッシュテーブルの実装です。はとのみを実装put()してget()いますが、好きなものを簡単に追加できます。hashCode()すべてのオブジェクトによって実装されるjavaのメソッドに依存しています。独自のインターフェースを簡単に作成できます

interface Hashable {
  int getHash();
}

必要に応じて、キーによって強制的に実装されます。

public class Hashtable<K, V> {
    private static class Entry<K,V> {
        private final K key;
        private final V val;

        Entry(K key, V val) {
            this.key = key;
            this.val = val;
        }
    }

    private static int BUCKET_COUNT = 13;

    @SuppressWarnings("unchecked")
    private List<Entry>[] buckets = new List[BUCKET_COUNT];

    public Hashtable() {
        for (int i = 0, l = buckets.length; i < l; i++) {
            buckets[i] = new ArrayList<Entry<K,V>>();
        }
    }

    public V get(K key) {
        int b = key.hashCode() % BUCKET_COUNT;
        List<Entry> entries = buckets[b];
        for (Entry e: entries) {
            if (e.key.equals(key)) {
                return e.val;
            }
        }
        return null;
    }

    public void put(K key, V val) {
        int b = key.hashCode() % BUCKET_COUNT;
        List<Entry> entries = buckets[b];
        entries.add(new Entry<K,V>(key, val));
    }
}

2

衝突解決にはさまざまな方法があります。そのいくつかは、個別チェーン、オープンアドレス指定、ロビンフードハッシュ、カッコウハッシュなどです。

Javaはハッシュtables.Hereで衝突を解決するための別のチェーンを使用していますそれが起こるどのように偉大なリンクです: http://javapapers.com/core-java/java-hashtable/

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.