HashTables çarpışmalarla nasıl başa çıkıyor?


101

Derece sınıflarımda HashTable, yeni Anahtar girişi bir başkasıyla çarpışırsa 'bir sonraki mevcut' kovasına yeni bir giriş koyacağını duydum .

HashTableBu çarpışma, çarpışma anahtarı ile geri çağırıldığında meydana gelirse , yine de doğru Değeri nasıl döndürür?

Bunu tahmin ediyorum Keysedilir Stringyazıp hashCode()diyelim Java tarafından oluşturulan döner varsayılan.

Kendi hash işlevimi uygularsam ve bunu bir arama tablosunun parçası olarak kullanırsam (yani a HashMapveya Dictionary), çarpışmalarla başa çıkmak için hangi stratejiler mevcuttur?

Asal sayılarla ilgili notlar bile gördüm! Google aramasından çok net olmayan bilgiler.

Yanıtlar:


95

Karma tablolar, çarpışmaları iki yoldan biriyle ele alır.

1. Seçenek: Her bir paketin, o gruba hashing uygulanmış bir bağlantılı öğe listesi içermesini sağlayarak. Bu nedenle, kötü bir karma işlevi, karma tablolardaki aramaları çok yavaş yapabilir.

Seçenek 2: Karma tablo girişlerinin tümü doluysa, karma tablo sahip olduğu grupların sayısını artırabilir ve ardından tablodaki tüm öğeleri yeniden dağıtabilir. Hash fonksiyonu bir tamsayı döndürür ve hash tablosunun hash fonksiyonunun sonucunu alması ve onu tablonun boyutuna göre modifiye etmesi gerekir, böylece pakete ulaşacağından emin olabilir. Dolayısıyla boyutu artırarak, modulo hesaplamalarını yeniden yükler ve çalıştırır, eğer şanslıysanız nesneleri farklı kovalara gönderebilir.

Java, karma tablo uygulamalarında hem seçenek 1 hem de 2'yi kullanır.


1
İlk seçenek durumunda, bir dizi veya hatta bir ikili arama ağacı yerine bağlantılı bir listenin kullanılmasının herhangi bir nedeni var mı?

1
Yukarıdaki açıklama üst düzeydir, bağlantılı liste ile dizi arasında çok fazla fark yarattığını düşünmüyorum. Bir ikili arama ağacının aşırıya kaçacağını düşünüyorum. Ayrıca ConcurrentHashMap ve diğerleri gibi şeyleri araştırırsanız, performans farkı yaratabilecek birçok düşük seviyeli uygulama detayı, yukarıdaki yüksek seviyeli açıklamanın hesaba katmadığını düşünüyorum.
ams

2
Zincirleme kullanılıyorsa, anahtar verildiğinde, hangi öğeyi geri alacağımızı nasıl bileceğiz?
ChaoSXDemon

1
@ChaoSXDemon, zincirdeki listeyi anahtarla gezebilirsiniz, yinelenen anahtarlar sorun değildir, sorun aynı hashcode'a sahip iki farklı anahtardır.
AMS

1
@ams: Hangisi tercih edilir? Hash çarpışması için herhangi bir sınır var mı, bundan sonra 2. nokta JAVA tarafından yapılır?
Shashank Vivek

79

"Karma Tablosu, yeni Anahtar girişi bir başkasıyla çarpışırsa 'sonraki kullanılabilir' kovaya yeni bir giriş yerleştirecek." Den bahsettiğinizde , karma tablosunun Çarpışma çözümlemesinin Açık adresleme stratejisinden bahsediyorsunuz .


Çarpışmayı çözmek için hash tablosu için birkaç strateji vardır.

Birinci tür büyük yöntem, anahtarların (veya bunlara işaret edenlerin) ilişkili değerlerle birlikte tabloda saklanmasını gerektirir, bunlar ayrıca şunları içerir:

  • Ayrı zincirleme

görüntü açıklamasını buraya girin

  • Açık adresleme

görüntü açıklamasını buraya girin

  • Birleşik hashing
  • Guguklu haşlama
  • Robin Hood hash işlemi
  • 2 seçenekli hashing
  • Seksek hashı

Çarpışmanın üstesinden gelmek için bir diğer önemli yöntem Dinamik yeniden boyutlandırmadır ve bunun birkaç yolu vardır:

  • Tüm girişleri kopyalayarak yeniden boyutlandırma
  • Artımlı yeniden boyutlandırma
  • Monoton tuşlar

DÜZENLEME : Yukarıdakiler, daha fazla bilgi almak için bir göz atmanız gereken wiki_hash_table'dan ödünç alınmıştır .


3
"[...], anahtarların (veya onlara işaret edenlerin) ilişkili değerlerle birlikte tabloda depolanmasını gerektirir". Teşekkürler, değerleri depolamak için mekanizmalar hakkında okurken her zaman hemen anlaşılmayan nokta budur.
mtone

27

Çarpışmayı idare etmek için birden fazla teknik mevcuttur. Bazılarını açıklayacağım

Zincirleme: Zincirlemede değerleri saklamak için dizi indekslerini kullanırız. İkinci değerin karma kodu da aynı indeksi gösteriyorsa, o indeks değerini bağlantılı bir listeyle değiştiririz ve bu indeksi gösteren tüm değerler bağlantılı listede saklanır ve gerçek dizi indeksi bağlantılı listenin başını gösterir. Ancak bir dizinin indeksini gösteren tek bir karma kod varsa, değer doğrudan o indekste saklanır. Değerler alınırken aynı mantık uygulanır. Bu, çakışmaları önlemek için Java HashMap / Hashtable'da kullanılır.

Doğrusal problama: Bu teknik, tabloda saklanacak değerlerden daha fazla indeksimiz olduğunda kullanılır. Doğrusal ölçüm tekniği, boş bir yuva bulana kadar artmaya devam etme kavramı üzerinde çalışır. Sözde kod şuna benzer:

index = h(k) 

while( val(index) is occupied) 

index = (index+1) mod n

Çift hashing tekniği: Bu teknikte iki hashing fonksiyonu h1 (k) ve h2 (k) kullanıyoruz. Hl (k) 'daki aralık dolu ise, indeksi arttırmak için ikinci hızlı arama fonksiyonu h2 (k) kullanılır. Sözde kod şuna benzer:

index = h1(k)

while( val(index) is occupied)

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

Doğrusal problama ve çift hashing teknikleri, açık adresleme tekniğinin bir parçasıdır ve yalnızca mevcut yuvalar eklenecek öğe sayısından fazlaysa kullanılabilir. Zincirlemeden daha az bellek alır çünkü burada kullanılan fazladan bir yapı yoktur, ancak boş bir yuva bulana kadar çok fazla hareket olması nedeniyle yavaştır. Ayrıca açık adresleme tekniğinde, bir öğe bir yuvadan kaldırıldığında, öğenin buradan kaldırıldığını ve bu yüzden boş olduğunu belirtmek için bir mezar taşı koyarız.

Daha fazla bilgi için bu siteye bakın .


18

Yakın zamanda HackerNews'te yayınlanan bu blog yazısını okumanızı şiddetle tavsiye ederim: HashMap Java'da nasıl çalışır?

Kısaca cevap

İki farklı HashMap anahtar nesnesi aynı hashcode'a sahipse ne olur?

Aynı pakette depolanacaklar ancak bağlantılı listenin sonraki düğümü olmayacak. HashMap'te doğru anahtar değer çiftini tanımlamak için anahtarlar equals () yöntemi kullanılacaktır.


3
HashMap'ler çok ilginç ve derine iniyorlar! :)
Alex

1
Sanırım soru HashMap değil HashTables ile ilgili
Prashant Shubham

10

Derece sınıflarımda, yeni Anahtar girişi bir başkasıyla çarpışırsa, bir HashTable'ın 'bir sonraki mevcut' kovasına yeni bir giriş koyacağını duydum.

Bu Oracle JDK için en az (o aslında doğru değildir olan API farklı uygulamaları arasında değişebilir bir uygulama ayrıntı). Bunun yerine, her kova Java 8'den önceki bağlantılı bir giriş listesi ve Java 8 veya üzeri bir dengeli ağaç içerir.

o zaman HashTable, çarpışma anahtarı ile geri çağırırken bu çarpışma meydana gelirse doğru Değeri nasıl döndürecektir?

equals()Gerçekte eşleşen girişi bulmak için kullanır .

Kendi hashing işlevimi uygularsam ve bunu bir arama tablosunun parçası olarak kullanırsam (yani bir HashMap veya Sözlük), çarpışmalarla başa çıkmak için hangi stratejiler mevcuttur?

Farklı avantajları ve dezavantajları olan çeşitli çarpışma idare stratejileri vardır. Wikipedia'nın karma tablolara girişi iyi bir genel bakış sağlar.


Bu ikisi için doğrudur Hashtableve HashMapGüneş / Oracle tarafından jdk 1.6.0_22 yılında.
Nikita Rybak

@Nikita: Hashtable hakkında emin değilim ve şu anda kaynaklara erişimim yok, ancak HashMap'in hata ayıklayıcımda gördüğüm her sürümde doğrusal problama değil zincirleme kullandığından% 100 eminim.
Michael Borgwardt

@Michael Eh, şu anda HashMap'in kaynağına bakıyorum public V get(Object key)(yukarıdaki ile aynı sürüm). Bu bağlantılı listelerin göründüğü kesin bir versiyon bulursanız, bilmek isterim.
Nikita Rybak

@Niki: Şimdi aynı yönteme bakıyorum ve bunun bağlantılı bir Entrynesne listesi boyunca yinelemek için bir for döngüsü kullandığını görüyorum :localEntry = localEntry.next
Michael Borgwardt

@Michael Üzgünüm, bu benim hatam. Kodu yanlış yorumladım. doğal e = e.nextolarak değil ++index. +1
Nikita Rybak

7

Java 8'den beri güncelleme: Java 8, çarpışma yönetimi için kendi kendini dengeleyen bir ağaç kullanır ve arama için en kötü durumu O (n) 'den O (log n)' ye iyileştirir. Kendinden dengeli bir ağacın kullanımı Java 8'de zincirlemeye göre bir gelişme olarak tanıtıldı (java 7'ye kadar kullanıldı), bu bağlantılı bir liste kullanıyor ve arama için en kötü durumda O (n) var (geçiş yapması gerektiğinden liste)

Sorunuzun ikinci bölümünü yanıtlamak için, ekleme, hashmap'in temel dizisindeki belirli bir dizine belirli bir öğeyi eşleyerek yapılır, ancak bir çarpışma meydana geldiğinde, tüm öğeler yine de korunmalıdır (ikincil bir veri yapısında saklanmalıdır) ve yalnızca temeldeki dizide değiştirilmez). Bu genellikle her bir dizi bileşenini (yuva) ikincil bir veri yapısı (diğer bir deyişle paket) haline getirerek yapılır ve öğe, verilen dizi dizininde yer alan kovaya eklenir (anahtar zaten pakette yoksa, hangi durumda değiştirilir).

Arama sırasında, anahtara karşılık gelen dizi dizinine hashing uygulanır ve verilen paketteki (tam) anahtarla eşleşen bir öğe için arama gerçekleştirilir. Kovanın çarpışmaları işlemesi gerekmediğinden (anahtarları doğrudan karşılaştırır), bu çarpışma sorununu çözer, ancak bunu ikincil veri yapısında ekleme ve arama yapma pahasına yapar. Kilit nokta, bir karma eşlemde hem anahtarın hem de değerin depolanması ve bu nedenle karma çarpışsa bile, anahtarların doğrudan eşitlik için (kova içinde) karşılaştırılması ve böylece kovada benzersiz bir şekilde tanımlanabilmesidir.

Çarpışma işleme, çarpışma işleminin olmaması durumunda O (1) 'den zincirleme için O (n)' ye (ikincil veri yapısı olarak bağlantılı bir liste kullanılır) ve O (log n) 'ye en kötü durumdaki ekleme ve arama performansını getirir. dengeli ağaç için.

Referanslar:

Java 8, yüksek çarpışma durumunda HashMap nesnelerinde aşağıdaki iyileştirmeler / değişikliklerle birlikte geldi.

  • Java 7'de eklenen alternatif String hash işlevi kaldırıldı.

  • Çok sayıda çarpışan anahtar içeren paketler, belirli eşiğe ulaşıldıktan sonra girişlerini bağlantılı bir liste yerine dengeli bir ağaçta saklayacaktır.

Yukarıdaki değişiklikler, en kötü senaryolarda O (log (n)) performansını garanti eder ( https://www.nagarro.com/en/blog/post/24/performance-improvement-for-hashmap-in-java-8 )


Bir bağlantılı liste HashMap için en kötü durum eklemesinin O (N) değil, sadece O (1) olduğunu açıklayabilir misiniz? Bana öyle geliyor ki, yinelenmeyen anahtarlar için% 100 çarpışma oranınız varsa, bağlantılı listenin sonunu bulmak için HashMap'teki her nesneyi geçmek zorunda kalıyorsunuz, değil mi? Neyi kaçırıyorum?
mbm29414

Hashmap uygulamasının özel durumunda, aslında haklısınız, ancak listenin sonunu bulmanız gerektiği için değil. Genel bir bağlantılı liste uygulamasında, bir işaretçi hem başa hem de kuyruğa depolanır ve bu nedenle, O (1) 'de bir sonraki düğümü doğrudan kuyruğa ekleyerek ekleme yapılabilir, ancak hashmap durumunda, ekleme yöntemi Yinelemelerin olmamasını sağlaması gerekir ve bu nedenle, elemanın zaten var olup olmadığını kontrol etmek için listeyi aramalı ve dolayısıyla O (n) ile sonuçlanır. Ve bu nedenle, O (N) 'ye neden olan bir bağlantılı listeye empoze edilen set özelliğidir. Cevabım için bir düzeltme yapacağım :)
Daniel Valland


4

Java'nın HashMap'in hangi algoritmayı (Sun / Oracle / OpenJDK uygulamasında) kullandığı konusunda bazı karışıklıklar olduğu için, burada ilgili kaynak kodu parçacıkları (Ubuntu'da OpenJDK, 1.6.0_20'den):

/**
 * 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;
}

Bu yöntem, örneğin, tablosunda bir giriş aranırken olarak adlandırılır (alıntı hatlar 371 ile 355 dan) get(), containsKey()ve diğerleri. Buradaki for döngüsü, giriş nesneleri tarafından oluşturulan bağlantılı listeden geçer.

Giriş nesnelerinin kodu (691-705 + 759 satırları):

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)

}

Hemen ardından şu addEntry()yöntem gelir :

/**
 * 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);
}

Bu, eski ilk girişe (veya böyle bir giriş yoksa boş değer) bir bağlantıyla birlikte yeni Girişi paketin önüne ekler . Benzer şekilde, removeEntryForKey()yöntem listeden geçer ve listenin geri kalanının bozulmadan kalmasına izin vererek yalnızca bir girişi silmeye özen gösterir.

Yani, burada her bir bölüm için bir bağlantılı giriş listesi ve çok bu değişti şüphe _20etmek _22Üzerinde 1.2 den böyle olduğu için.

(Bu kod (c) 1997-2007 Sun Microsystems'dir ve GPL altında mevcuttur, ancak kopyalamak için Sun / Oracle'dan her JDK'da ve ayrıca OpenJDK'da src.zip'de bulunan orijinal dosyayı daha iyi kullanın.)


1
Bunu topluluk wiki olarak işaretledim , çünkü bu gerçekten bir cevap değil, diğer cevaplar için biraz tartışma. Yorumlarda bu tür kod alıntıları için yeterli alan yoktur.
Paŭlo Ebermann

3

işte java'da çok basit bir karma tablo uygulaması. yalnızca araçlarda put()ve get(), ancak istediğinizi kolayca ekleyebilirsiniz. hashCode()tüm nesneler tarafından uygulanan java yöntemine dayanır . kendi arayüzünüzü kolayca oluşturabilirsiniz,

interface Hashable {
  int getHash();
}

ve isterseniz anahtarlar tarafından uygulanmaya zorlayın.

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));
    }
}

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.