Làm thế nào để HashTables đối phó với va chạm?


97

Tôi đã nghe trong các lớp cấp bằng của mình rằng a HashTablesẽ đặt một mục nhập mới vào nhóm 'có sẵn tiếp theo' nếu mục nhập Khóa mới va chạm với mục nhập khác.

Làm thế nào để HashTablevẫn trả về Giá trị chính xác nếu sự va chạm này xảy ra khi gọi một người quay lại bằng phím xung đột?

Tôi giả sử rằng kiểu Keysare Stringvà các hashCode()trả về là giá trị mặc định được tạo bởi Java.

Nếu tôi triển khai hàm băm của riêng mình và sử dụng nó như một phần của bảng tra cứu (tức là a HashMaphoặc Dictionary), thì những chiến lược nào tồn tại để đối phó với các va chạm?

Tôi thậm chí đã thấy các ghi chú liên quan đến số nguyên tố! Thông tin không quá rõ ràng từ tìm kiếm của Google.

Câu trả lời:


92

Bảng băm xử lý các va chạm theo một trong hai cách.

Tùy chọn 1: Bằng cách đặt mỗi nhóm chứa một danh sách liên kết các phần tử được băm cho nhóm đó. Đây là lý do tại sao một hàm băm xấu có thể làm cho việc tra cứu trong bảng băm rất chậm.

Tùy chọn 2: Nếu tất cả các mục trong bảng băm đã đầy thì bảng băm có thể tăng số lượng nhóm mà nó có và sau đó phân phối lại tất cả các phần tử trong bảng. Hàm băm trả về một số nguyên và bảng băm phải lấy kết quả của hàm băm và sửa đổi nó theo kích thước của bảng theo cách có thể chắc chắn rằng nó sẽ vào thùng. Vì vậy, bằng cách tăng kích thước, nó sẽ rehash và chạy các tính toán modulo mà nếu bạn may mắn có thể gửi các đối tượng đến các nhóm khác nhau.

Java sử dụng cả tùy chọn 1 và 2 trong triển khai bảng băm của nó.


1
Trong trường hợp của tùy chọn đầu tiên, có lý do gì một danh sách liên kết được sử dụng thay vì một mảng hoặc thậm chí một cây tìm kiếm nhị phân không?

1
giải thích ở trên là cấp cao, tôi không nghĩ rằng nó tạo ra nhiều sự khác biệt như danh sách liên kết so với mảng. Tôi nghĩ rằng một cây tìm kiếm nhị phân sẽ là quá mức cần thiết. Ngoài ra, tôi nghĩ nếu bạn đào sâu vào những thứ như ConcurrentHashMap và những thứ khác có nhiều chi tiết triển khai cấp thấp có thể tạo ra sự khác biệt về hiệu suất, thì giải thích cấp cao ở trên không giải thích được.
ams

2
Nếu chuỗi được sử dụng, khi được trao chìa khóa, làm thế nào để chúng ta biết được vật phẩm nào cần lấy lại?
ChaoSXDemon

1
@ChaoSXDemon bạn có thể duyệt qua danh sách trong chuỗi bằng khóa, các khóa trùng lặp không phải là vấn đề, vấn đề là hai khóa khác nhau có cùng một mã băm.
ams

1
@ams: Cái nào được ưu tiên? là Có bất kỳ giới hạn nào cho xung đột Hash, sau điểm thứ 2 nào được thực thi bởi JAVA?
Shashank Vivek,

77

Khi bạn nói về "Bảng băm sẽ đặt một mục mới vào nhóm 'có sẵn tiếp theo' nếu mục nhập Khóa mới va chạm với mục khác.", Bạn đang nói về Chiến lược địa chỉ mở của Giải quyết xung đột của bảng băm.


Có một số chiến lược cho bảng băm để giải quyết xung đột.

Loại phương thức lớn đầu tiên yêu cầu các khóa (hoặc con trỏ đến chúng) được lưu trữ trong bảng, cùng với các giá trị được liên kết, bao gồm thêm:

  • Chuỗi riêng biệt

nhập mô tả hình ảnh ở đây

  • Mở địa chỉ

nhập mô tả hình ảnh ở đây

  • Băm kết hợp
  • Cuckoo băm
  • Robin Hood băm
  • Băm 2 lựa chọn
  • Băm lò cò

Một phương pháp quan trọng khác để xử lý va chạm là thay đổi kích thước động , hơn nữa có một số cách:

  • Thay đổi kích thước bằng cách sao chép tất cả các mục nhập
  • Thay đổi kích thước gia tăng
  • Phím đơn âm

CHỈNH SỬA : những thứ trên được mượn từ wiki_hash_table , bạn nên xem qua để có thêm thông tin.


3
"[...] yêu cầu các khóa (hoặc con trỏ tới chúng) được lưu trữ trong bảng, cùng với các giá trị được liên kết". Cảm ơn, đây là điểm không phải lúc nào cũng rõ ràng ngay lập tức khi đọc về cơ chế lưu trữ giá trị.
mtone

27

Có nhiều kỹ thuật có sẵn để xử lý va chạm. Tôi sẽ giải thích một số trong số họ

Chuỗi: Trong chuỗi chúng tôi sử dụng các chỉ mục mảng để lưu trữ các giá trị. Nếu mã băm của giá trị thứ hai cũng trỏ đến cùng một chỉ mục thì chúng tôi thay thế giá trị chỉ mục đó bằng một danh sách được liên kết và tất cả các giá trị trỏ đến chỉ mục đó được lưu trữ trong danh sách liên kết và chỉ mục mảng thực sự trỏ đến đầu danh sách được liên kết. Nhưng nếu chỉ có một mã băm trỏ đến một chỉ mục của mảng thì giá trị được lưu trực tiếp trong chỉ mục đó. Cùng một logic được áp dụng trong khi truy xuất các giá trị. Điều này được sử dụng trong Java HashMap / Hashtable để tránh va chạm.

Thăm dò tuyến tính: Kỹ thuật này được sử dụng khi chúng ta có nhiều chỉ mục trong bảng hơn giá trị được lưu trữ. Kỹ thuật thăm dò tuyến tính hoạt động dựa trên khái niệm tiếp tục tăng cho đến khi bạn tìm thấy một vị trí trống. Mã giả trông như thế này:

index = h(k) 

while( val(index) is occupied) 

index = (index+1) mod n

Kỹ thuật băm kép: Trong kỹ thuật này chúng ta sử dụng hai hàm băm h1 (k) và h2 (k). Nếu vị trí tại h1 (k) bị chiếm thì hàm băm thứ hai h2 (k) được sử dụng để tăng chỉ số. Mã giả trông như thế này:

index = h1(k)

while( val(index) is occupied)

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

Kỹ thuật thăm dò tuyến tính và kỹ thuật băm kép là một phần của kỹ thuật định địa chỉ mở và nó chỉ có thể được sử dụng nếu các vị trí có sẵn nhiều hơn số mục cần thêm. Nó tốn ít bộ nhớ hơn so với chuỗi vì không có cấu trúc bổ sung nào được sử dụng ở đây nhưng nó chậm do có nhiều chuyển động xảy ra cho đến khi chúng ta tìm thấy một khe trống. Ngoài ra trong kỹ thuật mở địa chỉ khi một vật phẩm được lấy ra khỏi một khe, chúng tôi đặt một bia mộ để chỉ ra rằng vật phẩm được lấy ra khỏi đây, đó là lý do tại sao nó trống rỗng.

Để biết thêm thông tin, hãy xem trang web này .


18

Tôi thực sự khuyên bạn nên đọc bài đăng trên blog này xuất hiện gần đây trên HackerNews: Cách HashMap hoạt động trong Java

Tóm lại, câu trả lời là

Điều gì sẽ xảy ra nếu hai đối tượng chính HashMap khác nhau có cùng một mã băm?

Chúng sẽ được lưu trữ trong cùng một nhóm nhưng không có nút tiếp theo của danh sách được liên kết. Và phương thức khóa bằng () sẽ được sử dụng để xác định cặp giá trị khóa chính xác trong HashMap.


3
HashMaps rất thú vị và chúng đi sâu! :)
Alex

1
Tôi nghĩ rằng câu hỏi là về HashTables không HashMap
Prashant Shubham

10

Tôi đã nghe nói trong các lớp cấp bằng của mình rằng HashTable sẽ đặt một mục nhập mới vào nhóm 'có sẵn tiếp theo' nếu mục nhập Khóa mới va chạm với mục nhập khác.

Điều này thực sự không đúng, ít nhất là đối với Oracle JDK (nó một chi tiết triển khai có thể khác nhau giữa các triển khai khác nhau của API). Thay vào đó, mỗi nhóm chứa danh sách các mục được liên kết trước Java 8 và một cây cân bằng trong Java 8 trở lên.

sau đó làm thế nào để HashTable vẫn trả về Giá trị chính xác nếu sự va chạm này xảy ra khi gọi lại một người bằng phím xung đột?

Nó sử dụng equals()để tìm mục nhập thực sự phù hợp.

Nếu tôi triển khai hàm băm của riêng mình và sử dụng nó như một phần của bảng tra cứu (tức là Bản đồ HashMap hoặc Từ điển), thì những chiến lược nào tồn tại để đối phó với các xung đột?

Có nhiều chiến lược xử lý va chạm với những ưu nhược điểm khác nhau. Mục nhập của Wikipedia về bảng băm cung cấp một cái nhìn tổng quan tốt.


Nó đúng cho cả hai HashtableHashMaptrong jdk 1.6.0_22 của Sun / Oracle.
Nikita Rybak

@Nikita: không chắc về Hashtable và tôi không có quyền truy cập vào các nguồn ngay bây giờ, nhưng tôi chắc chắn 100% HashMap sử dụng chuỗi và không phải thăm dò tuyến tính trong mọi phiên bản mà tôi từng thấy trong trình gỡ lỗi của mình.
Michael Borgwardt

@Michael Chà, tôi đang xem nguồn của HashMap public V get(Object key)ngay bây giờ (cùng một phiên bản như trên). Nếu bạn tìm thấy phiên bản chính xác nơi các danh sách liên kết đó xuất hiện, tôi rất muốn biết.
Nikita Rybak

@Niki: Tôi bây giờ nhìn vào cùng một phương pháp, và tôi thấy nó sử dụng một vòng lặp for để lặp thông qua một danh sách liên kết của Entrycác đối tượng:localEntry = localEntry.next
Michael Borgwardt

@Michael Xin lỗi, đó là sai lầm của tôi. Tôi đã giải thích mã theo cách sai. tự nhiên, e = e.nextlà không ++index. +1
Nikita Rybak

7

Cập nhật kể từ Java 8: Java 8 sử dụng cây tự cân bằng để xử lý va chạm, cải thiện trường hợp xấu nhất từ ​​O (n) thành O (log n) để tra cứu. Việc sử dụng cây tự cân bằng đã được giới thiệu trong Java 8 như là một cải tiến so với chuỗi (được sử dụng cho đến java 7), sử dụng danh sách liên kết và có trường hợp xấu nhất là O (n) để tra cứu (vì nó cần phải duyệt danh sách)

Để trả lời phần thứ hai của câu hỏi của bạn, việc chèn được thực hiện bằng cách ánh xạ một phần tử nhất định với một chỉ mục nhất định trong mảng bên dưới của bản đồ băm, tuy nhiên, khi xảy ra va chạm, tất cả các phần tử vẫn phải được bảo toàn (được lưu trữ trong cấu trúc dữ liệu phụ và không chỉ được thay thế trong mảng cơ bản). Điều này thường được thực hiện bằng cách đặt mỗi thành phần mảng (vị trí) là một cấu trúc dữ liệu thứ cấp (hay còn gọi là nhóm) và phần tử được thêm vào nhóm nằm trên chỉ số mảng đã cho (nếu khóa chưa tồn tại trong nhóm, trong trường hợp nào thì thay thế).

Trong quá trình tra cứu, khóa được băm thành chỉ số mảng tương ứng và tìm kiếm được thực hiện cho phần tử khớp với khóa (chính xác) trong nhóm đã cho. Bởi vì thùng không cần xử lý xung đột (so sánh các khóa trực tiếp), điều này giải quyết vấn đề xung đột, nhưng làm như vậy với chi phí phải thực hiện chèn và tra cứu trên cơ sở dữ liệu thứ cấp. Điểm mấu chốt là trong một bản đồ băm, cả khóa và giá trị đều được lưu trữ, và vì vậy ngay cả khi băm xung đột, các khóa được so sánh trực tiếp để bình đẳng (trong nhóm) và do đó có thể được xác định duy nhất trong nhóm.

Xử lý collission mang lại hiệu suất trong trường hợp xấu nhất là chèn và tra cứu từ O (1) trong trường hợp không xử lý cộng gộp đến O (n) để xâu chuỗi (danh sách liên kết được sử dụng làm cơ cấu dữ liệu thứ cấp) và O (log n) cho cây tự cân đối.

Người giới thiệu:

Java 8 đã đi kèm với những cải tiến / thay đổi sau đây của các đối tượng HashMap trong trường hợp có xung đột cao.

  • Hàm băm chuỗi thay thế được thêm vào trong Java 7 đã bị loại bỏ.

  • Các nhóm chứa một số lượng lớn các khóa xung đột sẽ lưu trữ các mục nhập của chúng trong một cây cân bằng thay vì một danh sách được liên kết sau khi đạt đến ngưỡng nhất định.

Những thay đổi trên đảm bảo hiệu suất của O (log (n)) trong các tình huống xấu nhất ( https://www.nagarro.com/en/blog/post/24/performance-improvement-for-hashmap-in-java-8 )


Bạn có thể giải thích cách chèn trường hợp xấu nhất cho HashMap danh sách liên kết chỉ là O (1) chứ không phải O (N) không? Có vẻ như với tôi rằng nếu bạn có tỷ lệ xung đột là 100% cho các khóa không trùng lặp, bạn sẽ phải duyệt qua mọi đối tượng trong HashMap để tìm cuối danh sách được liên kết, phải không? Tôi đang thiếu gì?
mbm29414

Trong trường hợp cụ thể của việc triển khai hashmap, bạn thực sự đúng, nhưng không phải vì bạn cần phải tìm cuối danh sách. Trong trường hợp chung triển khai danh sách liên kết, một con trỏ được lưu trữ ở cả phần đầu và phần đuôi, và do đó việc chèn có thể được thực hiện trong O (1) bằng cách gắn trực tiếp nút tiếp theo vào phần đuôi, nhưng trong trường hợp bản đồ băm, phương thức chèn cần đảm bảo không có bản sao và do đó phải tìm kiếm trong danh sách để kiểm tra xem phần tử đã tồn tại chưa, và do đó chúng ta kết thúc bằng O (n). Và do đó, nó là thuộc tính set được áp đặt trên danh sách liên kết gây ra O (N). Tôi sẽ sửa đổi câu trả lời của mình :)
Daniel Valland

4

Nó sẽ sử dụng phương thức bằng để xem liệu khóa có đồng đều hay không và đặc biệt là nếu có nhiều hơn một phần tử trong cùng một nhóm.


4

Vì có một số nhầm lẫn về thuật toán HashMap của Java đang sử dụng (trong triển khai Sun / Oracle / OpenJDK), đây là các đoạn mã nguồn liên quan (từ OpenJDK, 1.6.0_20, trên Ubuntu):

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

Phương pháp này (trích dẫn là từ dòng 355-371) được gọi khi nhìn lên một mục trong bảng, ví dụ từ get(), containsKey()và một số người khác. Vòng lặp for ở đây đi qua danh sách liên kết được tạo bởi các đối tượng mục nhập.

Đây là mã cho các đối tượng nhập (dòng 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)

}

Ngay sau đây là addEntry()phương pháp:

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

Thao tác này thêm Mục nhập mới ở phía trước nhóm, với liên kết đến Mục nhập cũ đầu tiên (hoặc null, nếu không có mục nào như vậy). Tương tự, removeEntryForKey()phương pháp này đi qua danh sách và chỉ xóa một mục nhập, giữ nguyên phần còn lại của danh sách.

Vì vậy, đây là danh sách mục nhập được liên kết cho từng nhóm và tôi rất nghi ngờ rằng điều này đã thay đổi từ _20thành _22, vì nó giống như thế này từ 1.2 trở đi.

(Mã này là (c) 1997-2007 Sun Microsystems và có sẵn theo GPL, nhưng để sao chép tốt hơn, hãy sử dụng tệp gốc, có trong src.zip trong mỗi JDK từ Sun / Oracle và cả trong OpenJDK.)


1
Tôi đã đánh dấu đây là wiki cộng đồng , vì nó không thực sự là một câu trả lời, cần thảo luận thêm về các câu trả lời khác. Trong nhận xét đơn giản là không đủ không gian cho các trích dẫn mã như vậy.
Paŭlo Ebermann

3

đây là một triển khai bảng băm rất đơn giản trong java. chỉ trong các dụng cụ put()get(), nhưng bạn có thể dễ dàng thêm bất cứ thứ gì bạn thích. nó dựa trên hashCode()phương thức của java được thực hiện bởi tất cả các đối tượng. bạn có thể dễ dàng tạo giao diện của riêng mình,

interface Hashable {
  int getHash();
}

và buộc nó phải được thực hiện bởi các phím nếu bạn muố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));
    }
}

2

Có nhiều phương pháp khác nhau để giải quyết va chạm, một số trong số đó là Chuỗi riêng biệt, Định địa chỉ mở, băm Robin Hood, Băm Cuckoo, v.v.

Java sử dụng Separate Chaining để giải quyết các xung đột trong bảng Hash. Đây là một liên kết tuyệt vời về cách nó xảy ra: http://javapapers.com/core-java/java-hashtable/

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.