HashTable은 충돌을 어떻게 처리합니까?


98

나는 학위 수업에서 HashTable 에서 새 키 항목이 다른 항목과 충돌하면 '다음 사용 가능'버킷에 새 항목을 넣는다 .

HashTable충돌 키로 다시 호출 할 때이 충돌이 발생하면 어떻게 올바른 값을 반환할까요?

나는 유형 Keys이 있다고 가정하고 Java가 생성 한 기본값을 반환합니다.StringhashCode()

자체 해싱 함수를 구현하고 조회 테이블 (예 : a HashMap또는 Dictionary)의 일부로 사용하는 경우 충돌을 처리하기위한 전략은 무엇입니까?

소수와 관련된 메모도 보았습니다! Google 검색에서 정보가 명확하지 않습니다.

답변:


93

해시 테이블은 두 가지 방법 중 하나로 충돌을 처리합니다.

옵션 1 : 각 버킷에 해당 버킷에 해시 된 요소의 링크 된 목록이 포함되도록합니다. 이것이 잘못된 해시 함수가 해시 테이블에서 조회를 매우 느리게 만들 수있는 이유입니다.

옵션 2 : 해시 테이블 항목이 모두 가득 차면 해시 테이블이 보유한 버킷 수를 늘린 다음 테이블의 모든 요소를 ​​재배포 할 수 있습니다. 해시 함수는 정수를 반환하고 해시 테이블은 해시 함수의 결과를 가져 와서 버킷에 도달 할 수 있도록 테이블의 크기에 맞게 수정해야합니다. 따라서 크기를 늘리면 운이 좋으면 객체를 다른 버킷으로 보낼 수있는 모듈로 계산을 다시 해시하고 실행합니다.

Java는 해시 테이블 구현에서 옵션 1과 2를 모두 사용합니다.


1
첫 번째 옵션의 경우 배열 또는 이진 검색 트리 대신 연결 목록이 사용되는 이유가 있습니까?

1
위의 설명은 수준이 높기 때문에 연결된 목록과 배열의 차이가별로 없다고 생각합니다. 이진 검색 트리가 과도하다고 생각합니다. 또한 ConcurrentHashMap 및 기타 성능 차이를 만들 수있는 많은 저수준 구현 세부 사항을 파헤 치면 위의 고수준 설명이 설명하지 않는다고 생각합니다.
ams

2
연결을 사용하는 경우 키가 주어 졌을 때 어떤 항목을 되 찾을 지 어떻게 알 수 있습니까?
ChaoSXDemon

1
@ChaoSXDemon 당신은 키로 체인의 목록을 순회 할 수 있습니다, 중복 키 문제는 동일한 해시 코드를 가진 두 개의 다른 키 문제가 아닙니다.
ams

1
@ams : 어느 것이 선호됩니까? 해시 충돌에 대한 제한이 있습니까?
Shashank Vivek

78

"새 키 항목이 다른 항목과 충돌하면 해시 테이블이 '다음 사용 가능한'버킷에 새 항목을 배치합니다."에 대해 이야기 할 때 해시 테이블 충돌 해결의 개방 주소 지정 전략 에 대해 이야기하고 있습니다.


충돌을 해결하기 위해 해시 테이블에 대한 몇 가지 전략이 있습니다.

첫 번째 종류의 큰 방법은 키 (또는 그에 대한 포인터)를 관련 값과 함께 테이블에 저장해야하며, 여기에는 다음이 추가로 포함됩니다.

  • 별도의 체인

여기에 이미지 설명 입력

  • 개방 주소 지정

여기에 이미지 설명 입력

  • 통합 해싱
  • 뻐꾸기 해싱
  • Robin Hood 해싱
  • 2-choice 해싱
  • Hopscotch 해싱

충돌을 처리하는 또 다른 중요한 방법은 Dynamic resizing 이며, 여기에는 여러 가지 방법이 있습니다.

  • 모든 항목을 복사하여 크기 조정
  • 증분 크기 조정
  • 단조로운 키

편집 : 위의 내용은 wiki_hash_table 에서 빌려 왔으며 더 많은 정보를 얻으려면 가야합니다.


3
"[...] 키 (또는 그에 대한 포인터)가 관련 값과 함께 테이블에 저장되어야합니다." 감사합니다. 이것은 값을 저장하는 메커니즘에 대해 읽을 때 항상 명확하지 않은 점입니다.
mtone 2015-06-28

27

충돌을 처리하는 데 사용할 수있는 여러 기술이 있습니다. 그들 중 일부를 설명하겠습니다

연결 : 연결 에서는 배열 인덱스를 사용하여 값을 저장합니다. 두 번째 값의 해시 코드도 동일한 인덱스를 가리키면 해당 인덱스 값을 연결된 목록으로 바꾸고 해당 인덱스를 가리키는 모든 값은 연결된 목록에 저장되고 실제 배열 인덱스는 연결된 목록의 머리를 가리 킵니다. 그러나 배열의 인덱스를 가리키는 해시 코드가 하나만있는 경우 값은 해당 인덱스에 직접 저장됩니다. 값을 검색하는 동안 동일한 논리가 적용됩니다. 충돌을 피하기 위해 Java HashMap / Hashtable에서 사용됩니다.

선형 프로빙 : 이 기술은 저장할 값보다 테이블에 더 많은 인덱스가있을 때 사용됩니다. 선형 프로빙 기술은 빈 슬롯을 찾을 때까지 계속 증가한다는 개념에서 작동합니다. 의사 코드는 다음과 같습니다.

index = h(k) 

while( val(index) is occupied) 

index = (index+1) mod n

이중 해싱 기술 : 이 기술에서는 두 개의 해싱 함수 h1 (k) 및 h2 (k)를 사용합니다. h1 (k)의 슬롯이 점유되면 두 번째 해싱 함수 h2 (k)가 인덱스를 증가시키는 데 사용됩니다. 의사 코드는 다음과 같습니다.

index = h1(k)

while( val(index) is occupied)

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

선형 프로빙 및 이중 해싱 기술은 개방형 주소 지정 기술의 일부이며 사용 가능한 슬롯이 추가 할 항목 수보다 많은 경우에만 사용할 수 있습니다. 여기에 추가 구조가 사용되지 않기 때문에 체이닝보다 메모리가 덜 사용되지만 빈 슬롯을 찾을 때까지 많은 움직임이 발생하기 때문에 느립니다. 또한 개방형 주소 지정 기술에서 항목이 슬롯에서 제거 될 때 항목이 여기에서 제거되었음을 나타내는 삭제 표시를 두어 비어있는 이유입니다.

자세한 내용은 이 사이트를 참조하십시오 .


18

최근 HackerNews에 게재 된이 블로그 게시물을 읽어 보시기 바랍니다. Java에서 HashMap이 작동하는 방법

간단히 말해서 대답은

두 개의 서로 다른 HashMap 키 객체가 동일한 해시 코드를 갖는 경우 어떻게됩니까?

동일한 버킷에 저장되지만 연결 목록의 다음 노드는 없습니다. 그리고 keys equals () 메소드는 HashMap에서 올바른 키 값 쌍을 식별하는 데 사용됩니다.


3
HashMaps는 매우 흥미롭고 깊이가 있습니다! :)
알렉스

1
나는 질문이 HashMap이 아닌 HashTables에 관한 것이라고 생각합니다
Prashant Shubham

10

학위 수업에서 새 키 항목이 다른 항목과 충돌하면 HashTable이 '다음 사용 가능한'버킷에 새 항목을 배치한다고 들었습니다.

이것은 오라클 JDK에 대한 최소한 (이것은 실제로 사실이 아니다 이다 API를 다른 구현에 따라서 다릅니다 수있는 구현 세부 사항). 대신 각 버킷에는 Java 8 이전의 연결된 항목 목록과 Java 8 이상의 균형 잡힌 트리가 포함됩니다.

그러면 충돌 키로 다시 호출 할 때이 충돌이 발생하면 HashTable이 올바른 값을 어떻게 반환합니까?

를 사용하여 equals()실제로 일치하는 항목을 찾습니다.

자체 해싱 함수를 구현하고 조회 테이블 (예 : HashMap 또는 사전)의 일부로 사용하는 경우 충돌을 처리하기위한 전략은 무엇입니까?

장점과 단점이 다른 다양한 충돌 처리 전략이 있습니다. 해시 테이블에 대한 Wikipedia 항목 은 좋은 개요를 제공합니다.


Sun / Oracle의 jdk 1.6.0_22 Hashtable와 둘 다 HashMap에 해당됩니다.
Nikita Rybak

@Nikita : Hashtable에 대해 잘 모르겠고, 지금은 소스에 액세스 할 수 없지만, 내 디버거에서 본 모든 단일 버전에서 HashMap이 체인화를 사용하고 선형 프로빙을 사용하지 않는다고 100 % 확신합니다.
Michael Borgwardt

@Michael 글쎄, 나는 지금 HashMap의 소스를 찾고 public V get(Object key)있습니다 (위와 같은 버전). 링크 된 목록이 표시되는 정확한 버전을 찾으면 알고 싶습니다.
Nikita Rybak

@Niki : 지금은 동일한 방법을보고 있는데 for 루프를 사용하여 연결된 Entry개체 목록을 반복하는 것을 봅니다 .localEntry = localEntry.next
Michael Borgwardt

@Michael 죄송합니다. 제 실수입니다. 코드를 잘못된 방식으로 해석했습니다. 당연히 e = e.next아닙니다 ++index. +1
Nikita Rybak

7

Java 8 이후 업데이트 : Java 8은 충돌 처리를 위해 자체 균형 트리를 사용하여 조회를위한 최악의 경우를 O (n)에서 O (log n)로 개선합니다. 자체 균형 트리의 사용은 연결 목록을 사용하고 조회에 대해 최악의 경우 O (n)을 갖는 체인 (Java 7까지 사용)에 대한 개선으로 Java 8에서 도입되었습니다 목록)

질문의 두 번째 부분에 답하기 위해 삽입은 주어진 요소를 해시 맵의 기본 배열에있는 지정된 인덱스에 매핑하여 수행되지만 충돌이 발생하면 모든 요소가 여전히 보존되어야합니다 (보조 데이터 구조에 저장 됨). , 기본 배열에서만 교체되지 않음). 이는 일반적으로 각 배열 구성 요소 (슬롯)를 보조 데이터 구조 (일명 버킷)로 만들고 요소가 지정된 배열 색인에있는 버킷에 추가됩니다 (키가 버킷에 아직 존재하지 않는 경우 대체되는 경우).

조회하는 동안 키는 해당 배열 인덱스로 해시되고 지정된 버킷의 (정확한) 키와 일치하는 요소에 대한 검색이 수행됩니다. 버킷이 충돌을 처리 할 필요가 없기 때문에 (키를 직접 비교) 충돌 문제는 해결되지만 보조 데이터 구조에서 삽입 및 조회를 수행해야하는 비용이 발생합니다. 요점은 해시 맵에서 키와 값이 모두 저장되므로 해시가 충돌하더라도 키가 버킷에서 동일한 지 직접 비교되어 버킷에서 고유하게 식별 될 수 있다는 것입니다.

충돌 처리는 충돌 처리가없는 경우 O (1)에서 연결 (연결된 목록이 보조 데이터 구조로 사용됨) 및 O (log n)에 대한 O (n)으로 삽입 및 조회의 최악의 경우 성능을 제공합니다. 균형 잡힌 나무를 위해.

참조 :

Java 8에는 충돌이 심한 경우 HashMap 객체가 다음과 같이 개선 / 변경되었습니다.

  • Java 7에 추가 된 대체 문자열 해시 함수가 제거되었습니다.

  • 충돌하는 키가 많은 버킷은 특정 임계 값에 도달 한 후 연결 목록 대신 균형 잡힌 트리에 항목을 저장합니다.

위의 변경 사항은 최악의 시나리오에서 O (log (n)) 성능을 보장합니다 ( https://www.nagarro.com/en/blog/post/24/performance-improvement-for-hashmap-in-java-8 )


연결 목록 HashMap에 대한 최악의 경우 삽입이 O (N)이 아닌 O (1) 인 이유를 설명 할 수 있습니까? 중복되지 않는 키에 대해 충돌 률이 100 %이면 연결된 목록의 끝을 찾기 위해 HashMap의 모든 개체를 탐색해야하는 것 같습니다. 맞죠? 내가 무엇을 놓치고 있습니까?
mbm29414

해시 맵 구현의 특정 경우에는 실제로 옳지 만 목록의 끝을 찾아야하기 때문이 아닙니다. 일반적인 경우에 링크드리스트 구현에서는 포인터가 헤드와 테일 모두에 저장되므로 다음 노드를 꼬리에 직접 연결하여 O (1)에서 삽입 할 수 있지만 해시 맵의 경우 삽입 메소드 중복이 없는지 확인해야하므로 요소가 이미 존재하는지 확인하기 위해 목록을 검색해야하므로 O (n)으로 끝납니다. 그래서 O (N)을 일으키는 것은 연결된 목록에 부과 된 set 속성입니다. 내 대답을 수정하겠습니다. :)
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.2에서 이와 같았 기 때문에 에서 _20로 변경된 것은 의심 스럽 _22습니다.

(이 코드는 (c) 1997-2007 Sun Microsystems이며 GPL에서 사용할 수 있지만 복사하려면 Sun / Oracle의 각 JDK 및 OpenJDK의 src.zip에 포함 된 원본 파일을 더 잘 사용하십시오.)


1
나는 이것을 커뮤니티 위키 로 표시했습니다. 실제로는 답변이 아니기 때문에 다른 답변에 대한 약간의 토론입니다. 주석에서는 그러한 코드 인용을위한 공간이 충분하지 않습니다.
Paŭlo Ebermann

3

다음은 자바로 구현 된 매우 간단한 해시 테이블입니다. 구현 put()및 에서만 가능 get()하지만 원하는 것을 쉽게 추가 할 수 있습니다. hashCode()모든 객체에 의해 구현되는 자바의 메소드 에 의존 합니다. 자신 만의 인터페이스를 쉽게 만들 수 있습니다.

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는 해시 테이블에서 충돌을 해결하기 위해 분리 체인을 사용합니다. 다음은 이러한 문제가 발생하는 방식에 대한 훌륭한 링크입니다. http://javapapers.com/core-java/java-hashtable/

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.